Rahvatarkus better ui

This commit is contained in:
Mihkel Martin Kasterpalu 2025-02-11 18:33:17 +02:00
parent 6847b6605c
commit 35ad2da1c3
10 changed files with 312 additions and 116 deletions

View file

@ -0,0 +1,18 @@
import { Tabs as TabsPrimitive } from "bits-ui";
import Content from "./tabs-content.svelte";
import List from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";
const Root = TabsPrimitive.Root;
export {
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value,
...restProps
}: TabsPrimitive.ContentProps = $props();
</script>
<TabsPrimitive.Content
bind:ref
class={cn(
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
className
)}
{value}
{...restProps}
/>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ListProps = $props();
</script>
<TabsPrimitive.List
bind:ref
class={cn(
"bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value,
...restProps
}: TabsPrimitive.TriggerProps = $props();
</script>
<TabsPrimitive.Trigger
bind:ref
class={cn(
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow",
className
)}
{value}
{...restProps}
/>

View file

@ -0,0 +1,16 @@
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { questions } from '$lib/server/db/schema';
import { sql } from 'drizzle-orm';
export async function GET() {
const results = await db
.select({
poolSize: sql`COUNT(CASE WHEN answer_count < 5 THEN 1 END)`
})
.from(questions);
return json({
size: Number(results[0].poolSize)
});
}

View file

@ -8,36 +8,47 @@ import { zod } from 'sveltekit-superforms/adapters';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
export const load: LayoutServerLoad = async ({ fetch, locals }) => { export const load: LayoutServerLoad = async ({ fetch, locals }) => {
const { session } = locals; const { session } = locals;
if (!session?.data?.userId) { if (!session?.data?.userId) {
await session.setData({ userId: nanoid() }); await session.setData({ userId: nanoid() });
await session.save(); await session.save();
} }
const user = session.data.userId; const user = session.data.userId;
const res = await fetch('/api/rahvatarkus/question') let question: Question | undefined = undefined;
.then(async (res) => {
const data = await res.json();
return { ok: res.ok, data: data };
})
.then((data) => {
return data;
});
let question: Question | undefined = undefined; const poolSize = await fetch('/api/rahvatarkus/pool')
.then((res) => {
return res.json();
})
.then((data) => {
return data.size;
});
if (res.ok) { if (poolSize !== 0) {
question = res.data; const res = await fetch('/api/rahvatarkus/question')
} .then(async (res) => {
const data = await res.json();
return { ok: res.ok, data: data };
})
.then((data) => {
return data;
});
return { if (res.ok) {
user: user, question = res.data;
question: question, }
question_form: await superValidate(zod(questionSchema)), }
answer_form: await superValidate({ questionId: question?.id }, zod(answerSchema), {
errors: false return {
}) user: user,
}; question: question,
poolSize,
question_form: await superValidate(zod(questionSchema)),
answer_form: await superValidate({ questionId: question?.id }, zod(answerSchema), {
errors: false
})
};
}; };

View file

@ -1,13 +1,27 @@
<script lang="ts"> <script lang="ts">
import * as Tabs from '$lib/components/ui/tabs/index.js';
import QuestionForm from './question-form.svelte'; import QuestionForm from './question-form.svelte';
import AnswerForm from './answer-form.svelte'; import AnswerForm from './answer-form.svelte';
let { data, children } = $props(); let { data, children } = $props();
$inspect(data); $inspect(data);
let firstTab = $derived(data?.question ? 'answer' : 'question');
</script> </script>
<QuestionForm {data} /> <Tabs.Root value={firstTab} class="flex w-full max-w-md flex-col items-center gap-2">
<AnswerForm {data} /> <Tabs.List class="grid w-full grid-cols-2">
<Tabs.Trigger value="answer">Vasta</Tabs.Trigger>
<Tabs.Trigger value="question">Küsi</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="answer" class="w-full">
<AnswerForm {data} />
</Tabs.Content>
<Tabs.Content value="question" class="w-full">
<QuestionForm {data} />
</Tabs.Content>
</Tabs.Root>
{@render children()} {@render children()}

View file

@ -1,65 +1,90 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types.js'; import type { PageData } from './$types.js';
import { MediaQuery } from 'svelte/reactivity';
import * as Accordion from '$lib/components/ui/accordion/index.js'; import * as Accordion from '$lib/components/ui/accordion/index.js';
import * as Pagination from '$lib/components/ui/pagination/index.js'; import * as Pagination from '$lib/components/ui/pagination/index.js';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import ChevronLeft from 'lucide-svelte/icons/chevron-left';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
$inspect(data); const isDesktop = new MediaQuery('(min-width: 768px)');
const siblingCount = $derived(isDesktop.current ? 1 : 0);
</script> </script>
{#await data.streamed.archive} <header class="mb-12 mt-32 flex flex-col items-center text-center font-title">
<p>loading</p> <h1 class="mb-1 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
{:then archive} Mida rahvas teab?
<Accordion.Root type="multiple" class="w-2/3 space-y-6"> </h1>
{#each archive.data as question} </header>
<Accordion.Item disabled={!(question.answers?.length > 0)} value={question.id}>
<Accordion.Trigger>{question.content}?</Accordion.Trigger> <div class="h-full w-full max-w-prose">
<Accordion.Content> {#await data.streamed.archive}
<ol class="ml-6 list-decimal [&>li]:mt-2"> <p>loading</p>
{:then archive}
<Accordion.Root type="multiple" class="space-y-6">
{#each archive.data as question}
<Accordion.Item disabled={!(question.answers?.length > 0)} value={question.id}>
<Accordion.Trigger>{question.content}?</Accordion.Trigger>
<Accordion.Content>
{#each question.answers as answer} {#each question.answers as answer}
<li> <blockquote
class="border-l-2 bg-muted/25 pl-4 italic leading-7 [&:not(:first-child)]:mt-3"
>
{answer.content} {answer.content}
</li> </blockquote>
{/each} {/each}
</ol> </Accordion.Content>
</Accordion.Content> </Accordion.Item>
</Accordion.Item> {/each}
{/each} </Accordion.Root>
</Accordion.Root> <Pagination.Root
<Pagination.Root onPageChange={(value: number) => {
onPageChange={(value: number) => { goto(`?leht=${value}`);
goto(`?leht=${value}`); }}
}} count={archive.meta.total}
count={archive.meta.total} perPage={data.pageSize}
perPage={data.pageSize} page={data.page}
page={data.page} {siblingCount}
> class="my-8"
{#snippet children({ pages, currentPage })} >
<Pagination.Content> {#snippet children({ pages, currentPage })}
<Pagination.Item> <Pagination.Content class="flex items-center">
<Pagination.PrevButton /> <Pagination.Item>
</Pagination.Item> <Pagination.PrevButton
{#each pages as page (page.key)} class="hover:bg-dark-10 active:scale-98 inline-flex size-6 items-center justify-center rounded-lg bg-transparent disabled:cursor-not-allowed disabled:text-muted-foreground hover:disabled:bg-transparent sm:size-10 md:mr-4"
{#if page.type === 'ellipsis'} >
<Pagination.Item> <ChevronLeft class="size-4 sm:size-6" />
<Pagination.Ellipsis /> </Pagination.PrevButton>
</Pagination.Item> </Pagination.Item>
{:else} <div class="flex items-center sm:gap-2.5">
<Pagination.Item isVisible={currentPage === page.value}> {#each pages as page (page.key)}
<Pagination.Link {page} isActive={currentPage === page.value}> {#if page.type === 'ellipsis'}
{page.value} <Pagination.Item>
</Pagination.Link> <Pagination.Ellipsis />
</Pagination.Item> </Pagination.Item>
{/if} {:else}
{/each} <Pagination.Item isVisible={currentPage === page.value}>
<Pagination.Item> <Pagination.Link {page} isActive={currentPage === page.value}>
<Pagination.NextButton /> {page.value}
</Pagination.Item> </Pagination.Link>
</Pagination.Content> </Pagination.Item>
{/snippet} {/if}
</Pagination.Root> {/each}
{/await} </div>
<Pagination.Item>
<Pagination.NextButton
class="hover:bg-dark-10 active:scale-98 inline-flex size-6 items-center justify-center rounded-lg bg-transparent disabled:cursor-not-allowed disabled:text-muted-foreground hover:disabled:bg-transparent sm:size-10 md:ml-4"
>
<ChevronRight class="size-4 sm:size-6" />
</Pagination.NextButton>
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
{/await}
</div>

View file

@ -6,11 +6,15 @@
import { zodClient } from 'sveltekit-superforms/adapters'; import { zodClient } from 'sveltekit-superforms/adapters';
import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms'; import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms';
import * as Card from '$lib/components/ui/card/index.js';
import * as Form from '$lib/components/ui/form/index.js'; import * as Form from '$lib/components/ui/form/index.js';
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
let { data }: { data: { answer_form: SuperValidated<Infer<FormSchema>>; question: Question } } = let {
$props(); data
}: {
data: { answer_form: SuperValidated<Infer<FormSchema>>; question: Question; poolSize: number };
} = $props();
const form = superForm(data.answer_form, { const form = superForm(data.answer_form, {
validators: zodClient(formSchema), validators: zodClient(formSchema),
@ -27,25 +31,53 @@
const { form: formData, enhance } = form; const { form: formData, enhance } = form;
</script> </script>
{#if data.question} <Card.Root>
<form method="POST" class="w-2/3 space-y-6" use:enhance action="?/answer"> <Card.Header>
<Form.Field {form} name="answer"> <Card.Title>Vasta vajajale</Card.Title>
<Form.Control> <Card.Description>
{#snippet children({ props })} {#if !data.question}
<Form.Label>{data.question.content}?</Form.Label> {#if data.poolSize === 0}
<Input {...props} bind:value={$formData.answer} /> Rahval said kõik küsimused otsa!
{/snippet} {:else}
</Form.Control> Oled kõigile vastanud, kuid enda küsimustel vastused puudu.
<Form.FieldErrors /> {/if}
</Form.Field> {:else}
<Form.Field {form} name="questionId"> Tänutäheks saad vastu ühe küsimuse küsida.
<Form.Control> {/if}
{#snippet children({ props })} </Card.Description>
<Input type="hidden" {...props} bind:value={$formData.questionId} /> </Card.Header>
{/snippet} {#if !data.question}
</Form.Control> <Card.Content>
<Form.FieldErrors /> {#if data.poolSize === 0}
</Form.Field> <p class="text-sm leading-6">Sellise erandjuhuga saad ühe korra niisama küsida!</p>
<Form.Button>Vasta</Form.Button> {:else}
</form> <p class="text-sm leading-6">Äkki tahaksid su sõbrad vastata või on meil mingi küsimus?</p>
{/if} {/if}
</Card.Content>
{:else}
<form method="POST" use:enhance action="?/answer">
<Card.Content>
<Form.Field {form} name="answer">
<Form.Control>
{#snippet children({ props })}
<Form.Label>{data.question.content}?</Form.Label>
<Input {...props} bind:value={$formData.answer} />
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="questionId">
<Form.Control>
{#snippet children({ props })}
<Input type="hidden" {...props} bind:value={$formData.questionId} />
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
</Card.Content>
<Card.Footer>
<Form.Button>Vasta</Form.Button>
</Card.Footer>
</form>
{/if}
</Card.Root>

View file

@ -1,14 +1,24 @@
<script lang="ts"> <script lang="ts">
import { formSchema, type FormSchema } from './question-schema'; import { formSchema, type FormSchema } from './question-schema';
import type { Question } from '$lib/types';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { zodClient } from 'sveltekit-superforms/adapters'; import { zodClient } from 'sveltekit-superforms/adapters';
import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms'; import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms';
import * as Form from '$lib/components/ui/form/index.js'; import * as Form from '$lib/components/ui/form/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
let { data }: { data: { question_form: SuperValidated<Infer<FormSchema>> } } = $props(); let {
data
}: {
data: {
question_form: SuperValidated<Infer<FormSchema>>;
question: Question;
poolSize: number;
};
} = $props();
const form = superForm(data.question_form, { const form = superForm(data.question_form, {
validators: zodClient(formSchema), validators: zodClient(formSchema),
@ -26,16 +36,25 @@
const { form: formData, enhance } = form; const { form: formData, enhance } = form;
</script> </script>
<form method="POST" class="w-2/3 space-y-6" use:enhance action="?/question"> <Card.Root>
<Form.Field {form} name="question"> <Card.Header>
<Form.Control> <Card.Title>Küsi rahvalt</Card.Title>
{#snippet children({ props })} <Card.Description>Sul on alles 0 küsimust. Vasta teistele kõigepealt!</Card.Description>
<Form.Label>Uus küsimus</Form.Label> </Card.Header>
<Input {...props} bind:value={$formData.question} /> <form method="POST" use:enhance action="?/question">
{/snippet} <Card.Content>
</Form.Control> <Form.Field {form} name="question">
<Form.Description>Küsi ükskõik mida sellelt kollektiiv intelektilt.</Form.Description> <Form.Control>
<Form.FieldErrors /> {#snippet children({ props })}
</Form.Field> <Form.Label>Küsimus rahvale</Form.Label>
<Form.Button>Küsi</Form.Button> <Input {...props} bind:value={$formData.question} />
</form> {/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
</Card.Content>
<Card.Footer>
<Form.Button>Küsi</Form.Button>
</Card.Footer>
</form>
</Card.Root>