Optimize rahvatarkus API, DB schema and data loading
This commit is contained in:
parent
74cc8a0458
commit
46287d6984
10 changed files with 227 additions and 147 deletions
|
@ -1,24 +1,44 @@
|
||||||
import { relations } from 'drizzle-orm/relations';
|
import { relations } from 'drizzle-orm/relations';
|
||||||
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export const questions = sqliteTable('questions', {
|
export const questions = sqliteTable('questions', {
|
||||||
id: text('id')
|
id: text('id')
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => nanoid()),
|
.$defaultFn(() => nanoid()),
|
||||||
creator: text('creator').notNull(), // session token
|
creator: text('creator').notNull(),
|
||||||
content: text('content').notNull().unique()
|
content: text('content').notNull().unique(),
|
||||||
|
answerCount: integer('answer_count').notNull().default(0),
|
||||||
|
createdAt: integer('created_at', { mode: 'timestamp' })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date())
|
||||||
});
|
});
|
||||||
|
|
||||||
export const answers = sqliteTable('answers', {
|
export const answers = sqliteTable('answers', {
|
||||||
id: text('id')
|
id: text('id')
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => nanoid()),
|
.$defaultFn(() => nanoid()),
|
||||||
creator: text('creator').notNull(), // session token
|
creator: text('creator').notNull(),
|
||||||
content: text('content').notNull(),
|
content: text('content').notNull(),
|
||||||
questionId: text('question_id').references(() => questions.id)
|
questionId: text('question_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => questions.id),
|
||||||
|
createdAt: integer('created_at', { mode: 'timestamp' })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
export const questionsIndexes = {
|
||||||
|
answerCountIdx: `CREATE INDEX idx_questions_answer_count ON questions (answer_count)`,
|
||||||
|
creatorIdx: `CREATE INDEX idx_questions_creator ON questions (creator)`
|
||||||
|
};
|
||||||
|
|
||||||
|
export const answersIndexes = {
|
||||||
|
questionIdIdx: `CREATE INDEX idx_answers_question_id ON answers (question_id)`,
|
||||||
|
creatorIdx: `CREATE INDEX idx_answers_creator ON answers (creator)`
|
||||||
|
};
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
export const questionsRelations = relations(questions, ({ many }) => ({
|
export const questionsRelations = relations(questions, ({ many }) => ({
|
||||||
answers: many(answers)
|
answers: many(answers)
|
||||||
|
|
|
@ -1,72 +1,62 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
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 } from 'drizzle-orm';
|
import { eq, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
const maxAnswers = 5;
|
const maxAnswers = 5;
|
||||||
|
|
||||||
export async function POST({ locals, request }) {
|
export async function POST({ locals, request }) {
|
||||||
const { userId, questionId, content }: { userId: string; questionId: string; content: string } =
|
const { userId, questionId, content } = await request.json();
|
||||||
await request.json();
|
|
||||||
const { session } = locals;
|
const { session } = locals;
|
||||||
|
|
||||||
if (!session?.data?.userId) {
|
if (!session?.data?.userId) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = session.data.userId;
|
const user = session.data.userId;
|
||||||
|
|
||||||
if (!user || !userId || user !== userId) {
|
if (!user || !userId || user !== userId) {
|
||||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content) {
|
// Start transaction
|
||||||
return json({ error: 'Answer is required' }, { status: 400 });
|
return await db.transaction(async (tx) => {
|
||||||
}
|
// Get question and validate in one query
|
||||||
|
const question = await tx
|
||||||
|
.select({
|
||||||
|
creator: questions.creator,
|
||||||
|
answerCount: questions.answerCount
|
||||||
|
})
|
||||||
|
.from(questions)
|
||||||
|
.where(eq(questions.id, questionId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!questionId) {
|
if (!question.length) {
|
||||||
return json({ error: 'No question specified' }, { status: 400 });
|
return json({ error: 'Question not found' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const question = await db
|
const [questionData] = question;
|
||||||
.select({
|
|
||||||
question: questions
|
|
||||||
})
|
|
||||||
.from(questions)
|
|
||||||
.where(eq(questions.id, questionId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!question) {
|
// if (questionData.creator === user) {
|
||||||
return json({ error: 'Question not found' }, { status: 400 });
|
// return json({ error: 'Cannot answer own question' }, { status: 400 });
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (question[0].question.creator === user) {
|
if (questionData.answerCount >= maxAnswers) {
|
||||||
return json({ error: 'Not allowed to answer your own question' }, { status: 400 });
|
return json({ error: 'No more answers needed' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentAnswers = await db
|
// Insert answer and update count atomically
|
||||||
.select({
|
const [newAnswer] = await tx
|
||||||
answer: answers
|
|
||||||
})
|
|
||||||
.from(answers)
|
|
||||||
.where(eq(answers.questionId, questionId));
|
|
||||||
|
|
||||||
if (currentAnswers.length >= maxAnswers) {
|
|
||||||
return json({ error: 'No more answers needed for this question' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [newAnswer] = await db
|
|
||||||
.insert(answers)
|
.insert(answers)
|
||||||
.values({
|
.values({
|
||||||
content: content,
|
content,
|
||||||
creator: userId,
|
creator: userId,
|
||||||
questionId: questionId
|
questionId
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(questions)
|
||||||
|
.set({ answerCount: sql`${questions.answerCount} + 1` })
|
||||||
|
.where(eq(questions.id, questionId));
|
||||||
|
|
||||||
return json(newAnswer);
|
return json(newAnswer);
|
||||||
} catch (e) {
|
});
|
||||||
return json({ error: e }, { status: 400 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,48 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
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 { eq, gt, sql } from 'drizzle-orm';
|
||||||
import type { Question } from '$lib/types';
|
import type { Question } from '$lib/types';
|
||||||
|
|
||||||
export async function GET({ params }) {
|
export async function GET({ params }) {
|
||||||
const limit = Math.min(parseInt(params.limit) || 10, 10);
|
const limit = Math.min(parseInt(params.limit) || 10, 10);
|
||||||
const offset = parseInt(params.offset) || 0;
|
const offset = parseInt(params.offset) || 0;
|
||||||
|
|
||||||
// Get total count
|
// Get total in parallel with data
|
||||||
const [{ total }] = await db
|
const totalPromise = db
|
||||||
.select({
|
.select({
|
||||||
total: sql`count(DISTINCT ${questions.id})`
|
count: sql`count(*)`
|
||||||
})
|
})
|
||||||
.from(questions)
|
.from(questions)
|
||||||
.innerJoin(answers, eq(questions.id, answers.questionId));
|
.where(gt(questions.answerCount, 0));
|
||||||
|
|
||||||
const results = await db
|
const questionsPromise = db
|
||||||
.select({
|
.select({
|
||||||
question: questions,
|
id: questions.id,
|
||||||
answers: answers
|
content: questions.content,
|
||||||
|
creator: questions.creator,
|
||||||
|
createdAt: questions.createdAt,
|
||||||
|
answers: sql`json_group_array(json_object(
|
||||||
|
'id', ${answers.id},
|
||||||
|
'content', ${answers.content},
|
||||||
|
'creator', ${answers.creator},
|
||||||
|
'createdAt', ${answers.createdAt}
|
||||||
|
))`
|
||||||
})
|
})
|
||||||
.from(questions)
|
.from(questions)
|
||||||
.innerJoin(answers, eq(questions.id, answers.questionId))
|
.innerJoin(answers, eq(questions.id, answers.questionId))
|
||||||
|
.where(gt(questions.answerCount, 0))
|
||||||
|
.groupBy(questions.id)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|
||||||
// Group answers by question
|
const [total, curr_questions] = await Promise.all([totalPromise, questionsPromise]);
|
||||||
type QuestionMap = {
|
|
||||||
[key: string]: Question;
|
|
||||||
};
|
|
||||||
|
|
||||||
const questionsWithAnswers = results.reduce<QuestionMap>((acc, row) => {
|
return json({
|
||||||
const questionId = row.question.id;
|
data: curr_questions.map((q) => ({
|
||||||
|
...q,
|
||||||
if (!acc[questionId]) {
|
answers: JSON.parse(q.answers)
|
||||||
acc[questionId] = {
|
})),
|
||||||
...row.question,
|
meta: { limit, offset, total: total[0].count }
|
||||||
answers: []
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.answers) {
|
|
||||||
acc[questionId].answers.push(row.answers);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return json({ data: Object.values(questionsWithAnswers), meta: { limit, offset, total } });
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,67 +1,109 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
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, count, and, not, sql } from 'drizzle-orm';
|
import { eq, and, not, sql, exists, lt, gt } from 'drizzle-orm';
|
||||||
|
|
||||||
export async function GET({ locals }) {
|
export async function GET({ locals }) {
|
||||||
const { session } = locals;
|
const { session } = locals;
|
||||||
|
if (!session?.data?.userId) return;
|
||||||
if (!session?.data?.userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = session.data.userId;
|
const user = session.data.userId;
|
||||||
|
|
||||||
const questionWithAnswerCount = await db
|
// Use the answerCount field and avoid joins
|
||||||
|
const eligibleQuestions = await db
|
||||||
.select({
|
.select({
|
||||||
id: questions.id,
|
id: questions.id,
|
||||||
content: questions.content,
|
content: questions.content,
|
||||||
answerCount: count(answers.id)
|
answerCount: questions.answerCount
|
||||||
})
|
})
|
||||||
.from(questions)
|
.from(questions)
|
||||||
.leftJoin(answers, eq(questions.id, answers.questionId))
|
.where(
|
||||||
.groupBy(questions.id)
|
and(
|
||||||
.having(and(sql`${count(answers.id)} < 5`, not(eq(questions.creator, user))))
|
lt(questions.answerCount, 5),
|
||||||
|
not(
|
||||||
|
exists(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(answers)
|
||||||
|
.where(and(eq(answers.questionId, questions.id), eq(answers.creator, user)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.orderBy(sql`RANDOM()`)
|
.orderBy(sql`RANDOM()`)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!questionWithAnswerCount.length) {
|
if (!eligibleQuestions.length) {
|
||||||
return json({ error: 'No questions available' }, { status: 404 });
|
return json({ error: 'No questions available' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return json(questionWithAnswerCount[0]);
|
return json(eligibleQuestions[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST({ locals, request }) {
|
export async function POST({ locals, request }) {
|
||||||
const { userId, content }: { userId: string; content: string } = await request.json();
|
const { userId, content }: { userId: string; content: string } = await request.json();
|
||||||
const { session } = locals;
|
const { session } = locals;
|
||||||
|
|
||||||
if (!session?.data?.userId) {
|
if (!session?.data?.userId) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = session.data.userId;
|
const user = session.data.userId;
|
||||||
|
|
||||||
if (!user || !userId || user !== userId) {
|
if (!user || !userId || user !== userId) {
|
||||||
console.log(user, userId);
|
|
||||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content) {
|
if (!content?.trim()) {
|
||||||
return json({ error: 'Content is required' }, { status: 400 });
|
return json({ error: 'Content is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize content
|
||||||
|
const normalizedContent = content.trim();
|
||||||
|
const finalContent =
|
||||||
|
normalizedContent.at(-1) === '?' ? normalizedContent.slice(0, -1) : normalizedContent;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [newQuestion] = await db
|
// Use transaction to ensure data consistency
|
||||||
.insert(questions)
|
const [newQuestion] = await db.transaction(async (tx) => {
|
||||||
.values({
|
// Check for duplicate questions first
|
||||||
content: content.at(-1) === '?' ? content.slice(0, -1) : content,
|
const existingQuestion = await tx
|
||||||
creator: userId
|
.select({ id: questions.id })
|
||||||
})
|
.from(questions)
|
||||||
.returning();
|
.where(eq(questions.content, finalContent))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingQuestion.length > 0) {
|
||||||
|
throw new Error('Question already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user's recent questions (optional rate limiting)
|
||||||
|
const recentQuestions = await tx
|
||||||
|
.select({ count: sql`count(*)` })
|
||||||
|
.from(questions)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(questions.creator, userId),
|
||||||
|
gt(questions.createdAt, sql`datetime('now', '-1 hour')`)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recentQuestions[0].count >= 10) {
|
||||||
|
throw new Error('Too many questions in the last hour');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the new question
|
||||||
|
return await tx
|
||||||
|
.insert(questions)
|
||||||
|
.values({
|
||||||
|
content: finalContent,
|
||||||
|
creator: userId,
|
||||||
|
answerCount: 0,
|
||||||
|
createdAt: new Date()
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
});
|
||||||
|
|
||||||
return json(newQuestion);
|
return json(newQuestion);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return json({ error: e }, { status: 400 });
|
const error = e instanceof Error ? e.message : 'Failed to create question';
|
||||||
|
return json({ error }, { status: 400 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
43
src/routes/vinge/rahvatarkus/+layout.server.ts
Normal file
43
src/routes/vinge/rahvatarkus/+layout.server.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
import type { Question } from '$lib/types';
|
||||||
|
import { formSchema as questionSchema } from './question-schema';
|
||||||
|
import { formSchema as answerSchema } from './answer-schema';
|
||||||
|
|
||||||
|
import { superValidate } from 'sveltekit-superforms';
|
||||||
|
import { zod } from 'sveltekit-superforms/adapters';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async ({ fetch, locals }) => {
|
||||||
|
const { session } = locals;
|
||||||
|
|
||||||
|
if (!session?.data?.userId) {
|
||||||
|
await session.setData({ userId: nanoid() });
|
||||||
|
await session.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
question = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: user,
|
||||||
|
question: question,
|
||||||
|
question_form: await superValidate(zod(questionSchema)),
|
||||||
|
answer_form: await superValidate({ questionId: question?.id }, zod(answerSchema), {
|
||||||
|
errors: false
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
13
src/routes/vinge/rahvatarkus/+layout.svelte
Normal file
13
src/routes/vinge/rahvatarkus/+layout.svelte
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import QuestionForm from './question-form.svelte';
|
||||||
|
import AnswerForm from './answer-form.svelte';
|
||||||
|
|
||||||
|
let { data, children } = $props();
|
||||||
|
|
||||||
|
$inspect(data);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<QuestionForm {data} />
|
||||||
|
<AnswerForm {data} />
|
||||||
|
|
||||||
|
{@render children()}
|
|
@ -1,41 +1,17 @@
|
||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
import type { Question } from '$lib/types';
|
|
||||||
import { formSchema as questionSchema } from './question-schema';
|
import { formSchema as questionSchema } from './question-schema';
|
||||||
import { formSchema as answerSchema } from './answer-schema';
|
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 { fail } from '@sveltejs/kit';
|
import { fail } from '@sveltejs/kit';
|
||||||
|
|
||||||
const perPage = 5;
|
const pageSize = 5;
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||||
const { session } = locals;
|
const page = Number(url.searchParams.get('leht')) || 0;
|
||||||
|
|
||||||
if (!session?.data?.userId) {
|
const archiveRes = fetch(`/api/rahvatarkus/archive/${pageSize}/${(page - 1) * pageSize}`)
|
||||||
await session.setData({ userId: nanoid() });
|
|
||||||
await session.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
question = res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const archiveRes = fetch(`/api/rahvatarkus/archive/${perPage}`)
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
|
@ -44,13 +20,8 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: user,
|
page,
|
||||||
question: question,
|
pageSize,
|
||||||
question_form: await superValidate(zod(questionSchema)),
|
|
||||||
answer_form: await superValidate({ questionId: question?.id }, zod(answerSchema), {
|
|
||||||
errors: false
|
|
||||||
}),
|
|
||||||
perPage,
|
|
||||||
streamed: {
|
streamed: {
|
||||||
archive: archiveRes
|
archive: archiveRes
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,13 @@
|
||||||
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 QuestionForm from './question-form.svelte';
|
import { goto } from '$app/navigation';
|
||||||
import AnswerForm from './answer-form.svelte';
|
|
||||||
import SuperDebug from 'sveltekit-superforms';
|
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
$inspect(data);
|
$inspect(data);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<QuestionForm {data} />
|
|
||||||
<AnswerForm {data} />
|
|
||||||
|
|
||||||
{#await data.streamed.archive}
|
{#await data.streamed.archive}
|
||||||
<p>loading</p>
|
<p>loading</p>
|
||||||
{:then archive}
|
{:then archive}
|
||||||
|
@ -35,7 +30,14 @@
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
{/each}
|
{/each}
|
||||||
</Accordion.Root>
|
</Accordion.Root>
|
||||||
<Pagination.Root count={archive.meta.total} perPage={data.perPage}>
|
<Pagination.Root
|
||||||
|
onPageChange={(value: number) => {
|
||||||
|
goto(`?leht=${value}`);
|
||||||
|
}}
|
||||||
|
count={archive.meta.total}
|
||||||
|
perPage={data.pageSize}
|
||||||
|
page={data.page}
|
||||||
|
>
|
||||||
{#snippet children({ pages, currentPage })}
|
{#snippet children({ pages, currentPage })}
|
||||||
<Pagination.Content>
|
<Pagination.Content>
|
||||||
<Pagination.Item>
|
<Pagination.Item>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
const form = superForm(data.answer_form, {
|
const form = superForm(data.answer_form, {
|
||||||
validators: zodClient(formSchema),
|
validators: zodClient(formSchema),
|
||||||
|
invalidateAll: 'force',
|
||||||
onUpdated: ({ form: f }) => {
|
onUpdated: ({ form: f }) => {
|
||||||
if (f.valid) {
|
if (f.valid) {
|
||||||
toast.success(`You submitted ${JSON.stringify(f.data, null, 2)}`);
|
toast.success(`You submitted ${JSON.stringify(f.data, null, 2)}`);
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
|
|
||||||
const form = superForm(data.question_form, {
|
const form = superForm(data.question_form, {
|
||||||
validators: zodClient(formSchema),
|
validators: zodClient(formSchema),
|
||||||
|
invalidateAll: false,
|
||||||
|
resetForm: true,
|
||||||
onUpdated: ({ form: f }) => {
|
onUpdated: ({ form: f }) => {
|
||||||
if (f.valid) {
|
if (f.valid) {
|
||||||
toast.success(`You submitted ${JSON.stringify(f.data, null, 2)}`);
|
toast.success(`You submitted ${JSON.stringify(f.data, null, 2)}`);
|
||||||
|
|
Loading…
Add table
Reference in a new issue