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: {
Sidebar: "@/components/Sidebar.astro",
PageSidebar: "@/components/PageSidebar.astro",
},
}),

View File

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

View File

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

View File

@ -26,7 +26,7 @@ const types = Array.from(typesSet).sort();
<div class="sl-container">
<div class="filters">
<div class="category-filters">
<h2>Categories</h2>
<h2>Categoriezs</h2>
{/* Check boxes for each category */}
{categories.map((category) => (
<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 type { Word, Tag, NewWord } from "@/types/types";
class SQLiteStorage {
class PGStorage {
async getAllWords(): Promise<Word[]> {
const allWords = await db.query.words.findMany({
with: {
@ -103,6 +103,15 @@ class SQLiteStorage {
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> {
const [word] = await db
.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",
tableOfContents: false,
prev: false,
next: false,
}}
>
<AddVocabForm client:only="react" />

View File

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

View File

@ -267,27 +267,20 @@ mark {
}
/* Override the exact Starlight rule that's causing issues */
.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 *)) {
.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 *)) {
margin-top: 0 !important;
}
/* Also target if the YAMLEditor itself is within sl-markdown-content */
.sl-markdown-content
:not(a, strong, em, del, span, input, code, br)
+ [yaml-editor]:not(
a,
strong,
em,
del,
span,
input,
code,
br,
:where(.not-content *)
) {
.sl-markdown-content :not(a, strong, em, del, span, input, code, br)+[yaml-editor]:not(a,
strong,
em,
del,
span,
input,
code,
br,
:where(.not-content *)) {
margin-top: 0 !important;
}