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: {
|
components: {
|
||||||
|
Sidebar: "@/components/Sidebar.astro",
|
||||||
PageSidebar: "@/components/PageSidebar.astro",
|
PageSidebar: "@/components/PageSidebar.astro",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -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"] {
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
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 { 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
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",
|
title: "Add Vocabulary",
|
||||||
tableOfContents: false,
|
tableOfContents: false,
|
||||||
prev: false,
|
prev: false,
|
||||||
|
next: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AddVocabForm client:only="react" />
|
<AddVocabForm client:only="react" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user