-
+
+ {/* Fehlermeldung */}
+ {error && (
+
+
+ {error}
+
+ )}
- {/* Fehlermeldung */}
- {error && (
-
-
- {error}
-
- )}
-
-
-
- { setUrl(e.target.value); setError(null) }}
- placeholder="https://noz.example.com"
- disabled={loading}
- required
- className="w-full bg-surface border border-border rounded-lg px-3 py-2 text-sm text-foreground placeholder:text-muted-fg/30 focus:outline-none focus:border-accent transition-colors font-mono disabled:opacity-50"
- />
-
+ {/* Server URL field — always shown */}
+
+
+
+ { setUrl(e.target.value); setError(null) }}
+ placeholder="https://noz.example.com"
+ disabled={loading}
+ required
+ className="w-full bg-surface border border-border rounded-lg px-3 py-2 text-sm text-foreground placeholder:text-muted-fg/30 focus:outline-none focus:border-accent transition-colors font-mono disabled:opacity-50"
+ />
+
+
+ {/* Token login */}
+ {showToken && (
+
+ )}
- {/* Divider */}
+ {/* Divider — only shown when both methods are available */}
+ {showToken && showAuthentik && (
+ )}
- {/* Authentik — noch nicht implementiert */}
+ {/* Authentik SSO */}
+ {showAuthentik && (
-
-
+ )}
+
Token aus{" "}
diff --git a/src/components/preview/preview.tsx b/src/components/preview/preview.tsx
index b8884ad..46b511a 100644
--- a/src/components/preview/preview.tsx
+++ b/src/components/preview/preview.tsx
@@ -196,7 +196,7 @@ export default function Preview({ content, className, notes, onSelectNote }: Pro
// Toast for unresolvable wikilinks
const [toast, setToast] = useState(null)
- const toastTimer = useRef>()
+ const toastTimer = useRef | undefined>(undefined)
const showToast = (msg: string) => {
setToast(msg)
clearTimeout(toastTimer.current)
diff --git a/src/hooks/use-notes.ts b/src/hooks/use-notes.ts
index b81298a..6b0382e 100644
--- a/src/hooks/use-notes.ts
+++ b/src/hooks/use-notes.ts
@@ -135,10 +135,7 @@ export function useNotes(): UseNotesReturn {
if (e instanceof NozApiError && e.status === 404) {
// Note was deleted externally — remove from local state, then refresh list
cacheRef.current.delete(slug)
- setTopics(prev => prev
- .map(t => ({ ...t, notes: t.notes.filter(n => n.slug !== slug) }))
- .filter(t => t.notes.length > 0 || t.subTopics?.length)
- )
+ setTopics(prev => filterTopicTree(prev, n => n.slug !== slug))
setDrafts(prev => prev.filter(n => n.slug !== slug))
setActiveNote(null)
// Reload full list so sidebar reflects server state
@@ -228,10 +225,7 @@ export function useNotes(): UseNotesReturn {
const deleteNote = useCallback(async (slug: string) => {
if (!DEV) await deleteNoteApi(slug)
cacheRef.current.delete(slug)
- setTopics(prev => prev
- .map(t => ({ ...t, notes: t.notes.filter(n => n.slug !== slug) }))
- .filter(t => t.notes.length > 0)
- )
+ setTopics(prev => filterTopicTree(prev, n => n.slug !== slug))
setDrafts(prev => prev.filter(n => n.slug !== slug))
setActiveNote(prev => {
if (prev?.slug !== slug) return prev
@@ -256,10 +250,7 @@ export function useNotes(): UseNotesReturn {
if (DEV) {
const cached = cacheRef.current.get(slug)
if (cached) cacheRef.current.set(slug, { ...cached, title })
- setTopics(prev => prev.map(t => ({
- ...t,
- notes: t.notes.map(n => n.slug === slug ? { ...n, title } : n),
- })))
+ setTopics(prev => mapTopicNotes(prev, n => n.slug === slug ? { ...n, title } : n))
setDrafts(prev => prev.map(n => n.slug === slug ? { ...n, title } : n))
setActiveNote(prev => prev?.slug === slug ? { ...prev, title } : prev)
return
@@ -267,10 +258,7 @@ export function useNotes(): UseNotesReturn {
await patchNoteApi(slug, { title })
const cached = cacheRef.current.get(slug)
if (cached) cacheRef.current.set(slug, { ...cached, title })
- setTopics(prev => prev.map(t => ({
- ...t,
- notes: t.notes.map(n => n.slug === slug ? { ...n, title } : n),
- })))
+ setTopics(prev => mapTopicNotes(prev, n => n.slug === slug ? { ...n, title } : n))
setDrafts(prev => prev.map(n => n.slug === slug ? { ...n, title } : n))
setActiveNote(prev => prev?.slug === slug ? { ...prev, title } : prev)
}, [])
@@ -279,7 +267,7 @@ export function useNotes(): UseNotesReturn {
const toggleDraft = useCallback(async (slug: string) => {
const meta =
- topicsRef.current.flatMap(t => t.notes).find(n => n.slug === slug) ??
+ flattenNotes(topicsRef.current).find(n => n.slug === slug) ??
draftsRef.current.find(n => n.slug === slug)
if (!meta || meta.draftStage === "draft") return // can't toggle _drafts/ notes here
@@ -291,7 +279,7 @@ export function useNotes(): UseNotesReturn {
const cached = cacheRef.current.get(slug)
if (cached) cacheRef.current.set(slug, { ...cached, draft: nextDraft, draftStage: nextStage })
- setTopics(prev => prev.map(t => ({ ...t, notes: t.notes.map(update) })))
+ setTopics(prev => mapTopicNotes(prev, update))
setActiveNote(prev => prev?.slug === slug ? { ...prev, draft: nextDraft, draftStage: nextStage } : prev)
}, [])
@@ -338,10 +326,7 @@ export function useNotes(): UseNotesReturn {
const cached = cacheRef.current.get(slug)
if (cached) cacheRef.current.set(slug, { ...cached, ...updates })
setActiveNote(prev => prev?.slug === slug ? { ...prev, ...updates } : prev)
- setTopics(prev => prev.map(t => ({
- ...t,
- notes: t.notes.map(n => n.slug === slug ? { ...n, ...updates } : n),
- })))
+ setTopics(prev => mapTopicNotes(prev, n => n.slug === slug ? { ...n, ...updates } : n))
}, [])
/* ── loadNotes ──────────────────────────────────────────────────── */
@@ -359,8 +344,7 @@ export function useNotes(): UseNotesReturn {
topicsRef.current = newTopics
draftsRef.current = newDrafts
- const firstSlug = newTopics[0]?.notes[0]?.slug
- ?? newTopics[0]?.subTopics?.[0]?.notes[0]?.slug
+ const firstSlug = flattenNotes(newTopics)[0]?.slug ?? newDrafts[0]?.slug
if (firstSlug) selectNote(firstSlug)
})
.catch((e: unknown) => {
@@ -395,6 +379,26 @@ function findNoteMeta(topics: Topic[], slug: string): NoteMeta | undefined {
return flattenNotes(topics).find(n => n.slug === slug)
}
+/** Map a function over every note in the tree, recursing into subTopics. */
+function mapTopicNotes(topics: Topic[], fn: (n: NoteMeta) => NoteMeta): Topic[] {
+ return topics.map(t => ({
+ ...t,
+ notes: t.notes.map(fn),
+ subTopics: t.subTopics ? mapTopicNotes(t.subTopics, fn) : undefined,
+ }))
+}
+
+/** Remove notes matching predicate, dropping empty topics (preserves topics that still have subTopics). */
+function filterTopicTree(topics: Topic[], keep: (n: NoteMeta) => boolean): Topic[] {
+ return topics
+ .map(t => ({
+ ...t,
+ notes: t.notes.filter(keep),
+ subTopics: t.subTopics ? filterTopicTree(t.subTopics, keep) : undefined,
+ }))
+ .filter(t => t.notes.length > 0 || (t.subTopics?.length ?? 0) > 0)
+}
+
/* ── buildTopicTree ─────────────────────────────────────────────────────────
Baut den Ordner-Baum aus Slug-Pfaden auf, nicht aus dem topic-Frontmatter-Feld.
Beispiel:
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 9e16604..be7846a 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -135,13 +135,6 @@ export async function patchNote(
});
}
-export async function moveNote(slug: string, newTopic: string): Promise {
- await request(`/api/cli/notes/${slug}/move`, {
- method: "POST",
- body: JSON.stringify({ newTopic }),
- });
-}
-
export async function createNote(
title: string,
topic?: string,
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index dce1a2f..427b5e4 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -32,3 +32,23 @@ export function isAuthenticated(): boolean {
if (process.env.NEXT_PUBLIC_DEV_FIXTURES === "true") return true;
return !!(getToken() && getServerUrl());
}
+
+/* ── Authentik redirect helpers (localStorage — persists across navigations) ── */
+
+const KEY_PENDING_URL = "noz_pending_url";
+
+function ls(): Storage | null {
+ return typeof window !== "undefined" ? localStorage : null;
+}
+
+export function setPendingServerUrl(url: string): void {
+ ls()?.setItem(KEY_PENDING_URL, url);
+}
+
+export function getPendingServerUrl(): string | null {
+ return ls()?.getItem(KEY_PENDING_URL) ?? null;
+}
+
+export function clearPendingServerUrl(): void {
+ ls()?.removeItem(KEY_PENDING_URL);
+}
diff --git a/src/lib/frontmatter.ts b/src/lib/frontmatter.ts
index 7e5413b..ae85f8b 100644
--- a/src/lib/frontmatter.ts
+++ b/src/lib/frontmatter.ts
@@ -1,3 +1,4 @@
+import matter from "gray-matter"
import type { NoteContent, NoteStatus, Lang } from "./types"
export interface FrontmatterFields {
@@ -7,6 +8,7 @@ export interface FrontmatterFields {
status: NoteStatus
lang: Lang
draft?: boolean
+ private?: boolean
order?: number
}
@@ -15,32 +17,20 @@ const DEFAULT: FrontmatterFields = {
}
export function splitContent(markdown: string): { fields: FrontmatterFields; body: string } {
- if (!markdown.startsWith("---\n")) return { fields: { ...DEFAULT }, body: markdown }
-
- const end = markdown.indexOf("\n---\n", 4)
- if (end === -1) return { fields: { ...DEFAULT }, body: markdown }
-
- const yamlStr = markdown.slice(4, end)
- const body = markdown.slice(end + 5)
- const fields = { ...DEFAULT }
-
- for (const line of yamlStr.split("\n")) {
- const m = line.match(/^(\w+):\s*(.*)$/)
- if (!m) continue
- const [, key, rawVal] = m
- const val = rawVal.replace(/^["']|["']$/g, "").trim()
- switch (key) {
- case "title": fields.title = val; break
- case "description": fields.description = val; break
- case "topic": fields.topic = val; break
- case "status": fields.status = val as NoteStatus; break
- case "lang": fields.lang = val as Lang; break
- case "draft": fields.draft = rawVal.trim() === "true"; break
- case "order": { const n = parseInt(rawVal.trim()); if (!isNaN(n)) fields.order = n } break
- }
+ const { data, content } = matter(markdown)
+ return {
+ fields: {
+ title: typeof data.title === "string" ? data.title : DEFAULT.title,
+ description: typeof data.description === "string" ? data.description : DEFAULT.description,
+ topic: typeof data.topic === "string" ? data.topic : DEFAULT.topic,
+ status: typeof data.status === "string" ? data.status as NoteStatus : DEFAULT.status,
+ lang: typeof data.lang === "string" ? data.lang as Lang : DEFAULT.lang,
+ draft: typeof data.draft === "boolean" ? data.draft : undefined,
+ private: typeof data.private === "boolean" ? data.private : undefined,
+ order: typeof data.order === "number" ? data.order : undefined,
+ },
+ body: content,
}
-
- return { fields, body }
}
export function buildMarkdown(note: NoteContent | FrontmatterFields, body: string): string {
@@ -53,8 +43,9 @@ export function buildMarkdown(note: NoteContent | FrontmatterFields, body: strin
`status: ${f.status}`,
`lang: ${f.lang}`,
]
- if (f.draft) lines.push("draft: true")
- if (f.order) lines.push(`order: ${f.order}`)
+ if (f.draft) lines.push("draft: true")
+ if (f.private) lines.push("private: true")
+ if (f.order) lines.push(`order: ${f.order}`)
lines.push("---", "")
return lines.join("\n") + body
}
@@ -67,6 +58,7 @@ function noteToFields(note: NoteContent): FrontmatterFields {
status: note.status,
lang: note.lang,
draft: note.draft,
+ private: note.private,
order: note.order,
}
}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 504f422..e79d213 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -10,6 +10,7 @@ export interface NoteMeta {
status: NoteStatus
lang: Lang
draft?: boolean
+ private?: boolean
draftStage?: DraftStage
order?: number
}