diff --git a/hindki/astro.config.mjs b/hindki/astro.config.mjs index 329654b..2688f8c 100644 --- a/hindki/astro.config.mjs +++ b/hindki/astro.config.mjs @@ -5,6 +5,7 @@ import react from "@astrojs/react"; import node from "@astrojs/node"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; +import { eq } from "drizzle-orm"; import { tags } from "./src/lib/db/schema.ts"; import "dotenv/config"; @@ -17,7 +18,8 @@ try { if (connectionString) { const client = postgres(connectionString); const db = drizzle(client, { schema: { tags } }); - tagsList = await db.select().from(tags); + // Only get page-level tags for the sidebar + tagsList = await db.select().from(tags).where(eq(tags.level, 'page')); await client.end(); } else { console.warn( diff --git a/hindki/drizzle/0002_late_leo.sql b/hindki/drizzle/0002_late_leo.sql new file mode 100644 index 0000000..b3d651a --- /dev/null +++ b/hindki/drizzle/0002_late_leo.sql @@ -0,0 +1,49 @@ +-- Step 1: Add columns (level temporarily nullable to allow data migration) +ALTER TABLE "tags" ADD COLUMN "level" text;--> statement-breakpoint +ALTER TABLE "tags" ADD COLUMN "parent_page_id" integer;--> statement-breakpoint + +-- Step 2: Set level for existing page tags +UPDATE "tags" SET "level" = 'page' WHERE "name" IN ('general', 'repetition', 'nationalities', 'vocations', 'hobbies', 'food-and-drink', 'social', 'home', 'body');--> statement-breakpoint + +-- Step 3: Set level and parentPageId for existing subheading tag (none exist yet, will be created manually) +-- UPDATE "tags" SET "level" = 'subheading', "parent_page_id" = (SELECT id FROM "tags" WHERE "name" = 'body') WHERE "name" = 'parts-of-the-body';--> statement-breakpoint + +-- Step 4: Insert grammar tags +INSERT INTO "tags" ("name", "description", "level") VALUES + ('Noun', 'Noun', 'grammar'), + ('Verb', 'Verb', 'grammar'), + ('Adjective', 'Adjective', 'grammar'), + ('Adverb', 'Adverb', 'grammar'), + ('Preposition', 'Preposition', 'grammar'), + ('Conjunction', 'Conjunction', 'grammar'), + ('Interjection', 'Interjection', 'grammar'), + ('Pronoun', 'Pronoun', 'grammar');--> statement-breakpoint + +-- Step 5: Insert gender tags +INSERT INTO "tags" ("name", "description", "level") VALUES + ('Masculine', 'Masculine gender', 'gender'), + ('Feminine', 'Feminine gender', 'gender');--> statement-breakpoint + +-- Step 6: Migrate word.type to word_tags (creating grammar tag associations) +INSERT INTO "word_tags" ("word_id", "tag_id") +SELECT w.id, t.id +FROM "words" w +JOIN "tags" t ON LOWER(w.type) = LOWER(t.name) +WHERE t.level = 'grammar';--> statement-breakpoint + +-- Step 7: Migrate word.gender to word_tags (creating gender tag associations) +INSERT INTO "word_tags" ("word_id", "tag_id") +SELECT w.id, t.id +FROM "words" w +JOIN "tags" t ON (w.gender = 'm' AND t.name = 'Masculine') OR (w.gender = 'f' AND t.name = 'Feminine') +WHERE w.gender IS NOT NULL;--> statement-breakpoint + +-- Step 8: Make level NOT NULL now that all rows have values +ALTER TABLE "tags" ALTER COLUMN "level" SET NOT NULL;--> statement-breakpoint + +-- Step 9: Add foreign key constraint for self-reference +ALTER TABLE "tags" ADD CONSTRAINT "tags_parent_page_id_fkey" FOREIGN KEY ("parent_page_id") REFERENCES "tags"("id");--> statement-breakpoint + +-- Step 10: Drop old columns from words table +ALTER TABLE "words" DROP COLUMN "type";--> statement-breakpoint +ALTER TABLE "words" DROP COLUMN "gender"; \ No newline at end of file diff --git a/hindki/drizzle/meta/0002_snapshot.json b/hindki/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..17a078d --- /dev/null +++ b/hindki/drizzle/meta/0002_snapshot.json @@ -0,0 +1,299 @@ +{ + "id": "ce79efb4-3263-4bd2-bf58-1f5ea26ef701", + "prevId": "318667f9-2cec-43d4-9e1c-9c6ddedb8664", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.examples": { + "name": "examples", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word_id": { + "name": "word_id", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "hindi": { + "name": "hindi", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "english": { + "name": "english", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "examples_word_id_words_id_fk": { + "name": "examples_word_id_words_id_fk", + "tableFrom": "examples", + "tableTo": "words", + "columnsFrom": [ + "word_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.see_also": { + "name": "see_also", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word_id": { + "name": "word_id", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "see_also_word_id_words_id_fk": { + "name": "see_also_word_id_words_id_fk", + "tableFrom": "see_also", + "tableTo": "words", + "columnsFrom": [ + "word_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_page_id": { + "name": "parent_page_id", + "type": "serial", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tags_name_unique": { + "name": "tags_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.word_tags": { + "name": "word_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "word_id": { + "name": "word_id", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "serial", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "word_tags_word_id_words_id_fk": { + "name": "word_tags_word_id_words_id_fk", + "tableFrom": "word_tags", + "tableTo": "words", + "columnsFrom": [ + "word_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "word_tags_tag_id_tags_id_fk": { + "name": "word_tags_tag_id_tags_id_fk", + "tableFrom": "word_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.words": { + "name": "words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "hindi": { + "name": "hindi", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "english": { + "name": "english", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/hindki/drizzle/meta/_journal.json b/hindki/drizzle/meta/_journal.json index 3de8fb2..2eb99be 100644 --- a/hindki/drizzle/meta/_journal.json +++ b/hindki/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1759452792993, "tag": "0001_luxuriant_iceman", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1759469327046, + "tag": "0002_late_leo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/hindki/scripts/restore-page-tags.ts b/hindki/scripts/restore-page-tags.ts new file mode 100644 index 0000000..7a50159 --- /dev/null +++ b/hindki/scripts/restore-page-tags.ts @@ -0,0 +1,82 @@ +import { readFileSync } from 'fs'; +import { parse } from 'yaml'; +import { db } from '../src/lib/db'; +import { words, tags, wordTags } from '../src/lib/db/schema'; +import { eq, and } from 'drizzle-orm'; + +interface YAMLWord { + hindi: string; + english: string; +} + +interface YAMLSection { + slug: string; + about: string; + words: YAMLWord[]; +} + +async function restorePageTags() { + const yamlContent = readFileSync('./src/vocab_list.yaml', 'utf-8'); + const sections: YAMLSection[] = parse(yamlContent); + + // Get all tag IDs + const allTags = await db.select().from(tags); + const tagMap = new Map(allTags.map(t => [t.name.toLowerCase(), t.id])); + + let restored = 0; + let notFound = 0; + + for (const section of sections) { + console.log(`Processing section: ${section.slug} with ${section.words.length} words`); + const pageTagId = tagMap.get(section.slug.toLowerCase()); + + if (!pageTagId) { + console.log(`Page tag not found: ${section.slug}`); + continue; + } + + console.log(`Found page tag ID: ${pageTagId} for ${section.slug}`); + + for (const yamlWord of section.words) { + // Find the word in DB by hindi text + const [dbWord] = await db + .select() + .from(words) + .where(eq(words.hindi, yamlWord.hindi)) + .limit(1); + + if (!dbWord) { + console.log(`Word not found: ${yamlWord.hindi} (${yamlWord.english})`); + notFound++; + continue; + } + + // Check if page tag association already exists + const existing = await db + .select() + .from(wordTags) + .where(and(eq(wordTags.wordId, dbWord.id), eq(wordTags.tagId, pageTagId))) + .limit(1); + + if (existing.length === 0) { + await db.insert(wordTags).values({ + wordId: dbWord.id, + tagId: pageTagId, + }); + restored++; + console.log(`Added ${section.slug} tag to: ${yamlWord.hindi}`); + } + } + } + + console.log(`\nRestoration complete!`); + console.log(`Page tags added: ${restored}`); + console.log(`Words not found: ${notFound}`); +} + +restorePageTags() + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/hindki/scripts/restore-type-gender.ts b/hindki/scripts/restore-type-gender.ts new file mode 100644 index 0000000..facf668 --- /dev/null +++ b/hindki/scripts/restore-type-gender.ts @@ -0,0 +1,99 @@ +import { readFileSync } from 'fs'; +import { parse } from 'yaml'; +import { db } from '../src/lib/db'; +import { words, tags, wordTags } from '../src/lib/db/schema'; +import { eq, and } from 'drizzle-orm'; + +interface YAMLWord { + hindi: string; + english: string; + type?: string; + gender?: 'm' | 'f'; +} + +interface YAMLSection { + slug: string; + about: string; + words: YAMLWord[]; +} + +async function restoreTypeAndGender() { + const yamlContent = readFileSync('./src/vocab_list.yaml', 'utf-8'); + const sections: YAMLSection[] = parse(yamlContent); + + // Get all tag IDs + const allTags = await db.select().from(tags); + const tagMap = new Map(allTags.map(t => [t.name.toLowerCase(), t.id])); + + let restored = 0; + let notFound = 0; + + for (const section of sections) { + for (const yamlWord of section.words) { + if (!yamlWord.type && !yamlWord.gender) continue; + + // Find the word in DB by hindi text + const [dbWord] = await db + .select() + .from(words) + .where(eq(words.hindi, yamlWord.hindi)) + .limit(1); + + if (!dbWord) { + console.log(`Word not found: ${yamlWord.hindi} (${yamlWord.english})`); + notFound++; + continue; + } + + const tagIdsToAdd: number[] = []; + + // Add grammar tag + if (yamlWord.type) { + const grammarTagId = tagMap.get(yamlWord.type.toLowerCase()); + if (grammarTagId) { + tagIdsToAdd.push(grammarTagId); + } else { + console.log(`Grammar tag not found: ${yamlWord.type}`); + } + } + + // Add gender tag + if (yamlWord.gender) { + const genderName = yamlWord.gender === 'm' ? 'masculine' : 'feminine'; + const genderTagId = tagMap.get(genderName); + if (genderTagId) { + tagIdsToAdd.push(genderTagId); + } + } + + // Insert word_tags associations + for (const tagId of tagIdsToAdd) { + // Check if already exists + const existing = await db + .select() + .from(wordTags) + .where(and(eq(wordTags.wordId, dbWord.id), eq(wordTags.tagId, tagId))) + .limit(1); + + if (existing.length === 0) { + await db.insert(wordTags).values({ + wordId: dbWord.id, + tagId: tagId, + }); + restored++; + } + } + } + } + + console.log(`\nRestoration complete!`); + console.log(`Tags added: ${restored}`); + console.log(`Words not found: ${notFound}`); +} + +restoreTypeAndGender() + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/hindki/src/components/FlashCardActivity.tsx b/hindki/src/components/FlashCardActivity.tsx index 56d7234..25f5be5 100644 --- a/hindki/src/components/FlashCardActivity.tsx +++ b/hindki/src/components/FlashCardActivity.tsx @@ -81,36 +81,6 @@ export default function FlashCardActivity() { return (