Skip to content

Commit 2b43c28

Browse files
committed
added search and other features
1 parent b1e078a commit 2b43c28

20 files changed

Lines changed: 749 additions & 51 deletions

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!doctype html>
2-
<html lang="en">
2+
<html lang="de">
33
<head>
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/png" href="/logo.png" />

package-lock.json

Lines changed: 20 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"@fontsource/ibm-plex-mono": "^5.2.7",
14+
"@fontsource/outfit": "^5.2.8",
1315
"lucide-react": "^0.554.0",
1416
"react": "^19.2.0",
1517
"react-dom": "^19.2.0",
@@ -30,4 +32,4 @@
3032
"globals": "^16.5.0",
3133
"vite": "^5.4.0"
3234
}
33-
}
35+
}

src/components/Breadcrumbs.jsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { useLocation, Link } from 'react-router-dom';
3+
import styles from './Breadcrumbs.module.css';
4+
5+
const Breadcrumbs = () => {
6+
const { pathname } = useLocation();
7+
const parts = pathname.replace(/^\/wiki\//, '').split('/').filter(Boolean);
8+
9+
// Only show for nested paths (Folder/Page)
10+
if (parts.length < 2) return null;
11+
12+
const folder = parts[0];
13+
const page = parts[parts.length - 1];
14+
const folderLabel = folder.charAt(0).toUpperCase() + folder.slice(1).replace(/-/g, ' ');
15+
const pageLabel = page.charAt(0).toUpperCase() + page.slice(1).replace(/-/g, ' ');
16+
17+
return (
18+
<nav className={styles.breadcrumbs} aria-label="Breadcrumb">
19+
<Link to="/" className={styles.crumb}>Wiki</Link>
20+
<span className={styles.sep}></span>
21+
<Link to={`/wiki/${folder}`} className={styles.crumb}>{folderLabel}</Link>
22+
<span className={styles.sep}></span>
23+
<span className={`${styles.crumb} ${styles.current}`}>{pageLabel}</span>
24+
</nav>
25+
);
26+
};
27+
28+
export default Breadcrumbs;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.breadcrumbs {
2+
display: flex;
3+
align-items: center;
4+
gap: 0.375rem;
5+
margin-bottom: 1.5rem;
6+
font-size: 0.8rem;
7+
font-family: var(--font-sans);
8+
}
9+
10+
.crumb {
11+
color: var(--color-text-secondary);
12+
text-decoration: none;
13+
transition: color 0.15s;
14+
}
15+
16+
.crumb:hover {
17+
color: var(--color-text-primary);
18+
text-decoration: none;
19+
}
20+
21+
.current {
22+
color: var(--color-accent);
23+
font-weight: 500;
24+
}
25+
26+
.sep {
27+
color: var(--color-text-secondary);
28+
opacity: 0.4;
29+
}

src/components/Layout.jsx

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useRef, useEffect } from 'react';
22
import { Outlet } from 'react-router-dom';
3-
import { Menu, X } from 'lucide-react';
3+
import { Menu, X, Search as SearchIcon } from 'lucide-react';
44
import styles from './Layout.module.css';
55
import Sidebar from './Sidebar.jsx';
6+
import Search from './Search.jsx';
67

78
const Layout = () => {
89
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
10+
const [isSearchOpen, setIsSearchOpen] = useState(false);
11+
const [progress, setProgress] = useState(0);
12+
const contentRef = useRef(null);
913

1014
const toggleSidebar = () => {
1115
setIsSidebarOpen(!isSidebarOpen);
@@ -15,8 +19,35 @@ const Layout = () => {
1519
setIsSidebarOpen(false);
1620
};
1721

22+
const handleScroll = () => {
23+
const el = contentRef.current;
24+
if (!el) return;
25+
const pct = el.scrollTop / (el.scrollHeight - el.clientHeight) * 100;
26+
setProgress(Math.min(100, Math.max(0, pct)));
27+
};
28+
29+
useEffect(() => {
30+
const handler = (e) => {
31+
if (e.key === '/' && !isSearchOpen &&
32+
e.target.tagName !== 'INPUT' &&
33+
e.target.tagName !== 'TEXTAREA' &&
34+
!e.target.isContentEditable) {
35+
e.preventDefault();
36+
setIsSearchOpen(true);
37+
}
38+
};
39+
window.addEventListener('keydown', handler);
40+
return () => window.removeEventListener('keydown', handler);
41+
}, [isSearchOpen]);
42+
1843
return (
1944
<div className={styles.container}>
45+
<div
46+
className={styles.progressBar}
47+
style={{ width: `${progress}%` }}
48+
aria-hidden="true"
49+
/>
50+
2051
<header className={styles.header}>
2152
<button className={styles.menuButton} onClick={toggleSidebar} aria-label="Toggle Menu">
2253
{isSidebarOpen ? <X size={24} /> : <Menu size={24} />}
@@ -25,18 +56,28 @@ const Layout = () => {
2556
<img src="/logo.png" alt="Transient Wiki Logo" className={styles.logo} />
2657
<h1 className={styles.title}>Transient Wiki</h1>
2758
</div>
28-
<div style={{ width: 24 }}></div>
59+
<button
60+
className={styles.searchButton}
61+
onClick={() => setIsSearchOpen(true)}
62+
aria-label="Suchen"
63+
>
64+
<SearchIcon size={15} />
65+
<span className={styles.searchLabel}>Suchen</span>
66+
<kbd className={styles.searchKbd}>/</kbd>
67+
</button>
2968
</header>
3069

3170
<div className={styles.main}>
3271
<Sidebar isOpen={isSidebarOpen} onClose={closeSidebar} />
3372

34-
<main className={styles.content}>
73+
<main className={styles.content} ref={contentRef} onScroll={handleScroll}>
3574
<div className={styles.contentInner}>
3675
<Outlet />
3776
</div>
3877
</main>
3978
</div>
79+
80+
{isSearchOpen && <Search onClose={() => setIsSearchOpen(false)} />}
4081
</div>
4182
);
4283
};

src/components/Layout.module.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,66 @@
109109
transform: translateY(1px);
110110
}
111111

112+
.progressBar {
113+
position: fixed;
114+
top: 0;
115+
left: 0;
116+
height: 2px;
117+
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-hover));
118+
box-shadow: 0 0 8px var(--color-accent-glow-strong);
119+
z-index: 20;
120+
transition: width 0.1s linear;
121+
pointer-events: none;
122+
}
123+
124+
.searchButton {
125+
display: flex;
126+
align-items: center;
127+
gap: 0.5rem;
128+
background: rgba(255, 255, 255, 0.05);
129+
border: 1px solid var(--glass-border);
130+
border-radius: 8px;
131+
color: var(--color-text-secondary);
132+
cursor: pointer;
133+
padding: 0.35rem 0.75rem;
134+
font-family: var(--font-sans);
135+
font-size: 0.8rem;
136+
transition: all 0.2s ease;
137+
white-space: nowrap;
138+
}
139+
140+
.searchButton:hover {
141+
background: var(--color-bg-surface-hover);
142+
color: var(--color-text-primary);
143+
border-color: var(--glass-border-accent);
144+
}
145+
146+
.searchLabel {
147+
display: none;
148+
}
149+
150+
.searchKbd {
151+
display: none;
152+
background: rgba(255, 255, 255, 0.08);
153+
border: 1px solid var(--glass-border);
154+
border-radius: 4px;
155+
padding: 0.1rem 0.35rem;
156+
font-family: var(--font-mono);
157+
font-size: 0.75rem;
158+
color: var(--color-text-secondary);
159+
line-height: 1.4;
160+
}
161+
162+
@media (min-width: 769px) {
163+
.searchLabel {
164+
display: inline;
165+
}
166+
167+
.searchKbd {
168+
display: inline;
169+
}
170+
}
171+
112172
@media (max-width: 768px) {
113173
.menuButton {
114174
display: block;

src/components/Lightbox.jsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React, { useEffect } from 'react';
2+
import { X } from 'lucide-react';
3+
import styles from './Lightbox.module.css';
4+
5+
const Lightbox = ({ src, alt, onClose }) => {
6+
useEffect(() => {
7+
document.body.style.overflow = 'hidden';
8+
const handler = (e) => { if (e.key === 'Escape') onClose(); };
9+
window.addEventListener('keydown', handler);
10+
return () => {
11+
document.body.style.overflow = '';
12+
window.removeEventListener('keydown', handler);
13+
};
14+
}, [onClose]);
15+
16+
return (
17+
<div className={styles.overlay} onClick={onClose}>
18+
<button className={styles.close} onClick={onClose} aria-label="Schließen">
19+
<X size={20} />
20+
</button>
21+
<img
22+
src={src}
23+
alt={alt}
24+
className={styles.image}
25+
onClick={e => e.stopPropagation()}
26+
/>
27+
</div>
28+
);
29+
};
30+
31+
export default Lightbox;

0 commit comments

Comments
 (0)