Dynamic sidebar
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s

This commit is contained in:
ryan 2025-10-02 15:14:17 -07:00
parent 8c924ed54f
commit 675849b884
11 changed files with 178 additions and 76 deletions

View File

@ -97,6 +97,7 @@ export default defineConfig({
}, },
], ],
components: { components: {
Sidebar: "@/components/Sidebar.astro",
PageSidebar: "@/components/PageSidebar.astro", PageSidebar: "@/components/PageSidebar.astro",
}, },
}), }),

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import type { Tag } from '@/types/types'; import type { Tag } from '@/types/types';
import { titlecase } from '@/lib/utils';
interface WordFormData { interface WordFormData {
english: string; english: string;
@ -242,7 +243,7 @@ export default function AddVocabForm() {
<div className="vocab-form"> <div className="vocab-form">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<label>Tags:</label> <h3>Categories</h3>
<div className="tag-checkboxes"> <div className="tag-checkboxes">
{tags.map((tag) => ( {tags.map((tag) => (
<label key={tag.id} className="tag-checkbox"> <label key={tag.id} className="tag-checkbox">
@ -251,13 +252,12 @@ export default function AddVocabForm() {
checked={selectedTagIds.includes(tag.id)} checked={selectedTagIds.includes(tag.id)}
onChange={() => toggleTag(tag.id)} onChange={() => toggleTag(tag.id)}
/> />
{tag.name} <span>{titlecase(tag.name)}</span>
{tag.description && <span className="tag-desc"> - {tag.description}</span>}
</label> </label>
))} ))}
</div> </div>
<button type="button" onClick={() => setShowNewTag(!showNewTag)}> <button type="button" onClick={() => setShowNewTag(!showNewTag)}>
{showNewTag ? 'Cancel' : '+ Create New Tag'} {showNewTag ? 'Cancel' : 'New Category'}
</button> </button>
</div> </div>
@ -286,8 +286,8 @@ export default function AddVocabForm() {
</div> </div>
</> </>
)} )}
<h3>New Word</h3>
<div className="form-group"> <div className="form-group new-word">
<label htmlFor="english">English:</label> <label htmlFor="english">English:</label>
<input <input
type="text" type="text"
@ -300,7 +300,7 @@ export default function AddVocabForm() {
/> />
</div> </div>
<div className="form-group"> <div className="form-group new-word">
<label htmlFor="hindi">Hindi:</label> <label htmlFor="hindi">Hindi:</label>
<input <input
type="text" type="text"
@ -311,7 +311,7 @@ export default function AddVocabForm() {
/> />
</div> </div>
<div className="form-group"> <div className="form-group new-word">
<label htmlFor="type">Type:</label> <label htmlFor="type">Type:</label>
<select <select
id="type" id="type"
@ -329,8 +329,8 @@ export default function AddVocabForm() {
</select> </select>
</div> </div>
<div className="form-group"> <div className={`form-group new-word ${formData.type !== 'noun' ? 'hidden' : ''}`}>
<label htmlFor="gender">Gender (for nouns):</label> <label htmlFor="gender">Gender:</label>
<select <select
id="gender" id="gender"
value={formData.gender ?? ''} value={formData.gender ?? ''}
@ -348,7 +348,7 @@ export default function AddVocabForm() {
</select> </select>
</div> </div>
<div className="form-group"> <div className="form-group new-word">
<label htmlFor="note">Note:</label> <label htmlFor="note">Note:</label>
<textarea <textarea
id="note" id="note"
@ -358,7 +358,7 @@ export default function AddVocabForm() {
/> />
</div> </div>
<div className="form-group"> <div className="form-group new-word">
<label>Examples:</label> <label>Examples:</label>
{formData.examples?.map((example, index) => ( {formData.examples?.map((example, index) => (
<div key={index} className="example-group"> <div key={index} className="example-group">
@ -390,7 +390,7 @@ export default function AddVocabForm() {
</button> </button>
</div> </div>
<div className="form-group"> <div className="form-group new-word">
<label>See Also:</label> <label>See Also:</label>
{formData.seeAlso?.map((seeAlso, index) => ( {formData.seeAlso?.map((seeAlso, index) => (
<div key={index} className="see-also-group"> <div key={index} className="see-also-group">
@ -428,19 +428,33 @@ export default function AddVocabForm() {
)} )}
<style>{` <style>{`
.hidden {
display: none !important;
}
.vocab-form { .vocab-form {
font-size: 1rem;
max-width: 600px; max-width: 600px;
margin: 2rem 0; margin: 2rem 0;
font-family: var(--font-sans);
} }
.form-group { .form-group.new-word {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
display: grid;
grid-template-columns: 75px 1fr;
align-items: baseline;
gap: 1rem;
}
.new-word.form-group label {
text-align: right;
display: inline-block;
} }
.form-group label { .form-group label {
display: block; display: flex;
margin-bottom: 0.5rem; gap: 0.5rem;
font-weight: 600;
} }
.form-group input, .form-group input,
@ -455,17 +469,25 @@ export default function AddVocabForm() {
} }
.tag-checkboxes { .tag-checkboxes {
display: flex; display: grid;
flex-direction: column; grid-template-columns: repeat(auto-fit, minmax(175px, 1fr));
gap: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.form-group button {
margin: 0;
font-family: inherit;
white-space: nowrap;
width: min-content;
padding: 0 1rem;
}
.tag-checkbox { .tag-checkbox {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
font-weight: normal; font-weight: normal;
margin-top: 0;
} }
.tag-checkbox input[type="checkbox"] { .tag-checkbox input[type="checkbox"] {

View File

@ -37,7 +37,6 @@ export default function FlashCard({ word }: { word: VocabWord }) {
function flipCard() { function flipCard() {
setIsFlipped(!isFlipped); setIsFlipped(!isFlipped);
} }
console.log(options);
return ( return (
<div className="flash-card"> <div className="flash-card">

View File

@ -26,7 +26,7 @@ const types = Array.from(typesSet).sort();
<div class="sl-container"> <div class="sl-container">
<div class="filters"> <div class="filters">
<div class="category-filters"> <div class="category-filters">
<h2>Categories</h2> <h2>Categoriezs</h2>
{/* Check boxes for each category */} {/* Check boxes for each category */}
{categories.map((category) => ( {categories.map((category) => (
<div class="filter"> <div class="filter">

View File

@ -0,0 +1,41 @@
---
import Default from "@astrojs/starlight/components/Sidebar.astro";
import SidebarPersister from "@astrojs/starlight/components/SidebarPersister.astro";
import SidebarSublist from "@astrojs/starlight/components/SidebarSublist.astro";
import MobileMenuFooter from "virtual:starlight/components/MobileMenuFooter";
import { storage } from "../lib/storage";
import { titlecase } from "@/lib/utils";
const tags = await storage.getAllTags();
const { sidebar: astroSidebar } = Astro.locals.starlightRoute;
const tagsFromApi = {
type: "group",
label: "Vocabulary",
badge: undefined,
entries: tags.map((tag) => ({
type: "link",
label: titlecase(tag.name),
href: `/vocabulary/${tag.name}`,
isCurrent: Astro.url.pathname === `/vocabulary/${tag.name}`,
badge: undefined,
attrs: {},
})),
};
// Replace vocabulary with tags fetched from api
const sidebar = astroSidebar.map((item) => {
if (item.label === "Vocabulary") {
return tagsFromApi;
}
return item;
});
---
<SidebarPersister>
<SidebarSublist sublist={sidebar} />
</SidebarPersister>
<div class="md:sl-hidden">
<MobileMenuFooter />
</div>

View File

@ -3,7 +3,7 @@ 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 } from "@/types/types";
class SQLiteStorage { class PGStorage {
async getAllWords(): Promise<Word[]> { async getAllWords(): Promise<Word[]> {
const allWords = await db.query.words.findMany({ const allWords = await db.query.words.findMany({
with: { with: {
@ -103,6 +103,15 @@ class SQLiteStorage {
return await db.query.tags.findMany(); return await db.query.tags.findMany();
} }
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();
}
async createWord(newWord: NewWord): Promise<Word> { async createWord(newWord: NewWord): Promise<Word> {
const [word] = await db const [word] = await db
.insert(words) .insert(words)
@ -170,4 +179,4 @@ class SQLiteStorage {
} }
} }
export const storage = new SQLiteStorage(); export const storage = new PGStorage();

7
hindki/src/lib/utils.ts Normal file
View File

@ -0,0 +1,7 @@
export function titlecase(str: string) {
return str.replaceAll(/-/g, " ").replaceAll(/_/g, " ").split(" ").map(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);
}).join(" ");
}

View File

@ -0,0 +1,25 @@
import type { APIRoute } from 'astro';
import { storage } from '../../lib/storage';
export const prerender = false;
export const GET: APIRoute = async () => {
try {
const types = await storage.getAllTypes();
return new Response(JSON.stringify(types), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
console.error('Error in GET /api/types:', error);
return new Response(JSON.stringify({ error: 'Failed to read word types' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
});
}
};

View File

@ -8,6 +8,7 @@ import AddVocabForm from "../../components/AddVocabForm.tsx";
title: "Add Vocabulary", title: "Add Vocabulary",
tableOfContents: false, tableOfContents: false,
prev: false, prev: false,
next: false,
}} }}
> >
<AddVocabForm client:only="react" /> <AddVocabForm client:only="react" />

View File

@ -1,30 +1,27 @@
--- ---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro'; import AnchorHeading from "@astrojs/starlight/components/AnchorHeading.astro";
import VocabWord from '@/components/VocabWord.astro'; 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";
const md = markdownit().use(markdownItMark); const md = markdownit().use(markdownItMark);
export const prerender = false; export const prerender = false;
function titlecase(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
const { tag } = Astro.params; const { tag } = Astro.params;
if (!tag) { if (!tag) {
return Astro.redirect('/vocabulary'); return Astro.redirect("/vocabulary");
} }
const allTags = await storage.getAllTags(); const allTags = await storage.getAllTags();
const currentTag = allTags.find(t => t.name === tag); const currentTag = allTags.find((t) => t.name === tag);
if (!currentTag) { if (!currentTag) {
return Astro.redirect('/vocabulary'); return Astro.redirect("/vocabulary");
} }
const words = await storage.getWordsByTag(tag); const words = await storage.getWordsByTag(tag);
@ -35,16 +32,20 @@ const wordsByType = wordtypes.map((type) => ({
words: words.filter((word) => word.type === type), words: words.filter((word) => word.type === type),
})); }));
const headings = wordsByType.flatMap(({type, words}) => { const headings = wordsByType.flatMap(({ type, words }) => {
return [{ return [
text: type, {
depth: 2, text: type,
slug: type.toLowerCase().replace(/\s+/g, '-'), depth: 2,
}].concat(words.map((word) => ({ slug: type.toLowerCase().replace(/\s+/g, "-"),
text: word.hindi, },
depth: 3, ].concat(
slug: word.hindi.toLowerCase().replace(/\s+/g, '-'), words.map((word) => ({
}))); text: word.hindi,
depth: 3,
slug: word.hindi.toLowerCase().replace(/\s+/g, "-"),
})),
);
}); });
--- ---
@ -52,20 +53,23 @@ const headings = wordsByType.flatMap(({type, words}) => {
frontmatter={{ title: "Vocabulary: " + titlecase(tag) }} frontmatter={{ title: "Vocabulary: " + titlecase(tag) }}
headings={headings} headings={headings}
> >
{currentTag.description && <div set:html={md.render(currentTag.description)}/>}
{ {
wordsByType.map(({type, words}) => ( currentTag.description && (
<div set:html={md.render(currentTag.description)} />
)
}
{
wordsByType.map(({ type, words }) => (
<div class="word-type-section"> <div class="word-type-section">
<AnchorHeading level="3" id={type}>{type}</AnchorHeading> <AnchorHeading level="3" id={type}>
<ul class="part-of-speech-list"> {type}
{ </AnchorHeading>
words.map((word) => ( <ul class="part-of-speech-list">
<VocabWord {word} /> {words.map((word) => (
)) <VocabWord {word} />
} ))}
</ul> </ul>
</div> </div>
)) ))
} }
</StarlightPage> </StarlightPage>

View File

@ -267,27 +267,20 @@ mark {
} }
/* Override the exact Starlight rule that's causing issues */ /* Override the exact Starlight rule that's causing issues */
.sl-markdown-content .sl-markdown-content [yaml-editor] :not(a, strong, em, del, span, input, code, br)+ :not(a, strong, em, del, span, input, code, br, :where(.not-content *)) {
[yaml-editor]
:not(a, strong, em, del, span, input, code, br)
+ :not(a, strong, em, del, span, input, code, br, :where(.not-content *)) {
margin-top: 0 !important; margin-top: 0 !important;
} }
/* Also target if the YAMLEditor itself is within sl-markdown-content */ /* Also target if the YAMLEditor itself is within sl-markdown-content */
.sl-markdown-content .sl-markdown-content :not(a, strong, em, del, span, input, code, br)+[yaml-editor]:not(a,
:not(a, strong, em, del, span, input, code, br) strong,
+ [yaml-editor]:not( em,
a, del,
strong, span,
em, input,
del, code,
span, br,
input, :where(.not-content *)) {
code,
br,
:where(.not-content *)
) {
margin-top: 0 !important; margin-top: 0 !important;
} }
@ -452,4 +445,4 @@ mark {
.sl-markdown-content, .sl-markdown-content,
main { main {
background-color: var(--sl-color-bg); background-color: var(--sl-color-bg);
} }