Rahvatarkus better ui
This commit is contained in:
parent
6847b6605c
commit
35ad2da1c3
10 changed files with 312 additions and 116 deletions
18
src/lib/components/ui/tabs/index.ts
Normal file
18
src/lib/components/ui/tabs/index.ts
Normal 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,
|
||||
};
|
21
src/lib/components/ui/tabs/tabs-content.svelte
Normal file
21
src/lib/components/ui/tabs/tabs-content.svelte
Normal 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}
|
||||
/>
|
19
src/lib/components/ui/tabs/tabs-list.svelte
Normal file
19
src/lib/components/ui/tabs/tabs-list.svelte
Normal 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}
|
||||
/>
|
21
src/lib/components/ui/tabs/tabs-trigger.svelte
Normal file
21
src/lib/components/ui/tabs/tabs-trigger.svelte
Normal 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}
|
||||
/>
|
16
src/routes/api/rahvatarkus/pool/+server.ts
Normal file
16
src/routes/api/rahvatarkus/pool/+server.ts
Normal 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)
|
||||
});
|
||||
}
|
|
@ -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
|
||||
})
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue