Merge pull request #13 from OHTU-German-learning-website/master

merge
This commit is contained in:
erzabytyci
2026-06-07 16:15:56 +03:00
committed by GitHub
5 changed files with 229 additions and 53 deletions
+84 -4
View File
@@ -9,13 +9,51 @@
margin: 0 0 var(--u-sm); margin: 0 0 var(--u-sm);
} }
.glossary-index-item { .glossary-index-letter {
display: inline-block; display: inline-flex;
padding: 0.35rem 0.75rem; align-items: center;
justify-content: center;
padding: 0.45rem 0.75rem;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background-color: var(--bg3); background-color: var(--bg3);
color: var(--fg1); color: var(--fg1);
font-size: 0.95rem; font-weight: 600;
text-decoration: none;
border: 1px solid transparent;
transition: border-color 0.2s, background-color 0.2s;
}
.glossary-index-letter:hover,
.glossary-index-letter:focus {
background-color: var(--bg4);
border-color: var(--bg5);
}
.glossary-index-group {
padding: var(--u-sm);
border-radius: var(--radius-sm);
background-color: var(--bg3);
display: grid;
gap: var(--u-2xs);
}
.glossary-index-group-title {
margin: 0 0 var(--u-xs);
font-size: 1rem;
}
.glossary-index-entry {
display: inline-block;
color: var(--fg1);
text-decoration: none;
padding: 0.35rem 0.5rem;
border-radius: var(--radius-sm);
transition: background-color 0.2s;
}
.glossary-index-entry:hover,
.glossary-index-entry:focus {
background-color: var(--bg4);
} }
.glossary-card { .glossary-card {
@@ -25,18 +63,60 @@
border: 1px solid var(--bg3); border: 1px solid var(--bg3);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s; transition: box-shadow 0.2s;
scroll-margin-top: 100px;
} }
.glossary-card:hover { .glossary-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
} }
.glossary-letter-group {
border-top: 1px solid var(--bg3);
padding-top: var(--u-md);
}
.glossary-letter-title {
margin: 0 0 var(--u-sm);
font-size: 1.35rem;
scroll-margin-top: 100px;
}
.glossary-back-to-top {
display: inline-flex;
align-items: center;
color: var(--fg2);
text-decoration: none;
font-size: 0.95rem;
margin-top: var(--u-sm);
}
.glossary-back-to-top:hover,
.glossary-back-to-top:focus {
color: var(--fg1);
text-decoration: underline;
}
.glossary-word { .glossary-word {
color: var(--fg1); color: var(--fg1);
margin: 0; margin: 0;
font-size: 1.2rem; font-size: 1.2rem;
} }
.glossary-word-link {
text-decoration: none;
display: block;
border-radius: var(--radius-sm);
transition: background-color 0.2s, padding 0.2s;
padding: 0.25rem 0.5rem;
margin: -0.25rem -0.5rem;
}
.glossary-word-link:hover,
.glossary-word-link:focus {
background-color: var(--bg4);
}
.glossary-definition { .glossary-definition {
color: var(--fg2); color: var(--fg2);
margin: 0.5rem 0; margin: 0.5rem 0;
+108 -46
View File
@@ -57,27 +57,66 @@ export default function GlossaryList() {
a.word.localeCompare(b.word) a.word.localeCompare(b.word)
); );
const entriesByLetter = sortedEntries.reduce((map, entry) => {
const letter = (entry.word?.[0] || "").toUpperCase();
if (!map[letter]) {
map[letter] = [];
}
map[letter].push(entry);
return map;
}, {});
const letters = Object.keys(entriesByLetter).sort();
return ( return (
<Column gap="md" width="100%"> <Column gap="md" width="100%">
<Row justify="space-between" gap="xl" width="100%" align="center"> <Row justify="space-between" gap="xl" width="100%" align="center">
<h2>Glossareinträge</h2> <h2 id="glossary-top">Glossareinträge</h2>
</Row> </Row>
<LinkButton href="/admin/glossary/create"> <LinkButton href="/admin/glossary/create">
Neuen Eintrag erstellen Neuen Eintrag erstellen
</LinkButton> </LinkButton>
{sortedEntries.length > 0 && ( {letters.length > 0 && (
<Container className="glossary-index" mb="md"> <Container className="glossary-index" mb="md">
<h3>Register</h3> <h3>Register</h3>
<Container display="flex" flexWrap="wrap" gap="xs" mb="sm">
{letters.map((letter) => (
<a
key={letter}
className="glossary-index-letter"
href={`#letter-${letter}-section`}
>
{letter}
</a>
))}
</Container>
<Container <Container
display="grid" display="grid"
templateColumns="repeat(auto-fit, minmax(160px, 1fr))" templateColumns="repeat(auto-fit, minmax(160px, 1fr))"
gap="sm" gap="sm"
> >
{sortedEntries.map((entry) => ( {letters.map((letter) => (
<span key={entry.id} className="glossary-index-item"> <Container key={letter} className="glossary-index-group">
{entry.word} <h4
</span> id={`letter-${letter}`}
className="glossary-index-group-title"
>
{letter}
</h4>
<Column gap="xs">
{entriesByLetter[letter].map((entry) => (
<a
key={entry.id}
href={`#entry-${entry.id}`}
className="glossary-index-entry"
>
{entry.word}
</a>
))}
</Column>
</Container>
))} ))}
</Container> </Container>
</Container> </Container>
@@ -89,46 +128,69 @@ export default function GlossaryList() {
beginnen. beginnen.
</p> </p>
) : ( ) : (
<Container <Container display="grid" gap="md">
display="grid" {letters.map((letter) => (
templateColumns="repeat(auto-fill, minmax(300px, 1fr))" <Container key={letter} className="glossary-letter-group">
gap="md" <h3
> id={`letter-${letter}-section`}
{sortedEntries.map((entry) => ( className="glossary-letter-title"
<Container key={entry.id} className="glossary-card"> >
<Column gap="sm"> {letter}
<h4 className="glossary-word">{entry.word}</h4> </h3>
<RenderHTML data={entry.word_definition} disableGlossary /> <Container
<Row justify="space-between" className="glossary-meta"> display="grid"
<span> templateColumns="repeat(auto-fill, minmax(300px, 1fr))"
Erstellt: {new Date(entry.created_at).toLocaleDateString()} gap="md"
</span> >
<Row gap="sm"> {entriesByLetter[letter].map((entry) => (
<LinkButton <Container
href={`/admin/glossary/create?id=${entry.id}`} key={entry.id}
variant="outline" id={`entry-${entry.id}`}
size="sm" className="glossary-card"
> >
Bearbeiten <Column gap="sm">
</LinkButton> <h4 className="glossary-word">{entry.word}</h4>
<Button <RenderHTML
variant={ data={entry.word_definition}
confirmDelete === entry.id ? "danger" : "outline" disableGlossary
} />
size="sm" <Row justify="space-between" className="glossary-meta">
width="fit" <span>
onClick={() => handleDelete(entry.id)} Erstellt:{" "}
disabled={deleting === entry.id} {new Date(entry.created_at).toLocaleDateString()}
> </span>
{deleting === entry.id <Row gap="sm">
? "Löschen..." <LinkButton
: confirmDelete === entry.id href={`/admin/glossary/create?id=${entry.id}`}
? "Bestätigen" variant="outline"
: "Löschen"} size="sm"
</Button> >
</Row> Bearbeiten
</Row> </LinkButton>
</Column> <Button
variant={
confirmDelete === entry.id ? "danger" : "outline"
}
size="sm"
width="fit"
onClick={() => handleDelete(entry.id)}
disabled={deleting === entry.id}
>
{deleting === entry.id
? "Löschen..."
: confirmDelete === entry.id
? "Bestätigen"
: "Löschen"}
</Button>
</Row>
</Row>
</Column>
</Container>
))}
</Container>
<a href="#glossary-top" className="glossary-back-to-top">
Zurück nach oben
</a>
</Container> </Container>
))} ))}
</Container> </Container>
+31
View File
@@ -78,6 +78,22 @@
.glossary-letter-title { .glossary-letter-title {
margin: 0 0 var(--u-sm); margin: 0 0 var(--u-sm);
font-size: 1.35rem; font-size: 1.35rem;
scroll-margin-top: 100px;
}
.glossary-back-to-top {
display: inline-flex;
align-items: center;
color: var(--fg2);
text-decoration: none;
font-size: 0.95rem;
margin-top: var(--u-sm);
}
.glossary-back-to-top:hover,
.glossary-back-to-top:focus {
color: var(--fg1);
text-decoration: underline;
} }
.glossary-word { .glossary-word {
@@ -86,6 +102,21 @@
font-size: 1.2rem; font-size: 1.2rem;
} }
.glossary-word-link {
text-decoration: none;
display: block;
border-radius: var(--radius-sm);
transition: background-color 0.2s, padding 0.2s;
padding: 0.25rem 0.5rem;
margin: -0.25rem -0.5rem;
}
.glossary-word-link:hover,
.glossary-word-link:focus {
background-color: var(--bg4);
}
.glossary-definition { .glossary-definition {
color: var(--fg2); color: var(--fg2);
margin: 0.5rem 0; margin: 0.5rem 0;
+4 -1
View File
@@ -33,7 +33,7 @@ export default function GlossaryPage() {
return ( return (
<Column gap="md" width="100%"> <Column gap="md" width="100%">
<Row justify="space-between" gap="xl" width="100%" align="center"> <Row justify="space-between" gap="xl" width="100%" align="center">
<h2>Glossar</h2> <h2 id="glossary-top">Glossar</h2>
</Row> </Row>
{letters.length > 0 && ( {letters.length > 0 && (
@@ -114,6 +114,9 @@ export default function GlossaryPage() {
</Container> </Container>
))} ))}
</Container> </Container>
<a href="#glossary-top" className="glossary-back-to-top">
Zurück nach oben
</a>
</Container> </Container>
))} ))}
</Container> </Container>
@@ -92,7 +92,7 @@ const getValue = (cssProp, value) => {
return value; return value;
}; };
export const Container = memo(({ children, className, ...props }) => { export const Container = memo(({ children, className, id, ...props }) => {
const mediaQueries = {}; const mediaQueries = {};
const validLayoutPropsEntries = useMemo( const validLayoutPropsEntries = useMemo(
@@ -159,7 +159,7 @@ export const Container = memo(({ children, className, ...props }) => {
}; };
}, [validLayoutPropsEntries]); }, [validLayoutPropsEntries]);
return ( return (
<div style={style} className={className}> <div id={id} style={style} className={className}>
{children} {children}
</div> </div>
); );