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 { 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(
|
||||
|
||||
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,
|
||||
"tag": "0001_luxuriant_iceman",
|
||||
"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 (
|
||||
<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 && (
|
||||
<FlashCard word={currentWord} />
|
||||
)}
|
||||
|
||||
@ -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();
|
||||
<div class="sl-container">
|
||||
<div class="filters">
|
||||
<div class="category-filters">
|
||||
<h2>Categoriezs</h2>
|
||||
<h2>Categories</h2>
|
||||
{/* Check boxes for each category */}
|
||||
{categories.map((category) => (
|
||||
<div class="filter">
|
||||
@ -32,7 +33,7 @@ const words = await storage.getAllWords();
|
||||
value={category.id}
|
||||
checked
|
||||
/>
|
||||
<label for={category.name}>{category.name}</label>
|
||||
<label for={category.name}>{titlecase(category.name)}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -49,7 +50,7 @@ const words = await storage.getAllWords();
|
||||
value={type.toString()}
|
||||
checked
|
||||
/>
|
||||
<label for={type}>{type}</label>
|
||||
<label for={type}>{typeToTitle(type)}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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<string, ["note" | "tip", string]> = {
|
||||
"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') || [];
|
||||
---
|
||||
|
||||
<li id={word.hindi} class="word-entry">
|
||||
@ -25,29 +29,20 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
|
||||
>{word.english}</span
|
||||
>
|
||||
{
|
||||
word.gender && (
|
||||
genderTag && (
|
||||
<Badge
|
||||
variant={gender_lookup[word.gender][0]}
|
||||
text={gender_lookup[word.gender][1]}
|
||||
variant={gender_lookup[genderTag.name][0]}
|
||||
text={gender_lookup[genderTag.name][1]}
|
||||
class="gender-badge"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
word.tags &&
|
||||
word.tags
|
||||
.filter((tag: Tag) => {
|
||||
return !(
|
||||
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>
|
||||
))
|
||||
pageTags.map((tag: Tag) => (
|
||||
<a class="badge-link" href={`/vocabulary/${tag.name}`}>
|
||||
<Badge text={tag.name} class="tag-badge" />
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</AnchorHeading>
|
||||
{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';
|
||||
|
||||
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', {
|
||||
|
||||
@ -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<Word[]> {
|
||||
@ -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<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[]> {
|
||||
const types = new Set<string>();
|
||||
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<Word> {
|
||||
@ -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<Tag> {
|
||||
async createTag(name: string, level: TagLevel, description?: string, parentPageId?: number): Promise<Tag> {
|
||||
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<void> {
|
||||
|
||||
@ -5,3 +5,10 @@ export function titlecase(str: string) {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||
}).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 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 [
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user