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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ export const words = pgTable('words', {
id: serial('id').primaryKey(),
hindi: text('hindi').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
note: text('note'),
createdAt: timestamp('created_at').notNull().defaultNow(),

View File

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

View File

@ -7,6 +7,13 @@ 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;
}
const md = markdownit().use(markdownItMark);
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 wordsByType = wordtypes.map((type) => ({
type: titlecase(type!) + "s",
type: typeToTitle(type),
words: words.filter((word) => word.type === type),
}));
@ -37,7 +44,7 @@ const headings = wordsByType.flatMap(({ type, words }) => {
{
text: type,
depth: 2,
slug: type.toLowerCase().replace(/\s+/g, "-"),
slug: type!.toLowerCase().replace(/\s+/g, "-"),
},
].concat(
words.map((word) => ({
@ -61,7 +68,7 @@ const headings = wordsByType.flatMap(({ type, words }) => {
{
wordsByType.map(({ type, words }) => (
<div class="word-type-section">
<AnchorHeading level="3" id={type}>
<AnchorHeading level="3" id={type!}>
{type}
</AnchorHeading>
<ul class="part-of-speech-list">

View File

@ -6,7 +6,7 @@ export interface Word {
id: number;
english: string;
hindi: string;
type?: string | null;
type: string;
gender?: "m" | "f" | null;
note?: string | null;
examples?: Example[];
@ -38,7 +38,7 @@ export interface SeeAlso {
export interface NewWord {
hindi: string;
english: string;
type?: string;
type: string;
gender?: "m" | "f";
note?: string;
examples?: NewExample[];
@ -87,7 +87,7 @@ export const wordSchema = z.object({
id: z.number(),
english: z.string(),
hindi: z.string(),
type: z.string().optional().nullable(),
type: z.string(),
gender: z.enum(["m", "f"]).optional().nullable(),
note: z.string().optional().nullable(),
examples: z.array(exampleSchema).optional(),