Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions frontend/app/api/questions/[category]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import { db } from '@/db';
import { questions } from '@/db/schema';
import { categories, questions, questionTranslations } from '@/db/schema';
import { eq, sql, and, ilike } from 'drizzle-orm';

const DEFAULT_PAGE = 1;
Expand Down Expand Up @@ -29,26 +29,62 @@ export async function GET(

const search = searchParams.get('search')?.trim();

// Find category by slug
const [cat] = await db
.select()
.from(categories)
.where(eq(categories.slug, category.toLowerCase()))
.limit(1);

if (!cat) {
return NextResponse.json({
items: [],
total: 0,
page: 1,
totalPages: 0,
locale,
});
}

// Base conditions: category match + locale match
const baseCondition = and(
eq(questions.categorySlug, category.toLowerCase()),
eq(questions.locale, locale)
eq(questions.categoryId, cat.id),
eq(questionTranslations.locale, locale)
);

const whereCondition = search
? and(baseCondition, ilike(questions.question, `%${search}%`))
? and(baseCondition, ilike(questionTranslations.question, `%${search}%`))
: baseCondition;

// Count total
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(questions)
.innerJoin(
questionTranslations,
eq(questions.id, questionTranslations.questionId)
)
.where(whereCondition);

const total = Number(count);
const totalPages = Math.ceil(total / limit);

// Get items with translations
const items = await db
.select()
.select({
id: questions.id,
categoryId: questions.categoryId,
sortOrder: questions.sortOrder,
difficulty: questions.difficulty,
question: questionTranslations.question,
answerBlocks: questionTranslations.answerBlocks,
locale: questionTranslations.locale,
})
.from(questions)
.innerJoin(
questionTranslations,
eq(questions.id, questionTranslations.questionId)
)
.where(whereCondition)
.orderBy(questions.sortOrder)
.limit(limit)
Expand All @@ -75,4 +111,4 @@ export async function GET(
{ status: 500 }
);
}
}
}
1 change: 1 addition & 0 deletions frontend/app/api/quiz/guest-result/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export async function POST(req: Request) {
pointsAwarded,
});
} catch (error) {
console.error('Failed to save guest quiz result:', error);
return NextResponse.json(
{ success: false, error: "Failed to save result" },
{ status: 500 }
Expand Down
2 changes: 2 additions & 0 deletions frontend/components/quiz/PendingResultHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface Props {
export function PendingResultHandler({ userId }: Props) {
useEffect(() => {
const pending = getPendingQuizResult();
if (!pending) return;

if (pending) {
fetch("/api/quiz/guest-result", {
method: "POST",
Expand Down
49 changes: 41 additions & 8 deletions frontend/db/schema/categories.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
import { pgTable, uuid, varchar, text, unique } from 'drizzle-orm/pg-core';
import {
pgTable,
uuid,
varchar,
text,
integer,
timestamp,
primaryKey,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

export const categories = pgTable(
'categories',
export const categories = pgTable('categories', {
id: uuid('id').defaultRandom().primaryKey(),
slug: varchar('slug', { length: 50 }).notNull().unique(),
displayOrder: integer('display_order').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
});

export const categoryTranslations = pgTable(
'category_translations',
{
id: uuid('id').defaultRandom().primaryKey(),
slug: varchar('slug', { length: 50 }).notNull(),
categoryId: uuid('category_id')
.notNull()
.references(() => categories.id, { onDelete: 'cascade' }),
locale: varchar('locale', { length: 5 }).notNull(),
title: text('title').notNull(),
},
table => [
unique('categories_slug_locale_unique').on(table.slug, table.locale),
]
table => ({
pk: primaryKey({ columns: [table.categoryId, table.locale] }),
})
);

export const categoriesRelations = relations(categories, ({ many }) => ({
translations: many(categoryTranslations),
}));

export const categoryTranslationsRelations = relations(
categoryTranslations,
({ one }) => ({
category: one(categories, {
fields: [categoryTranslations.categoryId],
references: [categories.id],
}),
})
);
60 changes: 46 additions & 14 deletions frontend/db/schema/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,58 @@ import {
uuid,
varchar,
text,
jsonb,
integer,
unique,
timestamp,
primaryKey,
jsonb,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { categories } from './categories';

export const questions = pgTable('questions', {
id: uuid('id').defaultRandom().primaryKey(),
categoryId: uuid('category_id')
.notNull()
.references(() => categories.id, { onDelete: 'restrict' }),
sortOrder: integer('sort_order').notNull().default(0),
difficulty: varchar('difficulty', { length: 20 }).default('medium'),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow(),
});

export const questions = pgTable(
'questions',
export const questionTranslations = pgTable(
'question_translations',
{
id: uuid('id').defaultRandom().primaryKey(),
categorySlug: varchar('category_slug', { length: 50 }).notNull(),
questionId: uuid('question_id')
.notNull()
.references(() => questions.id, { onDelete: 'cascade' }),
locale: varchar('locale', { length: 5 }).notNull(),
question: text('question').notNull(),
answerBlocks: jsonb('answer_blocks').notNull(),
sortOrder: integer('sort_order').default(0),
},
table => [
unique('questions_category_locale_order_unique').on(
table.categorySlug,
table.locale,
table.sortOrder
),
]
table => ({
pk: primaryKey({ columns: [table.questionId, table.locale] }),
})
);
Comment on lines +14 to 42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if drizzle/schema.ts exists and compare structure
if [ -f "frontend/drizzle/schema.ts" ]; then
  echo "=== frontend/drizzle/schema.ts questions table ==="
  rg -n -A 20 'export const questions = pgTable' frontend/drizzle/schema.ts
  echo ""
  echo "=== frontend/db/schema/questions.ts questions table ==="
  rg -n -A 15 'export const questions = pgTable' frontend/db/schema/questions.ts
else
  echo "File frontend/drizzle/schema.ts does not exist"
fi

Repository: DevLoversTeam/devlovers.net

Length of output: 1430


🏁 Script executed:

#!/bin/bash
# Check the questionTranslations table in questions.ts
echo "=== frontend/db/schema/questions.ts - questionTranslations table ==="
rg -n -A 12 'export const questionTranslations = pgTable' frontend/db/schema/questions.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 612


Regenerate frontend/drizzle/schema.ts to match the updated schema structure.

The file frontend/drizzle/schema.ts contains an outdated questions table definition with question and answerBlocks directly on it, whereas the authoritative schema in frontend/db/schema/questions.ts has moved those columns to the questionTranslations table. Additionally, the new schema adds sortOrder, difficulty, createdAt, and updatedAt columns to the questions table, and changes the foreign key constraint from onDelete: 'cascade' to onDelete: 'restrict'. Run drizzle-kit pull to regenerate this introspection file.


export const questionsRelations = relations(questions, ({ one, many }) => ({
category: one(categories, {
fields: [questions.categoryId],
references: [categories.id],
}),
translations: many(questionTranslations),
}));

export const questionTranslationsRelations = relations(
questionTranslations,
({ one }) => ({
question: one(questions, {
fields: [questionTranslations.questionId],
references: [questions.id],
}),
})
);
7 changes: 5 additions & 2 deletions frontend/db/schema/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import {
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { users } from './users';
import { categories } from './categories';

export const quizzes = pgTable(
'quizzes',
{
id: uuid('id').defaultRandom().primaryKey(),
topicId: uuid('topic_id').notNull(),
categoryId: uuid('category_id')
.notNull()
.references(() => categories.id, { onDelete: 'restrict' }),
slug: varchar('slug', { length: 100 }).notNull(),
displayOrder: integer('display_order').notNull().default(0),
questionsCount: integer('questions_count').notNull().default(10),
Expand All @@ -33,7 +36,7 @@ export const quizzes = pgTable(
.defaultNow(),
},
table => ({
topicSlugUnique: unique().on(table.topicId, table.slug),
categorySlugUnique: unique().on(table.categoryId, table.slug),
})
);

Expand Down
38 changes: 27 additions & 11 deletions frontend/db/seed-categories.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
import 'dotenv/config';
import { db } from './index';
import { categories } from './schema';
import { categories, categoryTranslations } from './schema';
import { categoryNames } from '../data/category';
import { slugify } from '../utils/slugify';

const LOCALES = ['en', 'pl', 'uk'] as const;
const LOCALES = ['uk', 'en', 'pl'] as const;

async function seedCategories() {
const rows = categoryNames.flatMap(name => {
const slug = slugify(name);
for (let i = 0; i < categoryNames.length; i++) {
const name = categoryNames[i];
const slug = name.toLowerCase();

return LOCALES.map(locale => ({
slug,
// Insert category
const [category] = await db
.insert(categories)
.values({
slug,
displayOrder: i,
})
.onConflictDoNothing()
.returning();

if (!category) {
console.log(`Category ${slug} already exists, skipping...`);
continue;
}

// Insert translations
const translations = LOCALES.map(locale => ({
categoryId: category.id,
locale,
title: name,
}));
});

await db.insert(categories).values(rows).onConflictDoNothing();
await db.insert(categoryTranslations).values(translations).onConflictDoNothing();
}

console.log('✅ Categories seeded!');
console.log('Categories seeded!');
}

seedCategories().catch(err => {
console.error('❌ Seed failed...', err);
console.error('Seed failed:', err);
process.exit(1);
});
52 changes: 38 additions & 14 deletions frontend/db/seed-questions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dotenv/config';
import { eq } from 'drizzle-orm';
import { db } from './index';
import { questions } from './schema';
import { categories, questions, questionTranslations } from './schema';
import rawData from '../parse/questions.json';

type RawQuestion = {
Expand All @@ -23,22 +24,45 @@ async function seedQuestions() {
return;
}

const rows = data.flatMap(q =>
Object.entries(q.translations).map(([locale, content]) => ({
categorySlug: q.category,
locale,
sortOrder: q.order ?? 0,
question: content.question,
answerBlocks: content.answerBlocks,
}))
);
for (const q of data) {
// Find category by slug
const [category] = await db
.select()
.from(categories)
.where(eq(categories.slug, q.category))
.limit(1);

await db.insert(questions).values(rows).onConflictDoNothing();
if (!category) {
console.log(`Category ${q.category} not found, skipping question...`);
continue;
}

// Insert question
const [question] = await db
.insert(questions)
.values({
categoryId: category.id,
sortOrder: q.order ?? 0,
})
.returning();

// Insert translations
const translations = Object.entries(q.translations).map(
([locale, content]) => ({
questionId: question.id,
locale,
question: content.question,
answerBlocks: content.answerBlocks,
})
);

await db.insert(questionTranslations).values(translations);
}

console.log('✅ Questions seeded!');
console.log('Questions seeded!');
}

seedQuestions().catch(err => {
console.error('❌ Seed failed...', err);
console.error('Seed failed:', err);
process.exit(1);
});
});
Loading