Fixes
This commit is contained in:
804
mobile/app/more/wiki.tsx
Normal file
804
mobile/app/more/wiki.tsx
Normal file
@@ -0,0 +1,804 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
|
||||
import {
|
||||
createWikiPage,
|
||||
deleteWikiPage,
|
||||
fetchWikiPageById,
|
||||
fetchWikiTree,
|
||||
updateWikiPage,
|
||||
WikiTreeItem,
|
||||
} from '@/src/lib/api';
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
|
||||
const PRIMARY = '#69c350';
|
||||
|
||||
type FlatNode = WikiTreeItem & { depth: number };
|
||||
|
||||
type TiptapDoc = {
|
||||
type: string;
|
||||
content?: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
type EditorMessage = {
|
||||
type: 'ready' | 'content' | 'error';
|
||||
content?: unknown;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const WebViewComponent: any = (() => {
|
||||
try {
|
||||
// `react-native-webview` is optional until dependency install is complete on the user's machine.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require('react-native-webview').WebView;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
function createEmptyDoc(): TiptapDoc {
|
||||
return {
|
||||
type: 'doc',
|
||||
content: [{ type: 'paragraph', content: [] }],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDoc(value: unknown): TiptapDoc {
|
||||
if (value && typeof value === 'object' && (value as { type?: unknown }).type === 'doc') {
|
||||
return value as TiptapDoc;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return {
|
||||
type: 'doc',
|
||||
content: [{ type: 'paragraph', content: [{ type: 'text', text: value }] }],
|
||||
};
|
||||
}
|
||||
|
||||
return createEmptyDoc();
|
||||
}
|
||||
|
||||
function buildTiptapHtml(initialDoc: TiptapDoc): string {
|
||||
const initialDocJson = JSON.stringify(initialDoc).replace(/</g, '\\u003c');
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; background: #ffffff; color: #111827; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
||||
#app { display: flex; flex-direction: column; height: 100vh; }
|
||||
#toolbar { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px; border-bottom: 1px solid #e5e7eb; background: #f9fafb; }
|
||||
button { border: 1px solid #d1d5db; background: #fff; color: #374151; border-radius: 8px; padding: 6px 10px; font-size: 12px; }
|
||||
button:active { transform: translateY(1px); }
|
||||
#editor { flex: 1; overflow: auto; padding: 12px; font-size: 16px; line-height: 1.5; }
|
||||
.ProseMirror { min-height: 100%; outline: none; }
|
||||
.ProseMirror p { margin: 0 0 10px; }
|
||||
.ProseMirror h1 { font-size: 1.5rem; margin: 0.2em 0 0.5em; }
|
||||
.ProseMirror h2 { font-size: 1.25rem; margin: 0.2em 0 0.5em; }
|
||||
.ProseMirror ul, .ProseMirror ol { padding-left: 1.2rem; }
|
||||
.ProseMirror a { color: #2563eb; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="toolbar">
|
||||
<button id="bold" type="button"><b>B</b></button>
|
||||
<button id="italic" type="button"><i>I</i></button>
|
||||
<button id="h1" type="button">H1</button>
|
||||
<button id="h2" type="button">H2</button>
|
||||
<button id="bullet" type="button">Liste</button>
|
||||
<button id="ordered" type="button">Nummeriert</button>
|
||||
<button id="undo" type="button">↶</button>
|
||||
<button id="redo" type="button">↷</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { Editor } from 'https://esm.sh/@tiptap/core@2.10.0';
|
||||
import StarterKit from 'https://esm.sh/@tiptap/starter-kit@2.10.0';
|
||||
import Link from 'https://esm.sh/@tiptap/extension-link@2.10.0';
|
||||
|
||||
const initialContent = ${initialDocJson};
|
||||
|
||||
const send = (payload) => {
|
||||
if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify(payload));
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const editor = new Editor({
|
||||
element: document.querySelector('#editor'),
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Link.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),
|
||||
],
|
||||
content: initialContent,
|
||||
autofocus: false,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
spellcheck: 'true',
|
||||
},
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
send({ type: 'content', content: editor.getJSON() });
|
||||
},
|
||||
});
|
||||
|
||||
const click = (id, handler) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.addEventListener('click', () => {
|
||||
handler();
|
||||
editor.commands.focus();
|
||||
});
|
||||
};
|
||||
|
||||
click('bold', () => editor.chain().focus().toggleBold().run());
|
||||
click('italic', () => editor.chain().focus().toggleItalic().run());
|
||||
click('h1', () => editor.chain().focus().toggleHeading({ level: 1 }).run());
|
||||
click('h2', () => editor.chain().focus().toggleHeading({ level: 2 }).run());
|
||||
click('bullet', () => editor.chain().focus().toggleBulletList().run());
|
||||
click('ordered', () => editor.chain().focus().toggleOrderedList().run());
|
||||
click('undo', () => editor.chain().focus().undo().run());
|
||||
click('redo', () => editor.chain().focus().redo().run());
|
||||
|
||||
send({ type: 'ready' });
|
||||
send({ type: 'content', content: editor.getJSON() });
|
||||
} catch (error) {
|
||||
send({ type: 'error', message: String(error?.message || error) });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function isVirtualNode(node: WikiTreeItem): boolean {
|
||||
return Boolean(node.isVirtual) || String(node.id).startsWith('virtual-');
|
||||
}
|
||||
|
||||
export default function WikiScreen() {
|
||||
const { token } = useAuth();
|
||||
const params = useLocalSearchParams<{ entityType?: string; entityId?: string; entityUuid?: string; title?: string }>();
|
||||
|
||||
const entityType = useMemo(() => String(params.entityType || '').trim(), [params.entityType]);
|
||||
const entityId = useMemo(() => {
|
||||
const value = Number(params.entityId);
|
||||
return Number.isFinite(value) && value > 0 ? value : null;
|
||||
}, [params.entityId]);
|
||||
const entityUuid = useMemo(() => String(params.entityUuid || '').trim() || null, [params.entityUuid]);
|
||||
const screenTitle = useMemo(() => String(params.title || '').trim() || 'Wiki', [params.title]);
|
||||
|
||||
const [items, setItems] = useState<WikiTreeItem[]>([]);
|
||||
const [expandedIds, setExpandedIds] = useState<string[]>([]);
|
||||
const [selectedPageId, setSelectedPageId] = useState<string | null>(null);
|
||||
|
||||
const [selectedTitle, setSelectedTitle] = useState('');
|
||||
const [selectedDoc, setSelectedDoc] = useState<TiptapDoc>(createEmptyDoc());
|
||||
const [editorVersion, setEditorVersion] = useState(0);
|
||||
const [editorError, setEditorError] = useState<string | null>(null);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loadingPage, setLoadingPage] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<string | null>(null);
|
||||
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
const [newIsFolder, setNewIsFolder] = useState(false);
|
||||
|
||||
const webViewRef = useRef<any>(null);
|
||||
|
||||
const flatNodes = useMemo(() => {
|
||||
const byParent = new Map<string, WikiTreeItem[]>();
|
||||
|
||||
(items || []).forEach((item) => {
|
||||
const parentId = String(item.parentId || 'root');
|
||||
if (!byParent.has(parentId)) byParent.set(parentId, []);
|
||||
byParent.get(parentId)?.push(item);
|
||||
});
|
||||
|
||||
const sortNodes = (nodes: WikiTreeItem[]) =>
|
||||
nodes.sort((a, b) => {
|
||||
if ((a.isFolder ? 1 : 0) !== (b.isFolder ? 1 : 0)) return (b.isFolder ? 1 : 0) - (a.isFolder ? 1 : 0);
|
||||
const sortA = Number(a.sortOrder || 0);
|
||||
const sortB = Number(b.sortOrder || 0);
|
||||
if (sortA !== sortB) return sortA - sortB;
|
||||
return String(a.title || '').localeCompare(String(b.title || ''), 'de');
|
||||
});
|
||||
|
||||
byParent.forEach((nodes, key) => {
|
||||
byParent.set(key, sortNodes(nodes));
|
||||
});
|
||||
|
||||
const out: FlatNode[] = [];
|
||||
|
||||
const walk = (parentId: string, depth: number) => {
|
||||
const children = byParent.get(parentId) || [];
|
||||
children.forEach((child) => {
|
||||
out.push({ ...child, depth });
|
||||
const isExpandable = Boolean(child.isFolder) || (byParent.get(String(child.id)) || []).length > 0;
|
||||
if (isExpandable && expandedIds.includes(String(child.id))) {
|
||||
walk(String(child.id), depth + 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk('root', 0);
|
||||
return out;
|
||||
}, [expandedIds, items]);
|
||||
|
||||
const selectedNode = useMemo(
|
||||
() => (selectedPageId ? (items || []).find((item) => String(item.id) === String(selectedPageId)) || null : null),
|
||||
[items, selectedPageId]
|
||||
);
|
||||
const selectedNodeIsVirtual = useMemo(() => (selectedNode ? isVirtualNode(selectedNode) : false), [selectedNode]);
|
||||
|
||||
const isEntityScope = Boolean(entityType && (entityId || entityUuid));
|
||||
|
||||
const loadTree = useCallback(
|
||||
async (showSpinner = true) => {
|
||||
if (!token) return;
|
||||
if (showSpinner) setLoading(true);
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
|
||||
try {
|
||||
const rows = await fetchWikiTree(token, {
|
||||
entityType: entityType || undefined,
|
||||
entityId,
|
||||
entityUuid,
|
||||
});
|
||||
setItems(rows || []);
|
||||
|
||||
const autoExpand = new Set<string>();
|
||||
(rows || []).forEach((item) => {
|
||||
if (item.isFolder || isVirtualNode(item)) autoExpand.add(String(item.id));
|
||||
});
|
||||
setExpandedIds(Array.from(autoExpand));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Wiki konnte nicht geladen werden.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[entityId, entityType, entityUuid, token]
|
||||
);
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (pageId: string) => {
|
||||
if (!token) return;
|
||||
setLoadingPage(true);
|
||||
setError(null);
|
||||
setEditorError(null);
|
||||
|
||||
try {
|
||||
const page = await fetchWikiPageById(token, pageId);
|
||||
setSelectedPageId(String(page.id));
|
||||
setSelectedTitle(String(page.title || ''));
|
||||
setSelectedDoc(normalizeDoc(page.content));
|
||||
setEditorVersion((prev) => prev + 1);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Wiki-Seite konnte nicht geladen werden.');
|
||||
} finally {
|
||||
setLoadingPage(false);
|
||||
}
|
||||
},
|
||||
[token]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
void loadTree(true);
|
||||
}, [loadTree, token]);
|
||||
|
||||
async function onRefresh() {
|
||||
setRefreshing(true);
|
||||
await loadTree(false);
|
||||
}
|
||||
|
||||
async function onSavePage() {
|
||||
if (!token || !selectedPageId || saving) return;
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
|
||||
try {
|
||||
await updateWikiPage(token, selectedPageId, {
|
||||
title: selectedTitle.trim() || 'Ohne Titel',
|
||||
content: selectedDoc,
|
||||
});
|
||||
|
||||
setItems((prev) =>
|
||||
prev.map((item) =>
|
||||
String(item.id) === String(selectedPageId) ? { ...item, title: selectedTitle.trim() || 'Ohne Titel' } : item
|
||||
)
|
||||
);
|
||||
setInfo('Wiki-Seite gespeichert.');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Wiki-Seite konnte nicht gespeichert werden.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeletePage() {
|
||||
if (!token || !selectedPageId || deleting) return;
|
||||
setDeleting(true);
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
|
||||
try {
|
||||
await deleteWikiPage(token, selectedPageId);
|
||||
setSelectedPageId(null);
|
||||
setSelectedTitle('');
|
||||
setSelectedDoc(createEmptyDoc());
|
||||
setEditorVersion((prev) => prev + 1);
|
||||
await loadTree(false);
|
||||
setInfo('Wiki-Seite gelöscht.');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Wiki-Seite konnte nicht gelöscht werden.');
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function closeCreateModal() {
|
||||
setCreateModalOpen(false);
|
||||
setNewTitle('');
|
||||
setNewIsFolder(false);
|
||||
}
|
||||
|
||||
async function onCreatePage() {
|
||||
if (!token || creating) return;
|
||||
const title = newTitle.trim();
|
||||
if (!title) {
|
||||
setError('Bitte einen Titel eingeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
setCreating(true);
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
|
||||
try {
|
||||
const page = await createWikiPage(token, {
|
||||
title,
|
||||
parentId: null,
|
||||
isFolder: newIsFolder,
|
||||
entityType: entityType || undefined,
|
||||
entityId,
|
||||
entityUuid,
|
||||
});
|
||||
closeCreateModal();
|
||||
await loadTree(false);
|
||||
|
||||
if (!newIsFolder && page?.id) {
|
||||
await loadPage(String(page.id));
|
||||
}
|
||||
setInfo(newIsFolder ? 'Wiki-Ordner erstellt.' : 'Wiki-Seite erstellt.');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Wiki-Seite konnte nicht erstellt werden.');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.container}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
|
||||
<View style={styles.headerCard}>
|
||||
<Text style={styles.headerTitle}>{screenTitle}</Text>
|
||||
<Text style={styles.headerSubtitle}>{isEntityScope ? 'Entity-Wiki' : 'Zentrale Wissensübersicht'}</Text>
|
||||
|
||||
<Pressable style={styles.primaryButton} onPress={() => setCreateModalOpen(true)}>
|
||||
<Text style={styles.primaryButtonText}>Neue Seite</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||||
{info ? <Text style={styles.info}>{info}</Text> : null}
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.sectionTitle}>Seiten</Text>
|
||||
{loading ? (
|
||||
<View style={styles.loadingBox}>
|
||||
<ActivityIndicator />
|
||||
<Text style={styles.loadingText}>Wiki wird geladen...</Text>
|
||||
</View>
|
||||
) : flatNodes.length === 0 ? (
|
||||
<Text style={styles.empty}>Noch keine Wiki-Seiten vorhanden.</Text>
|
||||
) : (
|
||||
<View style={styles.treeWrap}>
|
||||
{flatNodes.map((node) => {
|
||||
const id = String(node.id);
|
||||
const isSelected = selectedPageId === id;
|
||||
const childrenExist = flatNodes.some((item) => String(item.parentId || '') === id);
|
||||
const expandable = Boolean(node.isFolder) || childrenExist || isVirtualNode(node);
|
||||
const expanded = expandedIds.includes(id);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={id}
|
||||
style={[styles.treeRow, isSelected ? styles.treeRowActive : null]}
|
||||
onPress={() => {
|
||||
if (expandable) {
|
||||
setExpandedIds((prev) => (prev.includes(id) ? prev.filter((entry) => entry !== id) : [...prev, id]));
|
||||
}
|
||||
|
||||
if (node.isFolder || isVirtualNode(node)) return;
|
||||
void loadPage(id);
|
||||
}}>
|
||||
<View style={[styles.treeRowInner, { paddingLeft: 10 + node.depth * 16 }]}>
|
||||
<Text style={styles.treeArrow}>{expandable ? (expanded ? '⌄' : '›') : '·'}</Text>
|
||||
<Text style={styles.treeIcon}>{node.isFolder || isVirtualNode(node) ? '📁' : '📄'}</Text>
|
||||
<Text style={styles.treeTitle} numberOfLines={1}>
|
||||
{String(node.title || 'Ohne Titel')}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.sectionTitle}>Inhalt</Text>
|
||||
|
||||
{!selectedPageId ? (
|
||||
<Text style={styles.empty}>Wähle eine Wiki-Seite aus der Liste aus.</Text>
|
||||
) : loadingPage ? (
|
||||
<View style={styles.loadingBox}>
|
||||
<ActivityIndicator />
|
||||
<Text style={styles.loadingText}>Inhalt wird geladen...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<TextInput
|
||||
value={selectedTitle}
|
||||
onChangeText={setSelectedTitle}
|
||||
placeholder="Titel"
|
||||
placeholderTextColor="#9ca3af"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<View style={styles.editorWrap}>
|
||||
{WebViewComponent ? (
|
||||
<WebViewComponent
|
||||
ref={webViewRef}
|
||||
key={`${selectedPageId}-${editorVersion}`}
|
||||
originWhitelist={['*']}
|
||||
source={{ html: buildTiptapHtml(selectedDoc) }}
|
||||
javaScriptEnabled
|
||||
domStorageEnabled
|
||||
onMessage={(event: any) => {
|
||||
try {
|
||||
const payload = JSON.parse(String(event?.nativeEvent?.data || '{}')) as EditorMessage;
|
||||
if (payload.type === 'content' && payload.content) {
|
||||
setSelectedDoc(normalizeDoc(payload.content));
|
||||
} else if (payload.type === 'error') {
|
||||
setEditorError(payload.message || 'Editor-Fehler');
|
||||
}
|
||||
} catch {
|
||||
setEditorError('Editor-Nachricht konnte nicht gelesen werden.');
|
||||
}
|
||||
}}
|
||||
onError={() => setEditorError('TipTap Editor konnte nicht geladen werden.')}
|
||||
setSupportMultipleWindows={false}
|
||||
style={styles.webView}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.missingWebView}>
|
||||
<Text style={styles.error}>`react-native-webview` ist noch nicht installiert.</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{editorError ? <Text style={styles.error}>{editorError}</Text> : null}
|
||||
|
||||
<View style={styles.editorActions}>
|
||||
<Pressable
|
||||
style={[styles.primaryButton, saving ? styles.buttonDisabled : null]}
|
||||
onPress={() => void onSavePage()}
|
||||
disabled={saving}>
|
||||
<Text style={styles.primaryButtonText}>{saving ? 'Speichert...' : 'Speichern'}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={[styles.deleteButton, deleting ? styles.buttonDisabled : null]}
|
||||
onPress={() => void onDeletePage()}
|
||||
disabled={deleting || Boolean(selectedNode?.isFolder) || selectedNodeIsVirtual}>
|
||||
<Text style={styles.deleteButtonText}>{deleting ? 'Löscht...' : 'Löschen'}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<Modal visible={createModalOpen} transparent animationType="fade" onRequestClose={closeCreateModal}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.modalKeyboardWrap}>
|
||||
<View style={styles.modalCard}>
|
||||
<Text style={styles.modalTitle}>Neue Wiki-Seite</Text>
|
||||
|
||||
<TextInput
|
||||
value={newTitle}
|
||||
onChangeText={setNewTitle}
|
||||
placeholder="Titel"
|
||||
placeholderTextColor="#9ca3af"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
{!isEntityScope ? (
|
||||
<Pressable
|
||||
style={[styles.filterButton, newIsFolder ? styles.filterButtonActive : null]}
|
||||
onPress={() => setNewIsFolder((prev) => !prev)}>
|
||||
<Text style={[styles.filterButtonText, newIsFolder ? styles.filterButtonTextActive : null]}>
|
||||
Als Ordner erstellen
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
<View style={styles.modalActions}>
|
||||
<Pressable style={styles.secondaryButton} onPress={closeCreateModal}>
|
||||
<Text style={styles.secondaryButtonText}>Abbrechen</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.primaryButton, creating ? styles.buttonDisabled : null]}
|
||||
onPress={() => void onCreatePage()}
|
||||
disabled={creating}>
|
||||
<Text style={styles.primaryButtonText}>{creating ? 'Erstelle...' : 'Erstellen'}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
gap: 12,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
headerCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e7eb',
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
},
|
||||
headerTitle: {
|
||||
color: '#111827',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
headerSubtitle: {
|
||||
color: '#6b7280',
|
||||
fontSize: 13,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e7eb',
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: '#111827',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
treeWrap: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e7eb',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
treeRow: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e5e7eb',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
treeRowActive: {
|
||||
backgroundColor: '#f0f9eb',
|
||||
},
|
||||
treeRowInner: {
|
||||
minHeight: 40,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingRight: 10,
|
||||
},
|
||||
treeArrow: {
|
||||
color: '#6b7280',
|
||||
fontSize: 15,
|
||||
width: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
treeIcon: {
|
||||
fontSize: 14,
|
||||
},
|
||||
treeTitle: {
|
||||
flex: 1,
|
||||
color: '#111827',
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 15,
|
||||
color: '#111827',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
editorWrap: {
|
||||
height: 320,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
webView: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
missingWebView: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 12,
|
||||
},
|
||||
editorActions: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
primaryButton: {
|
||||
minHeight: 38,
|
||||
borderRadius: 9,
|
||||
backgroundColor: PRIMARY,
|
||||
paddingHorizontal: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
primaryButtonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '700',
|
||||
fontSize: 13,
|
||||
},
|
||||
secondaryButton: {
|
||||
minHeight: 38,
|
||||
borderRadius: 9,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
backgroundColor: '#ffffff',
|
||||
paddingHorizontal: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
secondaryButtonText: {
|
||||
color: '#374151',
|
||||
fontWeight: '600',
|
||||
fontSize: 13,
|
||||
},
|
||||
deleteButton: {
|
||||
minHeight: 38,
|
||||
borderRadius: 9,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ef4444',
|
||||
backgroundColor: '#ffffff',
|
||||
paddingHorizontal: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
deleteButtonText: {
|
||||
color: '#b91c1c',
|
||||
fontWeight: '700',
|
||||
fontSize: 13,
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
empty: {
|
||||
color: '#6b7280',
|
||||
fontSize: 13,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
loadingBox: {
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
loadingText: {
|
||||
color: '#6b7280',
|
||||
},
|
||||
error: {
|
||||
color: '#dc2626',
|
||||
fontSize: 13,
|
||||
},
|
||||
info: {
|
||||
color: '#166534',
|
||||
fontSize: 13,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
modalKeyboardWrap: {
|
||||
width: '100%',
|
||||
},
|
||||
modalCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
gap: 10,
|
||||
},
|
||||
modalTitle: {
|
||||
color: '#111827',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
modalActions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 8,
|
||||
},
|
||||
filterButton: {
|
||||
alignSelf: 'flex-start',
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
filterButtonActive: {
|
||||
borderColor: PRIMARY,
|
||||
backgroundColor: '#eff9ea',
|
||||
},
|
||||
filterButtonText: {
|
||||
color: '#374151',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
filterButtonTextActive: {
|
||||
color: '#3d7a30',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user