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';
export const load: LayoutServerLoad = async ({ fetch, locals }) => {
const { session } = locals;
const { session } = locals;
if (!session?.data?.userId) {
await session.setData({ userId: nanoid() });
await session.save();
}
if (!session?.data?.userId) {
await session.setData({ userId: nanoid() });
await session.save();
}
const user = session.data.userId;
const user = session.data.userId;
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;
});
let question: Question | undefined = undefined;
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) {
question = res.data;
}
if (poolSize !== 0) {
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 {
user: user,
question: question,
question_form: await superValidate(zod(questionSchema)),
answer_form: await superValidate({ questionId: question?.id }, zod(answerSchema), {
errors: false
})
};
if (res.ok) {
question = res.data;
}
}
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">
import * as Tabs from '$lib/components/ui/tabs/index.js';
import QuestionForm from './question-form.svelte';
import AnswerForm from './answer-form.svelte';
let { data, children } = $props();
$inspect(data);
let firstTab = $derived(data?.question ? 'answer' : 'question');
</script>
<QuestionForm {data} />
<AnswerForm {data} />
<Tabs.Root value={firstTab} class="flex w-full max-w-md flex-col items-center gap-2">
<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()}

View file

@ -1,65 +1,90 @@
<script lang="ts">
import type { PageData } from './$types.js';
import { MediaQuery } from 'svelte/reactivity';
import * as Accordion from '$lib/components/ui/accordion/index.js';
import * as Pagination from '$lib/components/ui/pagination/index.js';
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();
$inspect(data);
const isDesktop = new MediaQuery('(min-width: 768px)');
const siblingCount = $derived(isDesktop.current ? 1 : 0);
</script>
{#await data.streamed.archive}
<p>loading</p>
{:then archive}
<Accordion.Root type="multiple" class="w-2/3 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>
<ol class="ml-6 list-decimal [&>li]:mt-2">
<header class="mb-12 mt-32 flex flex-col items-center text-center font-title">
<h1 class="mb-1 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Mida rahvas teab?
</h1>
</header>
<div class="h-full w-full max-w-prose">
{#await data.streamed.archive}
<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}
<li>
<blockquote
class="border-l-2 bg-muted/25 pl-4 italic leading-7 [&:not(:first-child)]:mt-3"
>
{answer.content}
</li>
</blockquote>
{/each}
</ol>
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
<Pagination.Root
onPageChange={(value: number) => {
goto(`?leht=${value}`);
}}
count={archive.meta.total}
perPage={data.pageSize}
page={data.page}
>
{#snippet children({ pages, currentPage })}
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item>
<Pagination.Ellipsis />
</Pagination.Item>
{:else}
<Pagination.Item isVisible={currentPage === page.value}>
<Pagination.Link {page} isActive={currentPage === page.value}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
{/await}
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
<Pagination.Root
onPageChange={(value: number) => {
goto(`?leht=${value}`);
}}
count={archive.meta.total}
perPage={data.pageSize}
page={data.page}
{siblingCount}
class="my-8"
>
{#snippet children({ pages, currentPage })}
<Pagination.Content class="flex items-center">
<Pagination.Item>
<Pagination.PrevButton
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"
>
<ChevronLeft class="size-4 sm:size-6" />
</Pagination.PrevButton>
</Pagination.Item>
<div class="flex items-center sm:gap-2.5">
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item>
<Pagination.Ellipsis />
</Pagination.Item>
{:else}
<Pagination.Item isVisible={currentPage === page.value}>
<Pagination.Link {page} isActive={currentPage === page.value}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
</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 { 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 { Input } from '$lib/components/ui/input/index.js';
let { data }: { data: { answer_form: SuperValidated<Infer<FormSchema>>; question: Question } } =
$props();
let {
data
}: {
data: { answer_form: SuperValidated<Infer<FormSchema>>; question: Question; poolSize: number };
} = $props();
const form = superForm(data.answer_form, {
validators: zodClient(formSchema),
@ -27,25 +31,53 @@
const { form: formData, enhance } = form;
</script>
{#if data.question}
<form method="POST" class="w-2/3 space-y-6" use:enhance action="?/answer">
<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>
<Form.Button>Vasta</Form.Button>
</form>
{/if}
<Card.Root>
<Card.Header>
<Card.Title>Vasta vajajale</Card.Title>
<Card.Description>
{#if !data.question}
{#if data.poolSize === 0}
Rahval said kõik küsimused otsa!
{:else}
Oled kõigile vastanud, kuid enda küsimustel vastused puudu.
{/if}
{:else}
Tänutäheks saad vastu ühe küsimuse küsida.
{/if}
</Card.Description>
</Card.Header>
{#if !data.question}
<Card.Content>
{#if data.poolSize === 0}
<p class="text-sm leading-6">Sellise erandjuhuga saad ühe korra niisama küsida!</p>
{:else}
<p class="text-sm leading-6">Äkki tahaksid su sõbrad vastata või on meil mingi küsimus?</p>
{/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">
import { formSchema, type FormSchema } from './question-schema';
import type { Question } from '$lib/types';
import { toast } from 'svelte-sonner';
import { zodClient } from 'sveltekit-superforms/adapters';
import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms';
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';
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, {
validators: zodClient(formSchema),
@ -26,16 +36,25 @@
const { form: formData, enhance } = form;
</script>
<form method="POST" class="w-2/3 space-y-6" use:enhance action="?/question">
<Form.Field {form} name="question">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Uus küsimus</Form.Label>
<Input {...props} bind:value={$formData.question} />
{/snippet}
</Form.Control>
<Form.Description>Küsi ükskõik mida sellelt kollektiiv intelektilt.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Küsi</Form.Button>
</form>
<Card.Root>
<Card.Header>
<Card.Title>Küsi rahvalt</Card.Title>
<Card.Description>Sul on alles 0 küsimust. Vasta teistele kõigepealt!</Card.Description>
</Card.Header>
<form method="POST" use:enhance action="?/question">
<Card.Content>
<Form.Field {form} name="question">
<Form.Control>
{#snippet children({ props })}
<Form.Label>Küsimus rahvale</Form.Label>
<Input {...props} bind:value={$formData.question} />
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
</Card.Content>
<Card.Footer>
<Form.Button>Küsi</Form.Button>
</Card.Footer>
</form>
</Card.Root>