Rahvatarkus Better UI and errors
Handle more edgecases More specific error messages All error messages in Estonian
This commit is contained in:
parent
35ad2da1c3
commit
f26aa8f5c7
15 changed files with 243 additions and 74 deletions
28
src/lib/components/ui/textarea/index.ts
Normal file
28
src/lib/components/ui/textarea/index.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import Root from "./textarea.svelte";
|
||||||
|
|
||||||
|
type FormTextareaEvent<T extends Event = Event> = T & {
|
||||||
|
currentTarget: EventTarget & HTMLTextAreaElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TextareaEvents = {
|
||||||
|
blur: FormTextareaEvent<FocusEvent>;
|
||||||
|
change: FormTextareaEvent<Event>;
|
||||||
|
click: FormTextareaEvent<MouseEvent>;
|
||||||
|
focus: FormTextareaEvent<FocusEvent>;
|
||||||
|
keydown: FormTextareaEvent<KeyboardEvent>;
|
||||||
|
keypress: FormTextareaEvent<KeyboardEvent>;
|
||||||
|
keyup: FormTextareaEvent<KeyboardEvent>;
|
||||||
|
mouseover: FormTextareaEvent<MouseEvent>;
|
||||||
|
mouseenter: FormTextareaEvent<MouseEvent>;
|
||||||
|
mouseleave: FormTextareaEvent<MouseEvent>;
|
||||||
|
paste: FormTextareaEvent<ClipboardEvent>;
|
||||||
|
input: FormTextareaEvent<InputEvent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Textarea,
|
||||||
|
type TextareaEvents,
|
||||||
|
type FormTextareaEvent,
|
||||||
|
};
|
22
src/lib/components/ui/textarea/textarea.svelte
Normal file
22
src/lib/components/ui/textarea/textarea.svelte
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef, WithoutChildren } from "bits-ui";
|
||||||
|
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
bind:this={ref}
|
||||||
|
bind:value
|
||||||
|
class={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-sm focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
></textarea>
|
73
src/lib/server/rahvatarkus/QuestionBalance.ts
Normal file
73
src/lib/server/rahvatarkus/QuestionBalance.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
import getPoolSize from './getPoolSize';
|
||||||
|
|
||||||
|
// Based on code generated by Claude 3.5 Sonnet
|
||||||
|
class QuestionBalanceStore {
|
||||||
|
private balances: Map<string, number>;
|
||||||
|
private lastAccessed: Map<string, number>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.balances = new Map();
|
||||||
|
this.lastAccessed = new Map();
|
||||||
|
|
||||||
|
// Clean up expired sessions every hour
|
||||||
|
setInterval(() => this.cleanup(), 1000 * 60 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get remaining questions for a session
|
||||||
|
getBalance(sessionToken: string): number {
|
||||||
|
this.lastAccessed.set(sessionToken, Date.now());
|
||||||
|
return this.balances.get(sessionToken) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add questions to a session (e.g., after answering)
|
||||||
|
addQuestions(sessionToken: string, count: number = 1): void {
|
||||||
|
this.lastAccessed.set(sessionToken, Date.now());
|
||||||
|
|
||||||
|
const currentBalance = this.balances.get(sessionToken) || 0;
|
||||||
|
this.balances.set(sessionToken, currentBalance + count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a question from the balance
|
||||||
|
async useQuestion(sessionToken: string): Promise<boolean> {
|
||||||
|
this.lastAccessed.set(sessionToken, Date.now());
|
||||||
|
|
||||||
|
const currentBalance = this.balances.get(sessionToken) || 0;
|
||||||
|
|
||||||
|
// New users can neither ask nor answer in this case
|
||||||
|
// Allow one question for this edge case
|
||||||
|
if (currentBalance === 0) {
|
||||||
|
const poolSize = await getPoolSize();
|
||||||
|
return poolSize === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.balances.set(sessionToken, currentBalance - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up sessions that haven't been accessed in 24 hours
|
||||||
|
private cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const expiryTime = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
|
for (const [token, lastAccess] of this.lastAccessed.entries()) {
|
||||||
|
if (now - lastAccess > expiryTime) {
|
||||||
|
this.balances.delete(token);
|
||||||
|
this.lastAccessed.delete(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For debugging in development
|
||||||
|
debugInfo(): unknown {
|
||||||
|
if (!dev) return null;
|
||||||
|
return {
|
||||||
|
totalSessions: this.balances.size,
|
||||||
|
sessions: Object.fromEntries(this.balances),
|
||||||
|
lastAccessed: Object.fromEntries(this.lastAccessed)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
export const questionBalanceStore = new QuestionBalanceStore();
|
13
src/lib/server/rahvatarkus/getPoolSize.ts
Normal file
13
src/lib/server/rahvatarkus/getPoolSize.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { questions } from '$lib/server/db/schema';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const results = await db
|
||||||
|
.select({
|
||||||
|
poolSize: sql`COUNT(CASE WHEN answer_count < 5 THEN 1 END)`
|
||||||
|
})
|
||||||
|
.from(questions);
|
||||||
|
|
||||||
|
return Number(results[0].poolSize);
|
||||||
|
};
|
|
@ -1,7 +1,10 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
import { eq, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { questions, answers } from '$lib/server/db/schema';
|
import { questions, answers } from '$lib/server/db/schema';
|
||||||
import { eq, sql } from 'drizzle-orm';
|
import { questionBalanceStore } from '$lib/server/rahvatarkus/QuestionBalance';
|
||||||
|
|
||||||
const maxAnswers = 5;
|
const maxAnswers = 5;
|
||||||
|
|
||||||
|
@ -29,17 +32,17 @@ export async function POST({ locals, request }) {
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!question.length) {
|
if (!question.length) {
|
||||||
return json({ error: 'Question not found' }, { status: 400 });
|
return json({ error: 'Seda küsimust ei leitud' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [questionData] = question;
|
const [questionData] = question;
|
||||||
|
|
||||||
// if (questionData.creator === user) {
|
if (questionData.creator === user) {
|
||||||
// return json({ error: 'Cannot answer own question' }, { status: 400 });
|
return json({ error: 'Iseendale vastamine ei ole produktiivne' }, { status: 400 });
|
||||||
// }
|
}
|
||||||
|
|
||||||
if (questionData.answerCount >= maxAnswers) {
|
if (questionData.answerCount >= maxAnswers) {
|
||||||
return json({ error: 'No more answers needed' }, { status: 400 });
|
return json({ error: 'Sellel küsimusel on juba piisavalt küsimusi' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert answer and update count atomically
|
// Insert answer and update count atomically
|
||||||
|
@ -57,6 +60,9 @@ export async function POST({ locals, request }) {
|
||||||
.set({ answerCount: sql`${questions.answerCount} + 1` })
|
.set({ answerCount: sql`${questions.answerCount} + 1` })
|
||||||
.where(eq(questions.id, questionId));
|
.where(eq(questions.id, questionId));
|
||||||
|
|
||||||
|
// Valid answer, allow this user to ask one question
|
||||||
|
questionBalanceStore.addQuestions(user);
|
||||||
|
|
||||||
return json(newAnswer);
|
return json(newAnswer);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,8 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db';
|
import getPoolSize from '$lib/server/rahvatarkus/getPoolSize';
|
||||||
import { questions } from '$lib/server/db/schema';
|
|
||||||
import { sql } from 'drizzle-orm';
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const results = await db
|
const size = await getPoolSize();
|
||||||
.select({
|
|
||||||
poolSize: sql`COUNT(CASE WHEN answer_count < 5 THEN 1 END)`
|
|
||||||
})
|
|
||||||
.from(questions);
|
|
||||||
|
|
||||||
return json({
|
return json({ size });
|
||||||
size: Number(results[0].poolSize)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
import { eq, and, not, sql, exists, lt, gt } from 'drizzle-orm';
|
||||||
|
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { questions, answers } from '$lib/server/db/schema';
|
import { questions, answers } from '$lib/server/db/schema';
|
||||||
import { eq, and, not, sql, exists, lt, gt } from 'drizzle-orm';
|
import { questionBalanceStore } from '$lib/server/rahvatarkus/QuestionBalance';
|
||||||
|
|
||||||
export async function GET({ locals }) {
|
export async function GET({ locals }) {
|
||||||
const { session } = locals;
|
const { session } = locals;
|
||||||
|
@ -19,6 +22,7 @@ export async function GET({ locals }) {
|
||||||
.from(questions)
|
.from(questions)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
|
not(eq(questions.creator, user)),
|
||||||
lt(questions.answerCount, 5),
|
lt(questions.answerCount, 5),
|
||||||
not(
|
not(
|
||||||
exists(
|
exists(
|
||||||
|
@ -34,7 +38,7 @@ export async function GET({ locals }) {
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!eligibleQuestions.length) {
|
if (!eligibleQuestions.length) {
|
||||||
return json({ error: 'No questions available' }, { status: 404 });
|
return json({ error: '' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return json(eligibleQuestions[0]);
|
return json(eligibleQuestions[0]);
|
||||||
|
@ -52,7 +56,7 @@ export async function POST({ locals, request }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content?.trim()) {
|
if (!content?.trim()) {
|
||||||
return json({ error: 'Content is required' }, { status: 400 });
|
return json({ error: 'Küsimustel on sisu vaja' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize content
|
// Normalize content
|
||||||
|
@ -71,7 +75,7 @@ export async function POST({ locals, request }) {
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingQuestion.length > 0) {
|
if (existingQuestion.length > 0) {
|
||||||
throw new Error('Question already exists');
|
throw new Error('Seda on juba küsitud');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check user's recent questions (optional rate limiting)
|
// Check user's recent questions (optional rate limiting)
|
||||||
|
@ -86,7 +90,11 @@ export async function POST({ locals, request }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (recentQuestions[0].count >= 10) {
|
if (recentQuestions[0].count >= 10) {
|
||||||
throw new Error('Too many questions in the last hour');
|
throw new Error('Rahu rahu! Oled tunni aja jooksul liiga palju küsimusi esitanud');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!questionBalanceStore.useQuestion(user)) {
|
||||||
|
throw new Error('Pead vastama teistele enne küsimist');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the new question
|
// Insert the new question
|
||||||
|
@ -103,7 +111,7 @@ export async function POST({ locals, request }) {
|
||||||
|
|
||||||
return json(newQuestion);
|
return json(newQuestion);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e instanceof Error ? e.message : 'Failed to create question';
|
const error = e instanceof Error ? e.message : 'Küsimine põrus';
|
||||||
return json({ error }, { status: 400 });
|
return json({ error }, { status: 400 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { formSchema as answerSchema } from './answer-schema';
|
||||||
import { superValidate } from 'sveltekit-superforms';
|
import { superValidate } from 'sveltekit-superforms';
|
||||||
import { zod } from 'sveltekit-superforms/adapters';
|
import { zod } from 'sveltekit-superforms/adapters';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { questionBalanceStore } from '$lib/server/rahvatarkus/QuestionBalance';
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ fetch, locals }) => {
|
export const load: LayoutServerLoad = async ({ fetch, locals }) => {
|
||||||
const { session } = locals;
|
const { session } = locals;
|
||||||
|
@ -17,6 +18,8 @@ export const load: LayoutServerLoad = async ({ fetch, locals }) => {
|
||||||
|
|
||||||
const user = session.data.userId;
|
const user = session.data.userId;
|
||||||
|
|
||||||
|
const userBalance = questionBalanceStore.getBalance(user);
|
||||||
|
|
||||||
let question: Question | undefined = undefined;
|
let question: Question | undefined = undefined;
|
||||||
|
|
||||||
const poolSize = await fetch('/api/rahvatarkus/pool')
|
const poolSize = await fetch('/api/rahvatarkus/pool')
|
||||||
|
@ -43,7 +46,10 @@ export const load: LayoutServerLoad = async ({ fetch, locals }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: user,
|
user: {
|
||||||
|
id: user,
|
||||||
|
balance: userBalance
|
||||||
|
},
|
||||||
question: question,
|
question: question,
|
||||||
poolSize,
|
poolSize,
|
||||||
question_form: await superValidate(zod(questionSchema)),
|
question_form: await superValidate(zod(questionSchema)),
|
||||||
|
|
|
@ -3,15 +3,18 @@
|
||||||
|
|
||||||
import QuestionForm from './question-form.svelte';
|
import QuestionForm from './question-form.svelte';
|
||||||
import AnswerForm from './answer-form.svelte';
|
import AnswerForm from './answer-form.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let { data, children } = $props();
|
let { data, children } = $props();
|
||||||
|
|
||||||
$inspect(data);
|
let firstTab = $state('answer');
|
||||||
|
|
||||||
let firstTab = $derived(data?.question ? 'answer' : 'question');
|
onMount(() => {
|
||||||
|
firstTab = data?.question ? 'answer' : 'question';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tabs.Root value={firstTab} class="flex w-full max-w-md flex-col items-center gap-2">
|
<Tabs.Root value={firstTab} class="flex w-full max-w-md flex-col items-center gap-1">
|
||||||
<Tabs.List class="grid w-full grid-cols-2">
|
<Tabs.List class="grid w-full grid-cols-2">
|
||||||
<Tabs.Trigger value="answer">Vasta</Tabs.Trigger>
|
<Tabs.Trigger value="answer">Vasta</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="question">Küsi</Tabs.Trigger>
|
<Tabs.Trigger value="question">Küsi</Tabs.Trigger>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { fail } from '@sveltejs/kit';
|
||||||
const pageSize = 5;
|
const pageSize = 5;
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, url }) => {
|
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||||
const page = Number(url.searchParams.get('leht')) || 0;
|
const page = Number(url.searchParams.get('leht')) || 1;
|
||||||
|
|
||||||
const archiveRes = fetch(`/api/rahvatarkus/archive/${pageSize}/${(page - 1) * pageSize}`)
|
const archiveRes = fetch(`/api/rahvatarkus/archive/${pageSize}/${(page - 1) * pageSize}`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
@ -63,12 +63,12 @@ export const actions: Actions = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorMessage = response.data?.error;
|
if (response.data?.error) {
|
||||||
|
|
||||||
if (form.errors.answer) {
|
if (form.errors.answer) {
|
||||||
form.errors.answer.push(errorMessage);
|
form.errors.answer.push(response.data.error);
|
||||||
} else {
|
} else {
|
||||||
form.errors.answer = [errorMessage];
|
form.errors.answer = [response.data.error];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
|
@ -111,13 +111,11 @@ export const actions: Actions = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.data?.error?.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
if (response.data?.error) {
|
||||||
const errorMessage = 'Sellel küsimusel on juba vastus.';
|
|
||||||
|
|
||||||
if (form.errors.question) {
|
if (form.errors.question) {
|
||||||
form.errors.question.push(errorMessage);
|
form.errors.question.push(response.data.error);
|
||||||
} else {
|
} else {
|
||||||
form.errors.question = [errorMessage];
|
form.errors.question = [response.data.error];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
const siblingCount = $derived(isDesktop.current ? 1 : 0);
|
const siblingCount = $derived(isDesktop.current ? 1 : 0);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="mb-12 mt-32 flex flex-col items-center text-center font-title">
|
<header class="mb-12 mt-24 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">
|
<h1 class="mb-1 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||||
Mida rahvas teab?
|
Mida rahvas teab?
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import * as Card from '$lib/components/ui/card/index.js';
|
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';
|
||||||
|
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
data
|
data
|
||||||
|
@ -28,30 +29,25 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { form: formData, enhance } = form;
|
const { form: formData, enhance, constraints } = form;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>Vasta vajajale</Card.Title>
|
|
||||||
<Card.Description>
|
|
||||||
{#if !data.question}
|
{#if !data.question}
|
||||||
{#if data.poolSize === 0}
|
<Card.Title>Kõik vastatud</Card.Title>
|
||||||
Rahval said kõik küsimused otsa!
|
<Card.Description>Rahval said kõik küsimused otsa!</Card.Description>
|
||||||
{:else}
|
{:else}
|
||||||
Oled kõigile vastanud, kuid enda küsimustel vastused puudu.
|
<Card.Title>Vasta vajajale</Card.Title>
|
||||||
|
<Card.Description>Tänutäheks saad vastu ühe küsimuse küsida.</Card.Description>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
|
||||||
Tänutäheks saad vastu ühe küsimuse küsida.
|
|
||||||
{/if}
|
|
||||||
</Card.Description>
|
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
{#if !data.question}
|
{#if !data.question}
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
{#if data.poolSize === 0}
|
{#if data.poolSize === 0}
|
||||||
<p class="text-sm leading-6">Sellise erandjuhuga saad ühe korra niisama küsida!</p>
|
<p class="text-sm leading-6">Sellise erandjuhuga saad ühe korra niisama küsida!</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm leading-6">Äkki tahaksid su sõbrad vastata või on meil mingi küsimus?</p>
|
<p class="text-sm leading-6">Äkki tahaksid su sõbrad vastata või on neil küsimusi?</p>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -61,7 +57,7 @@
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<Form.Label>{data.question.content}?</Form.Label>
|
<Form.Label>{data.question.content}?</Form.Label>
|
||||||
<Input {...props} bind:value={$formData.answer} />
|
<Textarea {...props} bind:value={$formData.answer} class="resize-none" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
|
@ -72,6 +68,9 @@
|
||||||
<Input type="hidden" {...props} bind:value={$formData.questionId} />
|
<Input type="hidden" {...props} bind:value={$formData.questionId} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
<Form.Description class="text-right">
|
||||||
|
{$formData.answer.length}/{$constraints.answer?.maxlength}
|
||||||
|
</Form.Description>
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
|
@ -2,7 +2,10 @@ import { z } from 'zod';
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
questionId: z.string().length(21),
|
questionId: z.string().length(21),
|
||||||
answer: z.string().min(2).max(250)
|
answer: z
|
||||||
|
.string()
|
||||||
|
.min(2, 'Vastus peab olema vähemalt 2 tähemärki.')
|
||||||
|
.max(150, 'Vastus ei või olla pikem kui 150 tähemärki.')
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormSchema = typeof formSchema;
|
export type FormSchema = typeof formSchema;
|
||||||
|
|
|
@ -17,12 +17,15 @@
|
||||||
question_form: SuperValidated<Infer<FormSchema>>;
|
question_form: SuperValidated<Infer<FormSchema>>;
|
||||||
question: Question;
|
question: Question;
|
||||||
poolSize: number;
|
poolSize: number;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
balance: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const form = superForm(data.question_form, {
|
const form = superForm(data.question_form, {
|
||||||
validators: zodClient(formSchema),
|
validators: zodClient(formSchema),
|
||||||
invalidateAll: false,
|
|
||||||
resetForm: true,
|
resetForm: true,
|
||||||
onUpdated: ({ form: f }) => {
|
onUpdated: ({ form: f }) => {
|
||||||
if (f.valid) {
|
if (f.valid) {
|
||||||
|
@ -33,14 +36,19 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { form: formData, enhance } = form;
|
const { form: formData, enhance, constraints } = form;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>Küsi rahvalt</Card.Title>
|
<Card.Title>Küsi rahvalt</Card.Title>
|
||||||
<Card.Description>Sul on alles 0 küsimust. Vasta teistele kõigepealt!</Card.Description>
|
<Card.Description>Iga vastus annab võimaluse küsida ühe küsimuse.</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
{#if data.user.balance === 0 && (!data.question || data.poolSize > 0)}
|
||||||
|
<Card.Content>
|
||||||
|
<p class="text-sm leading-6">Enne küsimist pead kõigepealt vastama teistele!</p>
|
||||||
|
</Card.Content>
|
||||||
|
{:else}
|
||||||
<form method="POST" use:enhance action="?/question">
|
<form method="POST" use:enhance action="?/question">
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Form.Field {form} name="question">
|
<Form.Field {form} name="question">
|
||||||
|
@ -50,6 +58,12 @@
|
||||||
<Input {...props} bind:value={$formData.question} />
|
<Input {...props} bind:value={$formData.question} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
<Form.Description class="flex justify-between">
|
||||||
|
<p>
|
||||||
|
Sul on alles {data.user.balance > 0 ? data.user.balance : data.poolSize > 0 ? 0 : 1} küsimust.
|
||||||
|
</p>
|
||||||
|
<p>{$formData.question.length}/{$constraints.question?.maxlength}</p>
|
||||||
|
</Form.Description>
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
@ -57,4 +71,5 @@
|
||||||
<Form.Button>Küsi</Form.Button>
|
<Form.Button>Küsi</Form.Button>
|
||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</form>
|
</form>
|
||||||
|
{/if}
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
question: z.string().min(2).max(50)
|
question: z
|
||||||
|
.string()
|
||||||
|
.min(2, 'Küsimus peab olema vähemalt 2 tähemärki.')
|
||||||
|
.max(50, 'Küsimus ei või olla pikem kui 50 tähemärki.')
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormSchema = typeof formSchema;
|
export type FormSchema = typeof formSchema;
|
||||||
|
|
Loading…
Add table
Reference in a new issue