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)
|
||||||
|
});
|
||||||
|
}
|
|
@ -17,6 +17,17 @@ export const load: LayoutServerLoad = async ({ fetch, locals }) => {
|
||||||
|
|
||||||
const user = session.data.userId;
|
const user = session.data.userId;
|
||||||
|
|
||||||
|
let question: Question | undefined = undefined;
|
||||||
|
|
||||||
|
const poolSize = await fetch('/api/rahvatarkus/pool')
|
||||||
|
.then((res) => {
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
return data.size;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (poolSize !== 0) {
|
||||||
const res = await fetch('/api/rahvatarkus/question')
|
const res = await fetch('/api/rahvatarkus/question')
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
@ -26,15 +37,15 @@ export const load: LayoutServerLoad = async ({ fetch, locals }) => {
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
let question: Question | undefined = undefined;
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
question = res.data;
|
question = res.data;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: user,
|
user: user,
|
||||||
question: question,
|
question: question,
|
||||||
|
poolSize,
|
||||||
question_form: await superValidate(zod(questionSchema)),
|
question_form: await superValidate(zod(questionSchema)),
|
||||||
answer_form: await superValidate({ questionId: question?.id }, zod(answerSchema), {
|
answer_form: await superValidate({ questionId: question?.id }, zod(answerSchema), {
|
||||||
errors: false
|
errors: false
|
||||||
|
|
|
@ -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">
|
||||||
|
<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} />
|
<AnswerForm {data} />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="question" class="w-full">
|
||||||
|
<QuestionForm {data} />
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
|
@ -1,31 +1,43 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
<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}
|
{#await data.streamed.archive}
|
||||||
<p>loading</p>
|
<p>loading</p>
|
||||||
{:then archive}
|
{:then archive}
|
||||||
<Accordion.Root type="multiple" class="w-2/3 space-y-6">
|
<Accordion.Root type="multiple" class="space-y-6">
|
||||||
{#each archive.data as question}
|
{#each archive.data as question}
|
||||||
<Accordion.Item disabled={!(question.answers?.length > 0)} value={question.id}>
|
<Accordion.Item disabled={!(question.answers?.length > 0)} value={question.id}>
|
||||||
<Accordion.Trigger>{question.content}?</Accordion.Trigger>
|
<Accordion.Trigger>{question.content}?</Accordion.Trigger>
|
||||||
<Accordion.Content>
|
<Accordion.Content>
|
||||||
<ol class="ml-6 list-decimal [&>li]:mt-2">
|
|
||||||
{#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}
|
||||||
|
@ -37,12 +49,19 @@
|
||||||
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 })}
|
{#snippet children({ pages, currentPage })}
|
||||||
<Pagination.Content>
|
<Pagination.Content class="flex items-center">
|
||||||
<Pagination.Item>
|
<Pagination.Item>
|
||||||
<Pagination.PrevButton />
|
<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>
|
</Pagination.Item>
|
||||||
|
<div class="flex items-center sm:gap-2.5">
|
||||||
{#each pages as page (page.key)}
|
{#each pages as page (page.key)}
|
||||||
{#if page.type === 'ellipsis'}
|
{#if page.type === 'ellipsis'}
|
||||||
<Pagination.Item>
|
<Pagination.Item>
|
||||||
|
@ -56,10 +75,16 @@
|
||||||
</Pagination.Item>
|
</Pagination.Item>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
<Pagination.Item>
|
<Pagination.Item>
|
||||||
<Pagination.NextButton />
|
<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.Item>
|
||||||
</Pagination.Content>
|
</Pagination.Content>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Pagination.Root>
|
</Pagination.Root>
|
||||||
{/await}
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
|
@ -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,8 +31,32 @@
|
||||||
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>
|
||||||
|
<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.Field {form} name="answer">
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
|
@ -46,6 +74,10 @@
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Footer>
|
||||||
<Form.Button>Vasta</Form.Button>
|
<Form.Button>Vasta</Form.Button>
|
||||||
|
</Card.Footer>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
</Card.Root>
|
||||||
|
|
|
@ -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>
|
||||||
|
<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.Field {form} name="question">
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<Form.Label>Uus küsimus</Form.Label>
|
<Form.Label>Küsimus rahvale</Form.Label>
|
||||||
<Input {...props} bind:value={$formData.question} />
|
<Input {...props} bind:value={$formData.question} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.Description>Küsi ükskõik mida sellelt kollektiiv intelektilt.</Form.Description>
|
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Footer>
|
||||||
<Form.Button>Küsi</Form.Button>
|
<Form.Button>Küsi</Form.Button>
|
||||||
|
</Card.Footer>
|
||||||
</form>
|
</form>
|
||||||
|
</Card.Root>
|
||||||
|
|
Loading…
Add table
Reference in a new issue