MAJOR BREAKING CHANGE: gender and subheading -> tags
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
This commit is contained in:
parent
58e67fede3
commit
b350b58dfe
@ -5,6 +5,7 @@ import react from "@astrojs/react";
|
|||||||
import node from "@astrojs/node";
|
import node from "@astrojs/node";
|
||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { tags } from "./src/lib/db/schema.ts";
|
import { tags } from "./src/lib/db/schema.ts";
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
|
||||||
@ -17,7 +18,8 @@ try {
|
|||||||
if (connectionString) {
|
if (connectionString) {
|
||||||
const client = postgres(connectionString);
|
const client = postgres(connectionString);
|
||||||
const db = drizzle(client, { schema: { tags } });
|
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();
|
await client.end();
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|||||||
49
hindki/drizzle/0002_late_leo.sql
Normal file
49
hindki/drizzle/0002_late_leo.sql
Normal file
@ -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";
|
||||||
299
hindki/drizzle/meta/0002_snapshot.json
Normal file
299
hindki/drizzle/meta/0002_snapshot.json
Normal file
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,13 @@
|
|||||||
"when": 1759452792993,
|
"when": 1759452792993,
|
||||||
"tag": "0001_luxuriant_iceman",
|
"tag": "0001_luxuriant_iceman",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1759469327046,
|
||||||
|
"tag": "0002_late_leo",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
82
hindki/scripts/restore-page-tags.ts
Normal file
82
hindki/scripts/restore-page-tags.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
99
hindki/scripts/restore-type-gender.ts
Normal file
99
hindki/scripts/restore-type-gender.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
@ -81,36 +81,6 @@ export default function FlashCardActivity() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flash-card-activity">
|
<div className="flash-card-activity">
|
||||||
<div className="filters">
|
|
||||||
<div className="filter-group">
|
|
||||||
<h3>Tags</h3>
|
|
||||||
{tags.map(tag => (
|
|
||||||
<label key={tag.id} className="filter-checkbox">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedTagIds.includes(tag.id)}
|
|
||||||
onChange={() => toggleTag(tag.id)}
|
|
||||||
/>
|
|
||||||
{tag.name}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="filter-group">
|
|
||||||
<h3>Word Types</h3>
|
|
||||||
{['noun', 'verb', 'adjective', 'adverb', 'pronoun', 'conjunction', 'preposition', 'interjection'].map(type => (
|
|
||||||
<label key={type} className="filter-checkbox">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedTypes.includes(type)}
|
|
||||||
onChange={() => toggleType(type)}
|
|
||||||
/>
|
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentWord && (
|
{currentWord && (
|
||||||
<FlashCard word={currentWord} />
|
<FlashCard word={currentWord} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -6,8 +6,9 @@ import MobileTableOfContents from "@astrojs/starlight/components/MobileTableOfCo
|
|||||||
const isLearningPage = Astro.locals.starlightRoute.id === "learn";
|
const isLearningPage = Astro.locals.starlightRoute.id === "learn";
|
||||||
|
|
||||||
import { storage } from "@/lib/storage";
|
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 types = await storage.getAllTypes();
|
||||||
const words = await storage.getAllWords();
|
const words = await storage.getAllWords();
|
||||||
---
|
---
|
||||||
@ -20,7 +21,7 @@ const words = await storage.getAllWords();
|
|||||||
<div class="sl-container">
|
<div class="sl-container">
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<div class="category-filters">
|
<div class="category-filters">
|
||||||
<h2>Categoriezs</h2>
|
<h2>Categories</h2>
|
||||||
{/* Check boxes for each category */}
|
{/* Check boxes for each category */}
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
@ -32,7 +33,7 @@ const words = await storage.getAllWords();
|
|||||||
value={category.id}
|
value={category.id}
|
||||||
checked
|
checked
|
||||||
/>
|
/>
|
||||||
<label for={category.name}>{category.name}</label>
|
<label for={category.name}>{titlecase(category.name)}</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -49,7 +50,7 @@ const words = await storage.getAllWords();
|
|||||||
value={type.toString()}
|
value={type.toString()}
|
||||||
checked
|
checked
|
||||||
/>
|
/>
|
||||||
<label for={type}>{type}</label>
|
<label for={type}>{typeToTitle(type)}</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import MobileMenuFooter from "virtual:starlight/components/MobileMenuFooter";
|
|||||||
import { storage } from "../lib/storage";
|
import { storage } from "../lib/storage";
|
||||||
import { titlecase } from "@/lib/utils";
|
import { titlecase } from "@/lib/utils";
|
||||||
|
|
||||||
const tags = await storage.getAllTags();
|
const tags = await storage.getPageTags();
|
||||||
|
|
||||||
const { sidebar: astroSidebar } = Astro.locals.starlightRoute;
|
const { sidebar: astroSidebar } = Astro.locals.starlightRoute;
|
||||||
const tagsFromApi = {
|
const tagsFromApi = {
|
||||||
|
|||||||
@ -7,10 +7,14 @@ import type { Tag, VocabWord } from "@/types/types";
|
|||||||
const { word } = Astro.props as { word: VocabWord };
|
const { word } = Astro.props as { word: VocabWord };
|
||||||
const activeTag = Astro.params.tag;
|
const activeTag = Astro.params.tag;
|
||||||
|
|
||||||
const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
|
const gender_lookup: Record<string, ["note" | "tip", string]> = {
|
||||||
m: ["note", "masculine"],
|
"Masculine": ["note", "masculine"],
|
||||||
f: ["tip", "feminine"],
|
"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') || [];
|
||||||
---
|
---
|
||||||
|
|
||||||
<li id={word.hindi} class="word-entry">
|
<li id={word.hindi} class="word-entry">
|
||||||
@ -25,29 +29,20 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
|
|||||||
>{word.english}</span
|
>{word.english}</span
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
word.gender && (
|
genderTag && (
|
||||||
<Badge
|
<Badge
|
||||||
variant={gender_lookup[word.gender][0]}
|
variant={gender_lookup[genderTag.name][0]}
|
||||||
text={gender_lookup[word.gender][1]}
|
text={gender_lookup[genderTag.name][1]}
|
||||||
class="gender-badge"
|
class="gender-badge"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
word.tags &&
|
pageTags.map((tag: Tag) => (
|
||||||
word.tags
|
<a class="badge-link" href={`/vocabulary/${tag.name}`}>
|
||||||
.filter((tag: Tag) => {
|
<Badge text={tag.name} class="tag-badge" />
|
||||||
return !(
|
</a>
|
||||||
activeTag ||
|
))
|
||||||
tag.name == activeTag ||
|
|
||||||
tag.name == "general"
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((tag: Tag) => (
|
|
||||||
<a class="badge-link" href="/vocabulary/{tag.name}">
|
|
||||||
<Badge text={tag.name} class="tag-badge" />
|
|
||||||
</a>
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
</AnchorHeading>
|
</AnchorHeading>
|
||||||
{word.note && <div set:html={render(word.note)} />}
|
{word.note && <div set:html={render(word.note)} />}
|
||||||
|
|||||||
@ -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';
|
import { relations } from 'drizzle-orm';
|
||||||
|
|
||||||
export const words = pgTable('words', {
|
export const words = pgTable('words', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
hindi: text('hindi').notNull(),
|
hindi: text('hindi').notNull(),
|
||||||
english: text('english').notNull(),
|
english: text('english').notNull(),
|
||||||
type: text('type').notNull(), // noun, verb, adjective, etc.
|
|
||||||
gender: text('gender'), // m, f, or null
|
|
||||||
note: text('note'),
|
note: text('note'),
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||||
@ -24,6 +22,8 @@ export const tags = pgTable('tags', {
|
|||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
name: text('name').notNull().unique(),
|
name: text('name').notNull().unique(),
|
||||||
description: text('description'),
|
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', {
|
export const wordTags = pgTable('word_tags', {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { words, examples, tags, wordTags, seeAlso } from "./db/schema";
|
import { words, examples, tags, wordTags, seeAlso } from "./db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { Word, Tag, NewWord } from "@/types/types";
|
import type { Word, Tag, NewWord, TagLevel } from "@/types/types";
|
||||||
|
|
||||||
class PGStorage {
|
class PGStorage {
|
||||||
async getAllWords(): Promise<Word[]> {
|
async getAllWords(): Promise<Word[]> {
|
||||||
@ -21,11 +21,15 @@ class PGStorage {
|
|||||||
id: word.id,
|
id: word.id,
|
||||||
hindi: word.hindi,
|
hindi: word.hindi,
|
||||||
english: word.english,
|
english: word.english,
|
||||||
type: word.type,
|
|
||||||
gender: word.gender as "m" | "f" | null,
|
|
||||||
note: word.note,
|
note: word.note,
|
||||||
examples: word.examples.length > 0 ? word.examples : undefined,
|
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,
|
seeAlso: word.seeAlso.length > 0 ? word.seeAlso : undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -50,11 +54,15 @@ class PGStorage {
|
|||||||
id: word.id,
|
id: word.id,
|
||||||
hindi: word.hindi,
|
hindi: word.hindi,
|
||||||
english: word.english,
|
english: word.english,
|
||||||
type: word.type,
|
|
||||||
gender: word.gender as "m" | "f" | null,
|
|
||||||
note: word.note,
|
note: word.note,
|
||||||
examples: word.examples.length > 0 ? word.examples : undefined,
|
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,
|
seeAlso: word.seeAlso.length > 0 ? word.seeAlso : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -87,11 +95,15 @@ class PGStorage {
|
|||||||
id: wt.word.id,
|
id: wt.word.id,
|
||||||
hindi: wt.word.hindi,
|
hindi: wt.word.hindi,
|
||||||
english: wt.word.english,
|
english: wt.word.english,
|
||||||
type: wt.word.type,
|
|
||||||
gender: wt.word.gender as "m" | "f" | null,
|
|
||||||
note: wt.word.note,
|
note: wt.word.note,
|
||||||
examples: wt.word.examples.length > 0 ? wt.word.examples : undefined,
|
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 => ({
|
seeAlso: wt.word.seeAlso.length > 0 ? wt.word.seeAlso.map(sa => ({
|
||||||
...sa,
|
...sa,
|
||||||
reference: sa.reference || '',
|
reference: sa.reference || '',
|
||||||
@ -100,16 +112,58 @@ class PGStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllTags(): Promise<Tag[]> {
|
async getAllTags(): Promise<Tag[]> {
|
||||||
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<Tag[]> {
|
||||||
|
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<Tag[]> {
|
||||||
|
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<Tag[]> {
|
||||||
|
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<string[]> {
|
async getAllTypes(): Promise<string[]> {
|
||||||
const types = new Set<string>();
|
const grammarTags = await this.getGrammarTags();
|
||||||
const allWords = await this.getAllWords();
|
return grammarTags.map(t => t.name).sort();
|
||||||
allWords.forEach((word) => {
|
|
||||||
if (word.type) types.add(word.type);
|
|
||||||
});
|
|
||||||
return Array.from(types).sort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createWord(newWord: NewWord): Promise<Word> {
|
async createWord(newWord: NewWord): Promise<Word> {
|
||||||
@ -118,8 +172,6 @@ class PGStorage {
|
|||||||
.values({
|
.values({
|
||||||
hindi: newWord.hindi,
|
hindi: newWord.hindi,
|
||||||
english: newWord.english,
|
english: newWord.english,
|
||||||
type: newWord.type,
|
|
||||||
gender: newWord.gender || null,
|
|
||||||
note: newWord.note || null,
|
note: newWord.note || null,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
@ -162,16 +214,24 @@ class PGStorage {
|
|||||||
return createdWord!;
|
return createdWord!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTag(name: string, description?: string): Promise<Tag> {
|
async createTag(name: string, level: TagLevel, description?: string, parentPageId?: number): Promise<Tag> {
|
||||||
const [tag] = await db
|
const [tag] = await db
|
||||||
.insert(tags)
|
.insert(tags)
|
||||||
.values({
|
.values({
|
||||||
name,
|
name,
|
||||||
description: description || null,
|
description: description || null,
|
||||||
|
level,
|
||||||
|
parentPageId: parentPageId || undefined,
|
||||||
})
|
})
|
||||||
.returning();
|
.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<void> {
|
async deleteWord(id: number): Promise<void> {
|
||||||
|
|||||||
@ -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;
|
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);
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||||
}).join(" ");
|
}).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -5,14 +5,7 @@ import VocabWord from "@/components/VocabWord.astro";
|
|||||||
import markdownit from "markdown-it";
|
import markdownit from "markdown-it";
|
||||||
import markdownItMark from "markdown-it-mark";
|
import markdownItMark from "markdown-it-mark";
|
||||||
import { storage } from "@/lib/storage";
|
import { storage } from "@/lib/storage";
|
||||||
import { titlecase } from "@/lib/utils";
|
import { titlecase, typeToTitle } 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const md = markdownit().use(markdownItMark);
|
const md = markdownit().use(markdownItMark);
|
||||||
|
|
||||||
@ -33,11 +26,43 @@ if (!currentTag) {
|
|||||||
|
|
||||||
const words = await storage.getWordsByTag(tag);
|
const words = await storage.getWordsByTag(tag);
|
||||||
|
|
||||||
const wordtypes = [...new Set(words.map((word) => word.type).filter(Boolean))];
|
// Check if this page has subheading tags
|
||||||
const wordsByType = wordtypes.map((type) => ({
|
const subheadingTags = allTags.filter(t => t.level === 'subheading' && t.parentPageId === currentTag.id);
|
||||||
type: typeToTitle(type),
|
|
||||||
words: words.filter((word) => word.type === type),
|
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 }) => {
|
const headings = wordsByType.flatMap(({ type, words }) => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -6,8 +6,6 @@ export interface Word {
|
|||||||
id: number;
|
id: number;
|
||||||
english: string;
|
english: string;
|
||||||
hindi: string;
|
hindi: string;
|
||||||
type: string;
|
|
||||||
gender?: "m" | "f" | null;
|
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
examples?: Example[];
|
examples?: Example[];
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
@ -21,10 +19,14 @@ export interface Example {
|
|||||||
note?: string | null;
|
note?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TagLevel = 'page' | 'subheading' | 'grammar' | 'gender';
|
||||||
|
|
||||||
export interface Tag {
|
export interface Tag {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
level: TagLevel;
|
||||||
|
parentPageId?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeeAlso {
|
export interface SeeAlso {
|
||||||
@ -38,11 +40,9 @@ export interface SeeAlso {
|
|||||||
export interface NewWord {
|
export interface NewWord {
|
||||||
hindi: string;
|
hindi: string;
|
||||||
english: string;
|
english: string;
|
||||||
type: string;
|
|
||||||
gender?: "m" | "f";
|
|
||||||
note?: string;
|
note?: string;
|
||||||
examples?: NewExample[];
|
examples?: NewExample[];
|
||||||
tagIds?: number[];
|
tagIds?: number[]; // Now includes grammar and gender tags
|
||||||
seeAlso?: NewSeeAlso[];
|
seeAlso?: NewSeeAlso[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +55,8 @@ export interface NewExample {
|
|||||||
export interface NewTag {
|
export interface NewTag {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
level: TagLevel;
|
||||||
|
parentPageId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NewSeeAlso {
|
export interface NewSeeAlso {
|
||||||
@ -75,6 +77,8 @@ export const tagSchema = z.object({
|
|||||||
id: z.number(),
|
id: z.number(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional().nullable(),
|
||||||
|
level: z.enum(['page', 'subheading', 'grammar', 'gender']),
|
||||||
|
parentPageId: z.number().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const seeAlsoSchema = z.object({
|
export const seeAlsoSchema = z.object({
|
||||||
@ -87,8 +91,6 @@ export const wordSchema = z.object({
|
|||||||
id: z.number(),
|
id: z.number(),
|
||||||
english: z.string(),
|
english: z.string(),
|
||||||
hindi: z.string(),
|
hindi: z.string(),
|
||||||
type: z.string(),
|
|
||||||
gender: z.enum(["m", "f"]).optional().nullable(),
|
|
||||||
note: z.string().optional().nullable(),
|
note: z.string().optional().nullable(),
|
||||||
examples: z.array(exampleSchema).optional(),
|
examples: z.array(exampleSchema).optional(),
|
||||||
tags: z.array(tagSchema).optional(),
|
tags: z.array(tagSchema).optional(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user