Dynamic sidebar
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
8c924ed54f
commit
675849b884
@ -97,6 +97,7 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
components: {
|
||||
Sidebar: "@/components/Sidebar.astro",
|
||||
PageSidebar: "@/components/PageSidebar.astro",
|
||||
},
|
||||
}),
|
||||
|
||||
@ -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"] {
|
||||
|
||||
@ -37,7 +37,6 @@ export default function FlashCard({ word }: { word: VocabWord }) {
|
||||
function flipCard() {
|
||||
setIsFlipped(!isFlipped);
|
||||
}
|
||||
console.log(options);
|
||||
|
||||
return (
|
||||
<div className="flash-card">
|
||||
|
||||
@ -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">
|
||||
|
||||
41
hindki/src/components/Sidebar.astro
Normal file
41
hindki/src/components/Sidebar.astro
Normal 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>
|
||||
@ -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
7
hindki/src/lib/utils.ts
Normal 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(" ");
|
||||
}
|
||||
25
hindki/src/pages/api/types.json.ts
Normal file
25
hindki/src/pages/api/types.json.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -8,6 +8,7 @@ import AddVocabForm from "../../components/AddVocabForm.tsx";
|
||||
title: "Add Vocabulary",
|
||||
tableOfContents: false,
|
||||
prev: false,
|
||||
next: false,
|
||||
}}
|
||||
>
|
||||
<AddVocabForm client:only="react" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -452,4 +445,4 @@ mark {
|
||||
.sl-markdown-content,
|
||||
main {
|
||||
background-color: var(--sl-color-bg);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user