Some fixes
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s

This commit is contained in:
ryan 2025-10-02 18:06:03 -07:00
parent ed7cf8a254
commit 58e67fede3
10 changed files with 350 additions and 42 deletions

View File

@ -0,0 +1 @@
ALTER TABLE "words" ALTER COLUMN "type" SET NOT NULL;

View File

@ -0,0 +1,287 @@
{
"id": "318667f9-2cec-43d4-9e1c-9c6ddedb8664",
"prevId": "e5ed4824-2e96-4821-84c9-c33bd5d1cda8",
"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
}
},
"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": {}
}
}

View File

@ -8,6 +8,13 @@
"when": 1759431829568, "when": 1759431829568,
"tag": "0000_bouncy_demogoblin", "tag": "0000_bouncy_demogoblin",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1759452792993,
"tag": "0001_luxuriant_iceman",
"breakpoints": true
} }
] ]
} }

View File

@ -256,15 +256,12 @@ export default function AddVocabForm() {
</label> </label>
))} ))}
</div> </div>
<button type="button" onClick={() => setShowNewTag(!showNewTag)}>
{showNewTag ? 'Cancel' : 'New Category'}
</button>
</div> </div>
{showNewTag && ( {showNewTag && (
<> <>
<div className="form-group"> <div className="form-group new-word">
<label htmlFor="newTagName">Tag Name:</label> <label htmlFor="newTagName">Name:</label>
<input <input
type="text" type="text"
id="newTagName" id="newTagName"
@ -274,18 +271,21 @@ export default function AddVocabForm() {
required={showNewTag} required={showNewTag}
/> />
</div> </div>
<div className="form-group"> <div className="form-group new-word">
<label htmlFor="newTagDescription">Tag Description:</label> <label htmlFor="newTagDescription">Description:</label>
<input <textarea
type="text"
id="newTagDescription" id="newTagDescription"
value={newTagDescription} value={newTagDescription}
onChange={(e) => setNewTagDescription(e.target.value)} onChange={(e) => setNewTagDescription(e.target.value)}
rows={3}
placeholder="e.g., Words related to food and beverages" placeholder="e.g., Words related to food and beverages"
/> />
</div> </div>
</> </>
)} )}
<button type="button" onClick={() => setShowNewTag(!showNewTag)}>
{showNewTag ? 'Cancel' : 'New Category'}
</button>
<h3>New Word</h3> <h3>New Word</h3>
<div className="form-group new-word"> <div className="form-group new-word">
<label htmlFor="english">English:</label> <label htmlFor="english">English:</label>
@ -315,8 +315,18 @@ export default function AddVocabForm() {
<label htmlFor="type">Type:</label> <label htmlFor="type">Type:</label>
<select <select
id="type" id="type"
value={formData.type} value={['noun', 'verb', 'adjective', 'adverb', 'pronoun', 'conjunction', 'preposition', 'interjection'].includes(formData.type) ? formData.type : 'custom'}
onChange={(e) => setFormData({ ...formData, type: e.target.value })} onChange={(e) => {
const value = e.target.value;
if (value === 'custom') {
const customValue = prompt('Enter custom type:', formData.type);
if (customValue) {
setFormData({ ...formData, type: customValue });
}
} else {
setFormData({ ...formData, type: value });
}
}}
> >
<option value="noun">Noun</option> <option value="noun">Noun</option>
<option value="verb">Verb</option> <option value="verb">Verb</option>
@ -326,10 +336,11 @@ export default function AddVocabForm() {
<option value="conjunction">Conjunction</option> <option value="conjunction">Conjunction</option>
<option value="preposition">Preposition</option> <option value="preposition">Preposition</option>
<option value="interjection">Interjection</option> <option value="interjection">Interjection</option>
<option value="custom">{['noun', 'verb', 'adjective', 'adverb', 'pronoun', 'conjunction', 'preposition', 'interjection'].includes(formData.type) ? 'Custom...' : `Custom: ${formData.type}`}</option>
</select> </select>
</div> </div>
<div className={`form-group new-word ${formData.type !== 'noun' ? 'hidden' : ''}`}> <div className="form-group new-word">
<label htmlFor="gender">Gender:</label> <label htmlFor="gender">Gender:</label>
<select <select
id="gender" id="gender"

View File

@ -1,21 +1,15 @@
--- ---
// @ts-ignore
import Default from "@astrojs/starlight/components/PageSidebar.astro"; import Default from "@astrojs/starlight/components/PageSidebar.astro";
// @ts-ignore
import MobileTableOfContents from "@astrojs/starlight/components/MobileTableOfContents.astro"; import MobileTableOfContents from "@astrojs/starlight/components/MobileTableOfContents.astro";
import { Icon } from "@astrojs/starlight/components";
const isLearningPage = Astro.locals.starlightRoute.id === "learn"; const isLearningPage = Astro.locals.starlightRoute.id === "learn";
import { getCollection } from "astro:content"; import { storage } from "@/lib/storage";
const categories = await getCollection("vocabList"); const categories = await storage.getAllTags();
const typesSet = new Set<string>(); const types = await storage.getAllTypes();
categories.forEach((category) => { const words = await storage.getAllWords();
category.data.words.forEach((word) => {
if (word.type) {
typesSet.add(word.type);
}
});
});
const types = Array.from(typesSet).sort();
--- ---
{ {
@ -33,12 +27,12 @@ const types = Array.from(typesSet).sort();
<input <input
data-filter="category" data-filter="category"
type="checkbox" type="checkbox"
id={category.id} id={category.name}
name={category.id} name={category.name}
value={category.id} value={category.id}
checked checked
/> />
<label for={category.id}>{category.id}</label> <label for={category.name}>{category.name}</label>
</div> </div>
))} ))}
</div> </div>
@ -50,9 +44,9 @@ const types = Array.from(typesSet).sort();
<input <input
data-filter="type" data-filter="type"
type="checkbox" type="checkbox"
id={type} id={type.toString()}
name={type} name={type.toString()}
value={type} value={type.toString()}
checked checked
/> />
<label for={type}>{type}</label> <label for={type}>{type}</label>

View File

@ -2,6 +2,7 @@
import Default from "@astrojs/starlight/components/Sidebar.astro"; import Default from "@astrojs/starlight/components/Sidebar.astro";
import SidebarPersister from "@astrojs/starlight/components/SidebarPersister.astro"; import SidebarPersister from "@astrojs/starlight/components/SidebarPersister.astro";
import SidebarSublist from "@astrojs/starlight/components/SidebarSublist.astro"; import SidebarSublist from "@astrojs/starlight/components/SidebarSublist.astro";
// @ts-ignore
import MobileMenuFooter from "virtual:starlight/components/MobileMenuFooter"; 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";
@ -10,11 +11,11 @@ const tags = await storage.getAllTags();
const { sidebar: astroSidebar } = Astro.locals.starlightRoute; const { sidebar: astroSidebar } = Astro.locals.starlightRoute;
const tagsFromApi = { const tagsFromApi = {
type: "group", type: "group" as const,
label: "Vocabulary", label: "Vocabulary",
badge: undefined, badge: undefined,
entries: tags.map((tag) => ({ entries: tags.map((tag) => ({
type: "link", type: "link" as const,
label: titlecase(tag.name), label: titlecase(tag.name),
href: `/vocabulary/${tag.name}`, href: `/vocabulary/${tag.name}`,
isCurrent: Astro.url.pathname === `/vocabulary/${tag.name}`, isCurrent: Astro.url.pathname === `/vocabulary/${tag.name}`,
@ -24,7 +25,7 @@ const tagsFromApi = {
}; };
// Replace vocabulary with tags fetched from api // Replace vocabulary with tags fetched from api
const sidebar = astroSidebar.map((item) => { const sidebar = astroSidebar.map((item: any) => {
if (item.label === "Vocabulary") { if (item.label === "Vocabulary") {
return tagsFromApi; return tagsFromApi;
} }

View File

@ -5,7 +5,7 @@ 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'), // noun, verb, adjective, etc. type: text('type').notNull(), // noun, verb, adjective, etc.
gender: text('gender'), // m, f, or null 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(),

View File

@ -103,7 +103,7 @@ class PGStorage {
return await db.query.tags.findMany(); return await db.query.tags.findMany();
} }
async getAllTypes(): Promise<String[]> { async getAllTypes(): Promise<string[]> {
const types = new Set<string>(); const types = new Set<string>();
const allWords = await this.getAllWords(); const allWords = await this.getAllWords();
allWords.forEach((word) => { allWords.forEach((word) => {
@ -118,7 +118,7 @@ class PGStorage {
.values({ .values({
hindi: newWord.hindi, hindi: newWord.hindi,
english: newWord.english, english: newWord.english,
type: newWord.type || null, type: newWord.type,
gender: newWord.gender || null, gender: newWord.gender || null,
note: newWord.note || null, note: newWord.note || null,
}) })

View File

@ -7,6 +7,13 @@ import markdownItMark from "markdown-it-mark";
import { storage } from "@/lib/storage"; import { storage } from "@/lib/storage";
import { titlecase } from "@/lib/utils"; 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;
}
const md = markdownit().use(markdownItMark); const md = markdownit().use(markdownItMark);
export const prerender = false; export const prerender = false;
@ -28,7 +35,7 @@ const words = await storage.getWordsByTag(tag);
const wordtypes = [...new Set(words.map((word) => word.type).filter(Boolean))]; const wordtypes = [...new Set(words.map((word) => word.type).filter(Boolean))];
const wordsByType = wordtypes.map((type) => ({ const wordsByType = wordtypes.map((type) => ({
type: titlecase(type!) + "s", type: typeToTitle(type),
words: words.filter((word) => word.type === type), words: words.filter((word) => word.type === type),
})); }));
@ -37,7 +44,7 @@ const headings = wordsByType.flatMap(({ type, words }) => {
{ {
text: type, text: type,
depth: 2, depth: 2,
slug: type.toLowerCase().replace(/\s+/g, "-"), slug: type!.toLowerCase().replace(/\s+/g, "-"),
}, },
].concat( ].concat(
words.map((word) => ({ words.map((word) => ({
@ -61,7 +68,7 @@ const headings = wordsByType.flatMap(({ type, words }) => {
{ {
wordsByType.map(({ type, words }) => ( wordsByType.map(({ type, words }) => (
<div class="word-type-section"> <div class="word-type-section">
<AnchorHeading level="3" id={type}> <AnchorHeading level="3" id={type!}>
{type} {type}
</AnchorHeading> </AnchorHeading>
<ul class="part-of-speech-list"> <ul class="part-of-speech-list">

View File

@ -6,7 +6,7 @@ export interface Word {
id: number; id: number;
english: string; english: string;
hindi: string; hindi: string;
type?: string | null; type: string;
gender?: "m" | "f" | null; gender?: "m" | "f" | null;
note?: string | null; note?: string | null;
examples?: Example[]; examples?: Example[];
@ -38,7 +38,7 @@ export interface SeeAlso {
export interface NewWord { export interface NewWord {
hindi: string; hindi: string;
english: string; english: string;
type?: string; type: string;
gender?: "m" | "f"; gender?: "m" | "f";
note?: string; note?: string;
examples?: NewExample[]; examples?: NewExample[];
@ -87,7 +87,7 @@ 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().optional().nullable(), type: z.string(),
gender: z.enum(["m", "f"]).optional().nullable(), 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(),