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 (
-
-
-

Tags

- {tags.map(tag => ( - - ))} -
- -
-

Word Types

- {['noun', 'verb', 'adjective', 'adverb', 'pronoun', 'conjunction', 'preposition', 'interjection'].map(type => ( - - ))} -
-
- {currentWord && ( )} diff --git a/hindki/src/components/PageSidebar.astro b/hindki/src/components/PageSidebar.astro index c70d914..28fafe8 100644 --- a/hindki/src/components/PageSidebar.astro +++ b/hindki/src/components/PageSidebar.astro @@ -6,8 +6,9 @@ import MobileTableOfContents from "@astrojs/starlight/components/MobileTableOfCo const isLearningPage = Astro.locals.starlightRoute.id === "learn"; import { storage } from "@/lib/storage"; +import { titlecase, typeToTitle } from "@/lib/utils"; -const categories = await storage.getAllTags(); +const categories = await storage.getPageTags(); const types = await storage.getAllTypes(); const words = await storage.getAllWords(); --- @@ -20,7 +21,7 @@ const words = await storage.getAllWords();
-

Categoriezs

+

Categories

{/* Check boxes for each category */} {categories.map((category) => (
@@ -32,7 +33,7 @@ const words = await storage.getAllWords(); value={category.id} checked /> - +
))}
@@ -49,7 +50,7 @@ const words = await storage.getAllWords(); value={type.toString()} checked /> - +
))}
diff --git a/hindki/src/components/Sidebar.astro b/hindki/src/components/Sidebar.astro index b9683e0..e5addd2 100644 --- a/hindki/src/components/Sidebar.astro +++ b/hindki/src/components/Sidebar.astro @@ -7,7 +7,7 @@ import MobileMenuFooter from "virtual:starlight/components/MobileMenuFooter"; import { storage } from "../lib/storage"; import { titlecase } from "@/lib/utils"; -const tags = await storage.getAllTags(); +const tags = await storage.getPageTags(); const { sidebar: astroSidebar } = Astro.locals.starlightRoute; const tagsFromApi = { diff --git a/hindki/src/components/VocabWord.astro b/hindki/src/components/VocabWord.astro index 8e4bccb..16c426d 100644 --- a/hindki/src/components/VocabWord.astro +++ b/hindki/src/components/VocabWord.astro @@ -7,10 +7,14 @@ import type { Tag, VocabWord } from "@/types/types"; const { word } = Astro.props as { word: VocabWord }; const activeTag = Astro.params.tag; -const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = { - m: ["note", "masculine"], - f: ["tip", "feminine"], +const gender_lookup: Record = { + "Masculine": ["note", "masculine"], + "Feminine": ["tip", "feminine"], }; + +// Extract tags by level +const genderTag = word.tags?.find(t => t.level === 'gender'); +const pageTags = word.tags?.filter(t => t.level === 'page' && t.name !== activeTag && t.name !== 'general') || []; ---
  • @@ -25,29 +29,20 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = { >{word.english} { - word.gender && ( + genderTag && ( ) } { - word.tags && - word.tags - .filter((tag: Tag) => { - return !( - activeTag || - tag.name == activeTag || - tag.name == "general" - ); - }) - .map((tag: Tag) => ( - - - - )) + pageTags.map((tag: Tag) => ( + + + + )) } {word.note &&
    } diff --git a/hindki/src/lib/db/schema.ts b/hindki/src/lib/db/schema.ts index 8c32dea..30d3947 100644 --- a/hindki/src/lib/db/schema.ts +++ b/hindki/src/lib/db/schema.ts @@ -1,12 +1,10 @@ -import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; +import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; export const words = pgTable('words', { id: serial('id').primaryKey(), hindi: text('hindi').notNull(), english: text('english').notNull(), - type: text('type').notNull(), // noun, verb, adjective, etc. - gender: text('gender'), // m, f, or null note: text('note'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -24,6 +22,8 @@ export const tags = pgTable('tags', { id: serial('id').primaryKey(), name: text('name').notNull().unique(), description: text('description'), + level: text('level').notNull(), // 'page', 'subheading', 'grammar', 'gender' + parentPageId: integer('parent_page_id'), // For subheadings, references the parent page tag (FK constraint in migration) }); export const wordTags = pgTable('word_tags', { diff --git a/hindki/src/lib/storage.ts b/hindki/src/lib/storage.ts index 1196f9c..fd27485 100644 --- a/hindki/src/lib/storage.ts +++ b/hindki/src/lib/storage.ts @@ -1,7 +1,7 @@ import { db } from "./db"; import { words, examples, tags, wordTags, seeAlso } from "./db/schema"; import { eq } from "drizzle-orm"; -import type { Word, Tag, NewWord } from "@/types/types"; +import type { Word, Tag, NewWord, TagLevel } from "@/types/types"; class PGStorage { async getAllWords(): Promise { @@ -21,11 +21,15 @@ class PGStorage { id: word.id, hindi: word.hindi, english: word.english, - type: word.type, - gender: word.gender as "m" | "f" | null, note: word.note, examples: word.examples.length > 0 ? word.examples : undefined, - tags: word.tags.map((wt) => wt.tag), + tags: word.tags.map((wt) => ({ + id: wt.tag.id, + name: wt.tag.name, + description: wt.tag.description, + level: wt.tag.level as TagLevel, + parentPageId: wt.tag.parentPageId, + })), seeAlso: word.seeAlso.length > 0 ? word.seeAlso : undefined, })); } @@ -50,11 +54,15 @@ class PGStorage { id: word.id, hindi: word.hindi, english: word.english, - type: word.type, - gender: word.gender as "m" | "f" | null, note: word.note, examples: word.examples.length > 0 ? word.examples : undefined, - tags: word.tags.map((wt) => wt.tag), + tags: word.tags.map((wt) => ({ + id: wt.tag.id, + name: wt.tag.name, + description: wt.tag.description, + level: wt.tag.level as TagLevel, + parentPageId: wt.tag.parentPageId, + })), seeAlso: word.seeAlso.length > 0 ? word.seeAlso : undefined, }; } @@ -87,11 +95,15 @@ class PGStorage { id: wt.word.id, hindi: wt.word.hindi, english: wt.word.english, - type: wt.word.type, - gender: wt.word.gender as "m" | "f" | null, note: wt.word.note, examples: wt.word.examples.length > 0 ? wt.word.examples : undefined, - tags: wt.word.tags.map((t) => t.tag), + tags: wt.word.tags.map((t) => ({ + id: t.tag.id, + name: t.tag.name, + description: t.tag.description, + level: t.tag.level as TagLevel, + parentPageId: t.tag.parentPageId, + })), seeAlso: wt.word.seeAlso.length > 0 ? wt.word.seeAlso.map(sa => ({ ...sa, reference: sa.reference || '', @@ -100,16 +112,58 @@ class PGStorage { } async getAllTags(): Promise { - return await db.query.tags.findMany(); + const dbTags = await db.query.tags.findMany(); + return dbTags.map(tag => ({ + id: tag.id, + name: tag.name, + description: tag.description, + level: tag.level as TagLevel, + parentPageId: tag.parentPageId, + })); + } + + async getPageTags(): Promise { + const dbTags = await db.query.tags.findMany({ + where: eq(tags.level, 'page'), + }); + return dbTags.map(tag => ({ + id: tag.id, + name: tag.name, + description: tag.description, + level: tag.level as TagLevel, + parentPageId: tag.parentPageId, + })); + } + + async getGrammarTags(): Promise { + const dbTags = await db.query.tags.findMany({ + where: eq(tags.level, 'grammar'), + }); + return dbTags.map(tag => ({ + id: tag.id, + name: tag.name, + description: tag.description, + level: tag.level as TagLevel, + parentPageId: tag.parentPageId, + })); + } + + async getGenderTags(): Promise { + const dbTags = await db.query.tags.findMany({ + where: eq(tags.level, 'gender'), + }); + return dbTags.map(tag => ({ + id: tag.id, + name: tag.name, + description: tag.description, + level: tag.level as TagLevel, + parentPageId: tag.parentPageId, + })); } async getAllTypes(): Promise { - const types = new Set(); - const allWords = await this.getAllWords(); - allWords.forEach((word) => { - if (word.type) types.add(word.type); - }); - return Array.from(types).sort(); + const grammarTags = await this.getGrammarTags(); + return grammarTags.map(t => t.name).sort(); } async createWord(newWord: NewWord): Promise { @@ -118,8 +172,6 @@ class PGStorage { .values({ hindi: newWord.hindi, english: newWord.english, - type: newWord.type, - gender: newWord.gender || null, note: newWord.note || null, }) .returning(); @@ -162,16 +214,24 @@ class PGStorage { return createdWord!; } - async createTag(name: string, description?: string): Promise { + async createTag(name: string, level: TagLevel, description?: string, parentPageId?: number): Promise { const [tag] = await db .insert(tags) .values({ name, description: description || null, + level, + parentPageId: parentPageId || undefined, }) .returning(); - return tag; + return { + id: tag.id, + name: tag.name, + description: tag.description, + level: tag.level as TagLevel, + parentPageId: tag.parentPageId, + }; } async deleteWord(id: number): Promise { diff --git a/hindki/src/lib/utils.ts b/hindki/src/lib/utils.ts index 19bc686..22ca9f2 100644 --- a/hindki/src/lib/utils.ts +++ b/hindki/src/lib/utils.ts @@ -4,4 +4,11 @@ export function titlecase(str: string) { if (word == "and" || word == "or" || word == "the" || word == "in" || word == "on" || word == "at" || word == "to" || word == "for" || word == "but" || word == "is" || word == "of") return word; return word.charAt(0).toUpperCase() + word.slice(1); }).join(" "); -} \ No newline at end of file +} + +export function typeToTitle(type: string | null | undefined) { + if (!type) return ""; + const partsOfSpeech = ["noun", "verb", "adjective", "adverb"]; + const maybePlural = partsOfSpeech.includes(type.toLowerCase()) ? "s" : ""; + return titlecase(type!) + maybePlural; +} diff --git a/hindki/src/pages/vocabulary/[tag].astro b/hindki/src/pages/vocabulary/[tag].astro index 3e13bd4..0a7da9e 100644 --- a/hindki/src/pages/vocabulary/[tag].astro +++ b/hindki/src/pages/vocabulary/[tag].astro @@ -5,14 +5,7 @@ import VocabWord from "@/components/VocabWord.astro"; import markdownit from "markdown-it"; import markdownItMark from "markdown-it-mark"; import { storage } from "@/lib/storage"; -import { titlecase } from "@/lib/utils"; - -function typeToTitle(type: string | null | undefined) { - if (!type) return ""; - const partsOfSpeech = ["noun", "verb", "adjective", "adverb"]; - const maybePlural = partsOfSpeech.includes(type.toLowerCase()) ? "s" : ""; - return titlecase(type!) + maybePlural; -} +import { titlecase, typeToTitle } from "@/lib/utils"; const md = markdownit().use(markdownItMark); @@ -33,11 +26,43 @@ if (!currentTag) { const words = await storage.getWordsByTag(tag); -const wordtypes = [...new Set(words.map((word) => word.type).filter(Boolean))]; -const wordsByType = wordtypes.map((type) => ({ - type: typeToTitle(type), - words: words.filter((word) => word.type === type), -})); +// Check if this page has subheading tags +const subheadingTags = allTags.filter(t => t.level === 'subheading' && t.parentPageId === currentTag.id); + +let wordsByType; + +if (subheadingTags.length > 0) { + // Organize by subheadings (e.g., "Parts of the Body") + wordsByType = subheadingTags.map((subheading) => ({ + type: subheading.name, + words: words.filter((word) => word.tags?.some(t => t.id === subheading.id)), + })); +} else { + // Organize by grammar tags (part of speech) + const grammarTypes = [...new Set(words.flatMap((word) => + word.tags?.filter(t => t.level === 'grammar').map(t => t.name) || [] + ))]; + + // Include words with grammar tags + const wordsWithGrammar = grammarTypes.map((grammarTagName) => ({ + type: typeToTitle(grammarTagName), + words: words.filter((word) => word.tags?.some(t => t.level === 'grammar' && t.name === grammarTagName)), + })); + + // Find words WITHOUT any grammar tag + const wordsWithoutGrammar = words.filter((word) => + !word.tags?.some(t => t.level === 'grammar') + ); + + // Combine: grammar-organized words + uncategorized words + wordsByType = wordsWithGrammar; + if (wordsWithoutGrammar.length > 0) { + wordsByType.push({ + type: "Other", + words: wordsWithoutGrammar, + }); + } +} const headings = wordsByType.flatMap(({ type, words }) => { return [ diff --git a/hindki/src/types/types.ts b/hindki/src/types/types.ts index 3a1cd41..37a7582 100644 --- a/hindki/src/types/types.ts +++ b/hindki/src/types/types.ts @@ -6,8 +6,6 @@ export interface Word { id: number; english: string; hindi: string; - type: string; - gender?: "m" | "f" | null; note?: string | null; examples?: Example[]; tags?: Tag[]; @@ -21,10 +19,14 @@ export interface Example { note?: string | null; } +export type TagLevel = 'page' | 'subheading' | 'grammar' | 'gender'; + export interface Tag { id: number; name: string; description?: string | null; + level: TagLevel; + parentPageId?: number | null; } export interface SeeAlso { @@ -38,11 +40,9 @@ export interface SeeAlso { export interface NewWord { hindi: string; english: string; - type: string; - gender?: "m" | "f"; note?: string; examples?: NewExample[]; - tagIds?: number[]; + tagIds?: number[]; // Now includes grammar and gender tags seeAlso?: NewSeeAlso[]; } @@ -55,6 +55,8 @@ export interface NewExample { export interface NewTag { name: string; description?: string; + level: TagLevel; + parentPageId?: number; } export interface NewSeeAlso { @@ -75,6 +77,8 @@ export const tagSchema = z.object({ id: z.number(), name: z.string(), description: z.string().optional().nullable(), + level: z.enum(['page', 'subheading', 'grammar', 'gender']), + parentPageId: z.number().optional().nullable(), }); export const seeAlsoSchema = z.object({ @@ -87,8 +91,6 @@ export const wordSchema = z.object({ id: z.number(), english: z.string(), hindi: z.string(), - type: z.string(), - gender: z.enum(["m", "f"]).optional().nullable(), note: z.string().optional().nullable(), examples: z.array(exampleSchema).optional(), tags: z.array(tagSchema).optional(),