feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
31a027c93a
225 files changed
+38042
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import { useState } from 'react'
|
||||
import type { FolderItem, User } from '../api/client'
|
||||
import { useNavHistory, type View } from '../hooks/useNavHistory'
|
||||
import Sidebar from './Sidebar'
|
||||
import FileBrowser from '../pages/FileBrowser'
|
||||
import TrashPage from '../pages/TrashPage'
|
||||
import SharedPage from '../pages/SharedPage'
|
||||
import SharedWithMePage from '../pages/SharedWithMePage'
|
||||
import AdminPage from '../pages/AdminPage'
|
||||
import ProfilePage from '../pages/ProfilePage'
|
||||
import TagsPage from '../pages/TagsPage'
|
||||
import { UserMenuProvider } from './UserMenu'
|
||||
|
||||
export type { View }
|
||||
|
||||
interface Props {
|
||||
user: User
|
||||
onLogout: () => Promise<void>
|
||||
/** Lädt den Nutzer neu – für Speicherverbrauch und Anzeigename. */
|
||||
onUserChanged: () => Promise<void>
|
||||
}
|
||||
|
||||
export default function Shell({ user, onLogout, onUserChanged }: Props) {
|
||||
const nav = useNavHistory()
|
||||
const { view, folderPath } = nav
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
// Aus der Seitenleiste kennen wir nur Kennung und Name – die Kette darueber
|
||||
// holt die Liste nicht mit, also faengt der Pfad hier beim Ordner selbst an.
|
||||
function navigateToFolder(folderId: string, folderName: string) {
|
||||
setSidebarOpen(false)
|
||||
const stub: FolderItem = {
|
||||
id: folderId, owner_id: null, group_id: null, parent_id: null,
|
||||
name: folderName, created_at: '',
|
||||
}
|
||||
nav.setFolderPath([stub])
|
||||
}
|
||||
|
||||
return (
|
||||
<UserMenuProvider value={{
|
||||
user,
|
||||
onNavigate: (v) => { nav.setView(v); setSidebarOpen(false) },
|
||||
onLogout,
|
||||
onOpenSidebar: () => setSidebarOpen(true),
|
||||
}}>
|
||||
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
|
||||
{/* Mobile overlay */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 40 }}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Sidebar
|
||||
user={user}
|
||||
view={view}
|
||||
activeTagId={nav.tagId}
|
||||
onOpenTag={nav.setTag}
|
||||
activeCorrespondentId={nav.correspondentId}
|
||||
onOpenCorrespondent={nav.setCorrespondent}
|
||||
onNavigate={(v) => { nav.setView(v); setSidebarOpen(false) }}
|
||||
onNavigateToFolder={navigateToFolder}
|
||||
open={sidebarOpen}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
||||
{view === 'files' && (
|
||||
<FileBrowser
|
||||
user={user}
|
||||
folderPath={folderPath}
|
||||
onNavigate={nav.setFolderPath}
|
||||
onUserChanged={onUserChanged}
|
||||
/>
|
||||
)}
|
||||
{view === 'trash' && <TrashPage onUserChanged={onUserChanged} />}
|
||||
{view === 'incoming' && <SharedWithMePage onOpenFolder={navigateToFolder} />}
|
||||
{view === 'shared' && <SharedPage />}
|
||||
{view === 'admin' && <AdminPage />}
|
||||
{view === 'profile' && <ProfilePage user={user} onUserChanged={onUserChanged} />}
|
||||
{/* Etikett und Absender teilen sich die Seite – es ist dieselbe Frage
|
||||
(„alles mit diesem Merkmal"), nur ein anderes Merkmal. */}
|
||||
{(view === 'tags' || view === 'from') && (
|
||||
<TagsPage
|
||||
tagId={view === 'tags' ? nav.tagId : undefined}
|
||||
correspondentId={view === 'from' ? nav.correspondentId : undefined}
|
||||
onOpenFolder={(id, name) => {
|
||||
// Klick auf einen Treffer springt zum Ordner – dieselbe Regel wie
|
||||
// in der Suche: Geöffnet wird per Doppelklick auf die Datei.
|
||||
if (id) navigateToFolder(id, name)
|
||||
else nav.setFolderPath([])
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</UserMenuProvider>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user