mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-24 02:56:43 +02:00
Merge branch 'dev-1' into dev
This commit is contained in:
commit
d41d060bb3
1770 changed files with 168028 additions and 31031 deletions
276
apps-archived/storage/apps/web/src/routes/+layout.svelte
Normal file
276
apps-archived/storage/apps/web/src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import {
|
||||
isSidebarMode as sidebarModeStore,
|
||||
isNavCollapsed as collapsedStore,
|
||||
} from '$lib/stores/navigation';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
||||
import '../app.css';
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('storage');
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let loading = $state(true);
|
||||
let isSidebarMode = $state(false);
|
||||
let isCollapsed = $state(false);
|
||||
|
||||
// Use theme store's isDark directly
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
||||
// Theme variant dropdown items
|
||||
let themeVariantItems = $derived<PillDropdownItem[]>([
|
||||
...theme.variants.map((variant) => ({
|
||||
id: variant,
|
||||
label: THEME_DEFINITIONS[variant].label,
|
||||
icon: THEME_DEFINITIONS[variant].icon,
|
||||
onClick: () => theme.setVariant(variant),
|
||||
active: theme.variant === variant,
|
||||
})),
|
||||
{
|
||||
id: 'all-themes',
|
||||
label: 'Alle Themes',
|
||||
icon: 'palette',
|
||||
onClick: () => goto('/themes'),
|
||||
active: false,
|
||||
},
|
||||
]);
|
||||
|
||||
// Current theme variant label
|
||||
let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label);
|
||||
|
||||
// Language selector items
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as any);
|
||||
}
|
||||
let languageItems = $derived(
|
||||
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
|
||||
);
|
||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
|
||||
// User email for user dropdown
|
||||
let userEmail = $derived(authStore.user?.email || 'Menü');
|
||||
|
||||
// Navigation items for Storage
|
||||
const navItems: PillNavItem[] = [
|
||||
{ href: '/files', label: 'Dateien', icon: 'folder' },
|
||||
{ href: '/shared', label: 'Geteilt', icon: 'share' },
|
||||
{ href: '/favorites', label: 'Favoriten', icon: 'heart' },
|
||||
{ href: '/trash', label: 'Papierkorb', icon: 'trash' },
|
||||
{ href: '/search', label: 'Suche', icon: 'search' },
|
||||
{ href: '/feedback', label: 'Feedback', icon: 'chat' },
|
||||
];
|
||||
|
||||
// Navigation shortcuts
|
||||
const navRoutes = navItems.map((item) => item.href);
|
||||
|
||||
// Check if current path is auth page
|
||||
let isAuthPage = $derived(
|
||||
$page.url.pathname === '/login' ||
|
||||
$page.url.pathname === '/register' ||
|
||||
$page.url.pathname === '/forgot-password'
|
||||
);
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Cmd/Ctrl+K to open search
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
goto('/search');
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||
const num = parseInt(event.key);
|
||||
if (num >= 1 && num <= navRoutes.length) {
|
||||
event.preventDefault();
|
||||
const route = navRoutes[num - 1];
|
||||
if (route) {
|
||||
goto(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModeChange(isSidebar: boolean) {
|
||||
isSidebarMode = isSidebar;
|
||||
sidebarModeStore.set(isSidebar);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('storage-nav-sidebar', String(isSidebar));
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
isCollapsed = collapsed;
|
||||
collapsedStore.set(collapsed);
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('storage-nav-collapsed', String(collapsed));
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleTheme() {
|
||||
theme.toggleMode();
|
||||
}
|
||||
|
||||
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
|
||||
theme.setMode(mode);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize theme
|
||||
theme.initialize();
|
||||
|
||||
// Initialize auth
|
||||
await authStore.initialize();
|
||||
|
||||
// Load user settings
|
||||
await userSettings.load();
|
||||
|
||||
// Initialize sidebar mode from localStorage
|
||||
const savedSidebar = localStorage.getItem('storage-nav-sidebar');
|
||||
if (savedSidebar === 'true') {
|
||||
isSidebarMode = true;
|
||||
sidebarModeStore.set(true);
|
||||
}
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
const savedCollapsed = localStorage.getItem('storage-nav-collapsed');
|
||||
if (savedCollapsed === 'true') {
|
||||
isCollapsed = true;
|
||||
collapsedStore.set(true);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<ToastContainer />
|
||||
|
||||
{#if loading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-background">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="mb-4 inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"
|
||||
></div>
|
||||
<p class="text-muted-foreground">Laden...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if isAuthPage}
|
||||
<!-- Auth pages without navigation -->
|
||||
{@render children()}
|
||||
{:else}
|
||||
<!-- Navigation Layout -->
|
||||
<div class="layout-container">
|
||||
<PillNavigation
|
||||
items={navItems}
|
||||
currentPath={$page.url.pathname}
|
||||
appName="Storage"
|
||||
homeRoute="/files"
|
||||
onToggleTheme={handleToggleTheme}
|
||||
{isDark}
|
||||
{isSidebarMode}
|
||||
onModeChange={handleModeChange}
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
desktopPosition={userSettings.nav.desktopPosition}
|
||||
showThemeToggle={true}
|
||||
showThemeVariants={true}
|
||||
{themeVariantItems}
|
||||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
primaryColor="#3b82f6"
|
||||
showAppSwitcher={true}
|
||||
{appItems}
|
||||
{userEmail}
|
||||
settingsHref="/settings"
|
||||
manaHref="/mana"
|
||||
profileHref="/profile"
|
||||
allAppsHref="/apps"
|
||||
/>
|
||||
|
||||
<main
|
||||
class="main-content bg-background"
|
||||
class:sidebar-mode={isSidebarMode && !isCollapsed}
|
||||
class:floating-mode={!isSidebarMode && !isCollapsed}
|
||||
>
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.layout-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
transition: all 300ms ease;
|
||||
}
|
||||
|
||||
.main-content.floating-mode {
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.main-content.sidebar-mode {
|
||||
padding-left: 180px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 80rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.content-wrapper {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.content-wrapper {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
apps-archived/storage/apps/web/src/routes/+page.svelte
Normal file
13
apps-archived/storage/apps/web/src/routes/+page.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// Redirect to /files on root
|
||||
onMount(() => {
|
||||
goto('/files');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<p class="text-muted-foreground">Weiterleitung...</p>
|
||||
</div>
|
||||
237
apps-archived/storage/apps/web/src/routes/favorites/+page.svelte
Normal file
237
apps-archived/storage/apps/web/src/routes/favorites/+page.svelte
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { Heart, Grid, List } from 'lucide-svelte';
|
||||
import { searchApi } from '$lib/api/client';
|
||||
import type { StorageFile, StorageFolder } from '$lib/api/client';
|
||||
import { filesStore } from '$lib/stores/files.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import FileGrid from '$lib/components/files/FileGrid.svelte';
|
||||
import FileList from '$lib/components/files/FileList.svelte';
|
||||
|
||||
let files = $state<StorageFile[]>([]);
|
||||
let folders = $state<StorageFolder[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
filesStore.initViewMode();
|
||||
await loadFavorites();
|
||||
});
|
||||
|
||||
async function loadFavorites() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
const result = await searchApi.favorites();
|
||||
if (result.error) {
|
||||
error = result.error;
|
||||
} else if (result.data) {
|
||||
files = result.data.files;
|
||||
folders = result.data.folders;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function handleFolderClick(folder: StorageFolder) {
|
||||
goto(`/files/${folder.id}`);
|
||||
}
|
||||
|
||||
function handleFileClick(file: StorageFile) {
|
||||
console.log('File clicked:', file);
|
||||
}
|
||||
|
||||
async function handleFileAction(action: string, file: StorageFile) {
|
||||
if (action === 'favorite') {
|
||||
const result = await filesStore.toggleFileFavorite(file.id);
|
||||
if (!result.error) {
|
||||
files = files.filter((f) => f.id !== file.id);
|
||||
toast.success('Favorit entfernt');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFolderAction(action: string, folder: StorageFolder) {
|
||||
if (action === 'favorite') {
|
||||
const result = await filesStore.toggleFolderFavorite(folder.id);
|
||||
if (!result.error) {
|
||||
folders = folders.filter((f) => f.id !== folder.id);
|
||||
toast.success('Favorit entfernt');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Favoriten - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="favorites-page">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<Heart size={24} />
|
||||
Favoriten
|
||||
</h1>
|
||||
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={filesStore.viewMode === 'grid'}
|
||||
onclick={() => filesStore.setViewMode('grid')}
|
||||
aria-label="Rasteransicht"
|
||||
>
|
||||
<Grid size={18} />
|
||||
</button>
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={filesStore.viewMode === 'list'}
|
||||
onclick={() => filesStore.setViewMode('list')}
|
||||
aria-label="Listenansicht"
|
||||
>
|
||||
<List size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="error-state">
|
||||
<p>Fehler: {error}</p>
|
||||
<button onclick={loadFavorites}>Erneut versuchen</button>
|
||||
</div>
|
||||
{:else if files.length === 0 && folders.length === 0}
|
||||
<div class="empty-state">
|
||||
<Heart size={48} />
|
||||
<h2>Keine Favoriten</h2>
|
||||
<p>Markiere Dateien und Ordner als Favoriten, um sie hier schnell zu finden.</p>
|
||||
</div>
|
||||
{:else if filesStore.viewMode === 'grid'}
|
||||
<FileGrid
|
||||
{files}
|
||||
{folders}
|
||||
onFileClick={handleFileClick}
|
||||
onFolderClick={handleFolderClick}
|
||||
onFileAction={handleFileAction}
|
||||
onFolderAction={handleFolderAction}
|
||||
/>
|
||||
{:else}
|
||||
<FileList
|
||||
{files}
|
||||
{folders}
|
||||
onFileClick={handleFileClick}
|
||||
onFolderClick={handleFolderClick}
|
||||
onFileAction={handleFileAction}
|
||||
onFolderAction={handleFolderAction}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.favorites-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state .spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgb(var(--color-border));
|
||||
border-top-color: rgb(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state p,
|
||||
.error-state p {
|
||||
margin-top: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.error-state button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-primary));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
223
apps-archived/storage/apps/web/src/routes/feedback/+page.svelte
Normal file
223
apps-archived/storage/apps/web/src/routes/feedback/+page.svelte
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
<script lang="ts">
|
||||
import { MessageSquare, Send } from 'lucide-svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
|
||||
let type = $state<'bug' | 'feature' | 'other'>('feature');
|
||||
let message = $state('');
|
||||
let sending = $state(false);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!message.trim()) return;
|
||||
|
||||
sending = true;
|
||||
|
||||
// Simulate sending feedback
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
toast.success('Feedback gesendet! Vielen Dank.');
|
||||
message = '';
|
||||
type = 'feature';
|
||||
sending = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Feedback - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="feedback-page">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<MessageSquare size={24} />
|
||||
Feedback
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="feedback-card">
|
||||
<p class="intro">
|
||||
Wir freuen uns über dein Feedback! Teile uns mit, was wir verbessern können oder welche
|
||||
Funktionen du dir wünschst.
|
||||
</p>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div class="form-group">
|
||||
<label>Art des Feedbacks</label>
|
||||
<div class="type-selector">
|
||||
<button
|
||||
type="button"
|
||||
class="type-btn"
|
||||
class:active={type === 'bug'}
|
||||
onclick={() => (type = 'bug')}
|
||||
>
|
||||
Bug melden
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="type-btn"
|
||||
class:active={type === 'feature'}
|
||||
onclick={() => (type = 'feature')}
|
||||
>
|
||||
Feature-Wunsch
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="type-btn"
|
||||
class:active={type === 'other'}
|
||||
onclick={() => (type = 'other')}
|
||||
>
|
||||
Sonstiges
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message">Deine Nachricht</label>
|
||||
<textarea
|
||||
id="message"
|
||||
bind:value={message}
|
||||
placeholder="Beschreibe dein Feedback hier..."
|
||||
rows="6"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn" disabled={!message.trim() || sending}>
|
||||
<Send size={18} />
|
||||
{sending ? 'Wird gesendet...' : 'Feedback senden'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feedback-page {
|
||||
min-height: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.feedback-card {
|
||||
padding: 2rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.type-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
flex: 1;
|
||||
padding: 0.625rem;
|
||||
background: rgb(var(--color-surface));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.type-btn.active {
|
||||
background: rgba(var(--color-primary), 0.1);
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: rgb(var(--color-surface));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: rgb(var(--color-primary));
|
||||
box-shadow: 0 0 0 3px rgba(var(--color-primary), 0.1);
|
||||
}
|
||||
|
||||
textarea::placeholder {
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: rgb(var(--color-primary));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.type-selector {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
417
apps-archived/storage/apps/web/src/routes/files/+page.svelte
Normal file
417
apps-archived/storage/apps/web/src/routes/files/+page.svelte
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { Grid, List, Plus, FolderPlus, Upload } from 'lucide-svelte';
|
||||
import { filesStore } from '$lib/stores/files.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import type { StorageFile, StorageFolder } from '$lib/api/client';
|
||||
import FileGrid from '$lib/components/files/FileGrid.svelte';
|
||||
import FileList from '$lib/components/files/FileList.svelte';
|
||||
import Breadcrumb from '$lib/components/files/Breadcrumb.svelte';
|
||||
import UploadZone from '$lib/components/files/UploadZone.svelte';
|
||||
import NewFolderModal from '$lib/components/files/NewFolderModal.svelte';
|
||||
|
||||
let showUploadZone = $state(false);
|
||||
let showNewFolderModal = $state(false);
|
||||
let uploading = $state(false);
|
||||
let uploadProgress = $state(0);
|
||||
|
||||
// Breadcrumb items from current folder path
|
||||
let breadcrumbItems = $derived(
|
||||
filesStore.currentFolder
|
||||
? [{ id: filesStore.currentFolder.id, name: filesStore.currentFolder.name }]
|
||||
: []
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
filesStore.initViewMode();
|
||||
filesStore.loadFolder();
|
||||
});
|
||||
|
||||
function handleFolderClick(folder: StorageFolder) {
|
||||
goto(`/files/${folder.id}`);
|
||||
}
|
||||
|
||||
function handleFileClick(file: StorageFile) {
|
||||
// TODO: Open file preview
|
||||
console.log('File clicked:', file);
|
||||
}
|
||||
|
||||
async function handleFileAction(action: string, file: StorageFile) {
|
||||
switch (action) {
|
||||
case 'download':
|
||||
await filesStore.downloadFile(file.id, file.name);
|
||||
toast.success('Download gestartet');
|
||||
break;
|
||||
case 'rename':
|
||||
const newName = prompt('Neuer Name:', file.name);
|
||||
if (newName && newName !== file.name) {
|
||||
const result = await filesStore.renameFile(file.id, newName);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
toast.success('Datei umbenannt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'favorite':
|
||||
const favResult = await filesStore.toggleFileFavorite(file.id);
|
||||
if (!favResult.error) {
|
||||
toast.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (confirm('Datei in den Papierkorb verschieben?')) {
|
||||
const delResult = await filesStore.deleteFile(file.id);
|
||||
if (delResult.error) {
|
||||
toast.error(delResult.error);
|
||||
} else {
|
||||
toast.success('In den Papierkorb verschoben');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'share':
|
||||
// TODO: Open share modal
|
||||
toast.info('Teilen-Funktion kommt bald');
|
||||
break;
|
||||
case 'move':
|
||||
// TODO: Open move modal
|
||||
toast.info('Verschieben-Funktion kommt bald');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFolderAction(action: string, folder: StorageFolder) {
|
||||
switch (action) {
|
||||
case 'rename':
|
||||
const newName = prompt('Neuer Name:', folder.name);
|
||||
if (newName && newName !== folder.name) {
|
||||
const result = await filesStore.renameFolder(folder.id, newName);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
toast.success('Ordner umbenannt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'favorite':
|
||||
const favResult = await filesStore.toggleFolderFavorite(folder.id);
|
||||
if (!favResult.error) {
|
||||
toast.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (confirm('Ordner und Inhalt in den Papierkorb verschieben?')) {
|
||||
const delResult = await filesStore.deleteFolder(folder.id);
|
||||
if (delResult.error) {
|
||||
toast.error(delResult.error);
|
||||
} else {
|
||||
toast.success('In den Papierkorb verschoben');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'share':
|
||||
toast.info('Teilen-Funktion kommt bald');
|
||||
break;
|
||||
case 'move':
|
||||
toast.info('Verschieben-Funktion kommt bald');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpload(files: FileList) {
|
||||
uploading = true;
|
||||
uploadProgress = 0;
|
||||
|
||||
const totalFiles = files.length;
|
||||
let completed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const result = await filesStore.uploadFile(file);
|
||||
if (result.error) {
|
||||
toast.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
|
||||
}
|
||||
completed++;
|
||||
uploadProgress = Math.round((completed / totalFiles) * 100);
|
||||
}
|
||||
|
||||
uploading = false;
|
||||
uploadProgress = 0;
|
||||
showUploadZone = false;
|
||||
toast.success(`${totalFiles} Datei(en) hochgeladen`);
|
||||
}
|
||||
|
||||
async function handleCreateFolder(name: string, color?: string) {
|
||||
const result = await filesStore.createFolder(name, color);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
toast.success('Ordner erstellt');
|
||||
}
|
||||
}
|
||||
|
||||
function handleBreadcrumbNavigate(id: string | null) {
|
||||
if (id) {
|
||||
goto(`/files/${id}`);
|
||||
} else {
|
||||
goto('/files');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Meine Dateien - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="files-page">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h1>Meine Dateien</h1>
|
||||
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={filesStore.viewMode === 'grid'}
|
||||
onclick={() => filesStore.setViewMode('grid')}
|
||||
aria-label="Rasteransicht"
|
||||
>
|
||||
<Grid size={18} />
|
||||
</button>
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={filesStore.viewMode === 'list'}
|
||||
onclick={() => filesStore.setViewMode('list')}
|
||||
aria-label="Listenansicht"
|
||||
>
|
||||
<List size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="action-btn" onclick={() => (showNewFolderModal = true)}>
|
||||
<FolderPlus size={18} />
|
||||
<span>Neuer Ordner</span>
|
||||
</button>
|
||||
|
||||
<button class="action-btn primary" onclick={() => (showUploadZone = !showUploadZone)}>
|
||||
<Upload size={18} />
|
||||
<span>Hochladen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showUploadZone}
|
||||
<UploadZone onUpload={handleUpload} {uploading} progress={uploadProgress} />
|
||||
{/if}
|
||||
|
||||
{#if filesStore.loading}
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
{:else if filesStore.error}
|
||||
<div class="error-state">
|
||||
<p>Fehler: {filesStore.error}</p>
|
||||
<button onclick={() => filesStore.loadFolder()}>Erneut versuchen</button>
|
||||
</div>
|
||||
{:else if filesStore.files.length === 0 && filesStore.folders.length === 0}
|
||||
<div class="empty-state">
|
||||
<Upload size={48} />
|
||||
<h2>Noch keine Dateien</h2>
|
||||
<p>Lade deine ersten Dateien hoch oder erstelle einen Ordner.</p>
|
||||
<div class="empty-actions">
|
||||
<button class="action-btn" onclick={() => (showNewFolderModal = true)}>
|
||||
<FolderPlus size={18} />
|
||||
<span>Neuer Ordner</span>
|
||||
</button>
|
||||
<button class="action-btn primary" onclick={() => (showUploadZone = true)}>
|
||||
<Upload size={18} />
|
||||
<span>Hochladen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if filesStore.viewMode === 'grid'}
|
||||
<FileGrid
|
||||
files={filesStore.files}
|
||||
folders={filesStore.folders}
|
||||
onFileClick={handleFileClick}
|
||||
onFolderClick={handleFolderClick}
|
||||
onFileAction={handleFileAction}
|
||||
onFolderAction={handleFolderAction}
|
||||
/>
|
||||
{:else}
|
||||
<FileList
|
||||
files={filesStore.files}
|
||||
folders={filesStore.folders}
|
||||
onFileClick={handleFileClick}
|
||||
onFolderClick={handleFolderClick}
|
||||
onFileAction={handleFileAction}
|
||||
onFolderAction={handleFolderAction}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<NewFolderModal
|
||||
open={showNewFolderModal}
|
||||
onClose={() => (showNewFolderModal = false)}
|
||||
onCreate={handleCreateFolder}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.files-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgb(var(--color-surface));
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: rgb(var(--color-primary));
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state .spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgb(var(--color-border));
|
||||
border-top-color: rgb(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state p,
|
||||
.error-state p {
|
||||
margin-top: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.error-state button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-primary));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-btn span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,457 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { Grid, List, FolderPlus, Upload, ArrowLeft } from 'lucide-svelte';
|
||||
import { filesStore } from '$lib/stores/files.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import type { StorageFile, StorageFolder } from '$lib/api/client';
|
||||
import FileGrid from '$lib/components/files/FileGrid.svelte';
|
||||
import FileList from '$lib/components/files/FileList.svelte';
|
||||
import Breadcrumb from '$lib/components/files/Breadcrumb.svelte';
|
||||
import UploadZone from '$lib/components/files/UploadZone.svelte';
|
||||
import NewFolderModal from '$lib/components/files/NewFolderModal.svelte';
|
||||
|
||||
let showUploadZone = $state(false);
|
||||
let showNewFolderModal = $state(false);
|
||||
let uploading = $state(false);
|
||||
let uploadProgress = $state(0);
|
||||
|
||||
let folderId = $derived($page.params.folderId);
|
||||
|
||||
// Breadcrumb items from current folder path
|
||||
let breadcrumbItems = $derived(
|
||||
filesStore.currentFolder
|
||||
? [{ id: filesStore.currentFolder.id, name: filesStore.currentFolder.name }]
|
||||
: []
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (folderId) {
|
||||
filesStore.loadFolder(folderId);
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
filesStore.initViewMode();
|
||||
});
|
||||
|
||||
function handleFolderClick(folder: StorageFolder) {
|
||||
goto(`/files/${folder.id}`);
|
||||
}
|
||||
|
||||
function handleFileClick(file: StorageFile) {
|
||||
console.log('File clicked:', file);
|
||||
}
|
||||
|
||||
async function handleFileAction(action: string, file: StorageFile) {
|
||||
switch (action) {
|
||||
case 'download':
|
||||
await filesStore.downloadFile(file.id, file.name);
|
||||
toast.success('Download gestartet');
|
||||
break;
|
||||
case 'rename':
|
||||
const newName = prompt('Neuer Name:', file.name);
|
||||
if (newName && newName !== file.name) {
|
||||
const result = await filesStore.renameFile(file.id, newName);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
toast.success('Datei umbenannt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'favorite':
|
||||
const favResult = await filesStore.toggleFileFavorite(file.id);
|
||||
if (!favResult.error) {
|
||||
toast.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (confirm('Datei in den Papierkorb verschieben?')) {
|
||||
const delResult = await filesStore.deleteFile(file.id);
|
||||
if (delResult.error) {
|
||||
toast.error(delResult.error);
|
||||
} else {
|
||||
toast.success('In den Papierkorb verschoben');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'share':
|
||||
toast.info('Teilen-Funktion kommt bald');
|
||||
break;
|
||||
case 'move':
|
||||
toast.info('Verschieben-Funktion kommt bald');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFolderAction(action: string, folder: StorageFolder) {
|
||||
switch (action) {
|
||||
case 'rename':
|
||||
const newName = prompt('Neuer Name:', folder.name);
|
||||
if (newName && newName !== folder.name) {
|
||||
const result = await filesStore.renameFolder(folder.id, newName);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
toast.success('Ordner umbenannt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'favorite':
|
||||
const favResult = await filesStore.toggleFolderFavorite(folder.id);
|
||||
if (!favResult.error) {
|
||||
toast.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (confirm('Ordner und Inhalt in den Papierkorb verschieben?')) {
|
||||
const delResult = await filesStore.deleteFolder(folder.id);
|
||||
if (delResult.error) {
|
||||
toast.error(delResult.error);
|
||||
} else {
|
||||
toast.success('In den Papierkorb verschoben');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'share':
|
||||
toast.info('Teilen-Funktion kommt bald');
|
||||
break;
|
||||
case 'move':
|
||||
toast.info('Verschieben-Funktion kommt bald');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpload(files: FileList) {
|
||||
uploading = true;
|
||||
uploadProgress = 0;
|
||||
|
||||
const totalFiles = files.length;
|
||||
let completed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const result = await filesStore.uploadFile(file);
|
||||
if (result.error) {
|
||||
toast.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
|
||||
}
|
||||
completed++;
|
||||
uploadProgress = Math.round((completed / totalFiles) * 100);
|
||||
}
|
||||
|
||||
uploading = false;
|
||||
uploadProgress = 0;
|
||||
showUploadZone = false;
|
||||
toast.success(`${totalFiles} Datei(en) hochgeladen`);
|
||||
}
|
||||
|
||||
async function handleCreateFolder(name: string, color?: string) {
|
||||
const result = await filesStore.createFolder(name, color);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
toast.success('Ordner erstellt');
|
||||
}
|
||||
}
|
||||
|
||||
function handleBreadcrumbNavigate(id: string | null) {
|
||||
if (id) {
|
||||
goto(`/files/${id}`);
|
||||
} else {
|
||||
goto('/files');
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
const parentId = filesStore.currentFolder?.parentFolderId;
|
||||
if (parentId) {
|
||||
goto(`/files/${parentId}`);
|
||||
} else {
|
||||
goto('/files');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{filesStore.currentFolder?.name || 'Ordner'} - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="files-page">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<button class="back-btn" onclick={goBack} aria-label="Zurück">
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<div>
|
||||
<h1>{filesStore.currentFolder?.name || 'Ordner'}</h1>
|
||||
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={filesStore.viewMode === 'grid'}
|
||||
onclick={() => filesStore.setViewMode('grid')}
|
||||
aria-label="Rasteransicht"
|
||||
>
|
||||
<Grid size={18} />
|
||||
</button>
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={filesStore.viewMode === 'list'}
|
||||
onclick={() => filesStore.setViewMode('list')}
|
||||
aria-label="Listenansicht"
|
||||
>
|
||||
<List size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="action-btn" onclick={() => (showNewFolderModal = true)}>
|
||||
<FolderPlus size={18} />
|
||||
<span>Neuer Ordner</span>
|
||||
</button>
|
||||
|
||||
<button class="action-btn primary" onclick={() => (showUploadZone = !showUploadZone)}>
|
||||
<Upload size={18} />
|
||||
<span>Hochladen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showUploadZone}
|
||||
<UploadZone onUpload={handleUpload} {uploading} progress={uploadProgress} />
|
||||
{/if}
|
||||
|
||||
{#if filesStore.loading}
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
{:else if filesStore.error}
|
||||
<div class="error-state">
|
||||
<p>Fehler: {filesStore.error}</p>
|
||||
<button onclick={() => filesStore.loadFolder(folderId)}>Erneut versuchen</button>
|
||||
</div>
|
||||
{:else if filesStore.files.length === 0 && filesStore.folders.length === 0}
|
||||
<div class="empty-state">
|
||||
<Upload size={48} />
|
||||
<h2>Leerer Ordner</h2>
|
||||
<p>Dieser Ordner ist leer. Lade Dateien hoch oder erstelle Unterordner.</p>
|
||||
<div class="empty-actions">
|
||||
<button class="action-btn" onclick={() => (showNewFolderModal = true)}>
|
||||
<FolderPlus size={18} />
|
||||
<span>Neuer Ordner</span>
|
||||
</button>
|
||||
<button class="action-btn primary" onclick={() => (showUploadZone = true)}>
|
||||
<Upload size={18} />
|
||||
<span>Hochladen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if filesStore.viewMode === 'grid'}
|
||||
<FileGrid
|
||||
files={filesStore.files}
|
||||
folders={filesStore.folders}
|
||||
onFileClick={handleFileClick}
|
||||
onFolderClick={handleFolderClick}
|
||||
onFileAction={handleFileAction}
|
||||
onFolderAction={handleFolderAction}
|
||||
/>
|
||||
{:else}
|
||||
<FileList
|
||||
files={filesStore.files}
|
||||
folders={filesStore.folders}
|
||||
onFileClick={handleFileClick}
|
||||
onFolderClick={handleFolderClick}
|
||||
onFileAction={handleFileAction}
|
||||
onFolderAction={handleFolderAction}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<NewFolderModal
|
||||
open={showNewFolderModal}
|
||||
onClose={() => (showNewFolderModal = false)}
|
||||
onCreate={handleCreateFolder}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.files-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: 0.5rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: rgb(var(--color-text-primary));
|
||||
background: rgb(var(--color-surface));
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgb(var(--color-surface));
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: rgb(var(--color-primary));
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state .spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgb(var(--color-border));
|
||||
border-top-color: rgb(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state p,
|
||||
.error-state p {
|
||||
margin-top: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.error-state button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-primary));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-btn span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
|
||||
import { ManaIcon } from '@manacore/shared-branding';
|
||||
import { getForgotPasswordTranslations } from '@manacore/shared-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
const translations = $derived(getForgotPasswordTranslations($locale || 'de'));
|
||||
|
||||
async function handleResetPassword(email: string) {
|
||||
return authStore.resetPassword(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{translations.title} - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<ForgotPasswordPage
|
||||
appName="Storage"
|
||||
logo={ManaIcon}
|
||||
primaryColor="#3b82f6"
|
||||
onResetPassword={handleResetPassword}
|
||||
{goto}
|
||||
loginPath="/login"
|
||||
lightBackground="#eff6ff"
|
||||
darkBackground="#0f172a"
|
||||
{translations}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
{/snippet}
|
||||
</ForgotPasswordPage>
|
||||
38
apps-archived/storage/apps/web/src/routes/login/+page.svelte
Normal file
38
apps-archived/storage/apps/web/src/routes/login/+page.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { ManaIcon } from '@manacore/shared-branding';
|
||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
return authStore.signIn(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{translations.title} - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<LoginPage
|
||||
appName="Storage"
|
||||
logo={ManaIcon}
|
||||
primaryColor="#3b82f6"
|
||||
onSignIn={handleSignIn}
|
||||
{goto}
|
||||
successRedirect="/files"
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#eff6ff"
|
||||
darkBackground="#0f172a"
|
||||
{translations}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
{/snippet}
|
||||
</LoginPage>
|
||||
132
apps-archived/storage/apps/web/src/routes/profile/+page.svelte
Normal file
132
apps-archived/storage/apps/web/src/routes/profile/+page.svelte
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<script lang="ts">
|
||||
import { User } from 'lucide-svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Profil - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="profile-page">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<User size={24} />
|
||||
Profil
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="profile-card">
|
||||
<div class="avatar">
|
||||
<User size={48} />
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<h2>{authStore.user?.email || 'Nicht angemeldet'}</h2>
|
||||
<p>Mitglied seit 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-section">
|
||||
<h3>Kontoinformationen</h3>
|
||||
<div class="info-row">
|
||||
<span class="label">E-Mail</span>
|
||||
<span class="value">{authStore.user?.email || '—'}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Benutzer-ID</span>
|
||||
<span class="value">{authStore.user?.id || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.profile-page {
|
||||
min-height: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-xl);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: 50%;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.profile-info h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.profile-info p {
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.profile-section {
|
||||
padding: 1.5rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
|
||||
.profile-section h3 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid rgb(var(--color-border));
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import { ManaIcon } from '@manacore/shared-branding';
|
||||
import { getRegisterTranslations } from '@manacore/shared-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
const translations = $derived(getRegisterTranslations($locale || 'de'));
|
||||
|
||||
async function handleSignUp(email: string, password: string) {
|
||||
return authStore.signUp(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{translations.title} - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<RegisterPage
|
||||
appName="Storage"
|
||||
logo={ManaIcon}
|
||||
primaryColor="#3b82f6"
|
||||
onSignUp={handleSignUp}
|
||||
{goto}
|
||||
successRedirect="/files"
|
||||
loginPath="/login"
|
||||
lightBackground="#eff6ff"
|
||||
darkBackground="#0f172a"
|
||||
{translations}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
{/snippet}
|
||||
</RegisterPage>
|
||||
285
apps-archived/storage/apps/web/src/routes/search/+page.svelte
Normal file
285
apps-archived/storage/apps/web/src/routes/search/+page.svelte
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { Search, Grid, List } from 'lucide-svelte';
|
||||
import { searchApi } from '$lib/api/client';
|
||||
import type { StorageFile, StorageFolder } from '$lib/api/client';
|
||||
import { filesStore } from '$lib/stores/files.svelte';
|
||||
import FileGrid from '$lib/components/files/FileGrid.svelte';
|
||||
import FileList from '$lib/components/files/FileList.svelte';
|
||||
|
||||
let query = $state('');
|
||||
let files = $state<StorageFile[]>([]);
|
||||
let folders = $state<StorageFolder[]>([]);
|
||||
let loading = $state(false);
|
||||
let searched = $state(false);
|
||||
|
||||
// Get initial query from URL
|
||||
let initialQuery = $derived($page.url.searchParams.get('q') || '');
|
||||
|
||||
onMount(() => {
|
||||
filesStore.initViewMode();
|
||||
if (initialQuery) {
|
||||
query = initialQuery;
|
||||
handleSearch();
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
if (!query.trim()) return;
|
||||
|
||||
loading = true;
|
||||
searched = true;
|
||||
|
||||
const result = await searchApi.search(query.trim());
|
||||
if (result.data) {
|
||||
files = result.data.files;
|
||||
folders = result.data.folders;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
|
||||
// Update URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('q', query.trim());
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFolderClick(folder: StorageFolder) {
|
||||
goto(`/files/${folder.id}`);
|
||||
}
|
||||
|
||||
function handleFileClick(file: StorageFile) {
|
||||
console.log('File clicked:', file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Suche - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="search-page">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<Search size={24} />
|
||||
Suche
|
||||
</h1>
|
||||
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={filesStore.viewMode === 'grid'}
|
||||
onclick={() => filesStore.setViewMode('grid')}
|
||||
aria-label="Rasteransicht"
|
||||
>
|
||||
<Grid size={18} />
|
||||
</button>
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={filesStore.viewMode === 'list'}
|
||||
onclick={() => filesStore.setViewMode('list')}
|
||||
aria-label="Listenansicht"
|
||||
>
|
||||
<List size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-bar">
|
||||
<Search size={20} />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={query}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder="Dateien und Ordner durchsuchen..."
|
||||
autofocus
|
||||
/>
|
||||
<button onclick={handleSearch} disabled={!query.trim() || loading}>
|
||||
{loading ? 'Suche...' : 'Suchen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Suche läuft...</p>
|
||||
</div>
|
||||
{:else if searched && files.length === 0 && folders.length === 0}
|
||||
<div class="empty-state">
|
||||
<Search size={48} />
|
||||
<h2>Keine Ergebnisse</h2>
|
||||
<p>Keine Dateien oder Ordner für "{query}" gefunden.</p>
|
||||
</div>
|
||||
{:else if searched}
|
||||
<div class="results-header">
|
||||
<span>{files.length + folders.length} Ergebnis(se) für "{query}"</span>
|
||||
</div>
|
||||
|
||||
{#if filesStore.viewMode === 'grid'}
|
||||
<FileGrid {files} {folders} onFileClick={handleFileClick} onFolderClick={handleFolderClick} />
|
||||
{:else}
|
||||
<FileList {files} {folders} onFileClick={handleFileClick} onFolderClick={handleFolderClick} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<Search size={48} />
|
||||
<h2>Dateien durchsuchen</h2>
|
||||
<p>Gib einen Suchbegriff ein, um Dateien und Ordner zu finden.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.search-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: rgb(var(--color-text-secondary));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-bar :global(svg) {
|
||||
color: rgb(var(--color-text-secondary));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-bar input::placeholder {
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
}
|
||||
|
||||
.search-bar button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-primary));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.search-bar button:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.search-bar button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.results-header {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state .spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgb(var(--color-border));
|
||||
border-top-color: rgb(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state p {
|
||||
margin-top: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
124
apps-archived/storage/apps/web/src/routes/settings/+page.svelte
Normal file
124
apps-archived/storage/apps/web/src/routes/settings/+page.svelte
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import {
|
||||
SettingsPage,
|
||||
SettingsSection,
|
||||
SettingsCard,
|
||||
SettingsRow,
|
||||
GlobalSettingsSection,
|
||||
} from '@manacore/shared-ui';
|
||||
|
||||
onMount(async () => {
|
||||
await userSettings.load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Einstellungen - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<SettingsPage title="Einstellungen" subtitle="Passe die App an deine Vorlieben an.">
|
||||
<!-- Appearance Section -->
|
||||
<SettingsSection title="Darstellung">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
<SettingsCard>
|
||||
<div class="px-5 py-4">
|
||||
<p class="font-medium text-[hsl(var(--foreground))] mb-2">Farbmodus</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">
|
||||
Wähle zwischen Hell, Dunkel oder System
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
{#each ['light', 'dark', 'system'] as mode}
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors {theme.mode === mode
|
||||
? 'bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]'
|
||||
: 'bg-[hsl(var(--muted))] hover:bg-[hsl(var(--muted))]/80 text-[hsl(var(--foreground))]'}"
|
||||
onclick={() => theme.setMode(mode as 'light' | 'dark' | 'system')}
|
||||
>
|
||||
{mode === 'light' ? 'Hell' : mode === 'dark' ? 'Dunkel' : 'System'}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 py-4 border-t border-[hsl(var(--border))]">
|
||||
<p class="font-medium text-[hsl(var(--foreground))] mb-2">Theme</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">Wähle eine Farbpalette</p>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{#each theme.variants as variant}
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors {theme.variant ===
|
||||
variant
|
||||
? 'bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]'
|
||||
: 'bg-[hsl(var(--muted))] hover:bg-[hsl(var(--muted))]/80 text-[hsl(var(--foreground))]'}"
|
||||
onclick={() => theme.setVariant(variant)}
|
||||
>
|
||||
{THEME_DEFINITIONS[variant].label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<!-- Storage Section -->
|
||||
<SettingsSection title="Speicher">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
<SettingsCard>
|
||||
<div class="px-5 py-4">
|
||||
<p class="font-medium text-[hsl(var(--foreground))] mb-2">Speicherplatz</p>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))] mb-4">Dein genutzter Speicherplatz</p>
|
||||
<div class="w-full h-2 bg-[hsl(var(--muted))] rounded-full overflow-hidden mb-2">
|
||||
<div class="h-full bg-[hsl(var(--primary))] rounded-full" style="width: 25%"></div>
|
||||
</div>
|
||||
<p class="text-sm text-[hsl(var(--muted-foreground))]">2.5 GB von 10 GB verwendet</p>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<!-- Global Settings Section -->
|
||||
<GlobalSettingsSection {userSettings} />
|
||||
|
||||
<!-- About Section -->
|
||||
<SettingsSection title="Über">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
<SettingsCard>
|
||||
<SettingsRow label="Version" border={false}>
|
||||
<span class="text-[hsl(var(--muted-foreground))]">1.0.0</span>
|
||||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
</SettingsPage>
|
||||
337
apps-archived/storage/apps/web/src/routes/shared/+page.svelte
Normal file
337
apps-archived/storage/apps/web/src/routes/shared/+page.svelte
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Share2, Link, Copy, Trash2 } from 'lucide-svelte';
|
||||
import { sharesApi } from '$lib/api/client';
|
||||
import type { Share } from '$lib/api/client';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
|
||||
let shares = $state<Share[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
await loadShares();
|
||||
});
|
||||
|
||||
async function loadShares() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
const result = await sharesApi.list();
|
||||
if (result.error) {
|
||||
error = result.error;
|
||||
} else if (result.data) {
|
||||
shares = result.data;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function copyShareLink(token: string) {
|
||||
const url = `${window.location.origin}/s/${token}`;
|
||||
await navigator.clipboard.writeText(url);
|
||||
toast.success('Link kopiert!');
|
||||
}
|
||||
|
||||
async function deleteShare(id: string) {
|
||||
if (!confirm('Share-Link wirklich löschen?')) return;
|
||||
|
||||
const result = await sharesApi.delete(id);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
shares = shares.filter((s) => s.id !== id);
|
||||
toast.success('Share-Link gelöscht');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return 'Kein Ablaufdatum';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
function getAccessLevelLabel(level: string): string {
|
||||
switch (level) {
|
||||
case 'view':
|
||||
return 'Ansehen';
|
||||
case 'download':
|
||||
return 'Herunterladen';
|
||||
case 'edit':
|
||||
return 'Bearbeiten';
|
||||
default:
|
||||
return level;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Geteilt - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="shared-page">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<Share2 size={24} />
|
||||
Geteilte Links
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="error-state">
|
||||
<p>Fehler: {error}</p>
|
||||
<button onclick={loadShares}>Erneut versuchen</button>
|
||||
</div>
|
||||
{:else if shares.length === 0}
|
||||
<div class="empty-state">
|
||||
<Share2 size={48} />
|
||||
<h2>Keine geteilten Links</h2>
|
||||
<p>Teile Dateien oder Ordner, um Links hier zu verwalten.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="shares-list">
|
||||
{#each shares as share (share.id)}
|
||||
<div class="share-item">
|
||||
<div class="share-info">
|
||||
<div class="share-icon">
|
||||
<Link size={20} />
|
||||
</div>
|
||||
<div class="share-details">
|
||||
<span class="share-type">
|
||||
{share.shareType === 'file' ? 'Datei' : 'Ordner'}
|
||||
</span>
|
||||
<div class="share-meta">
|
||||
<span class="badge">{getAccessLevelLabel(share.accessLevel)}</span>
|
||||
{#if share.password}
|
||||
<span class="badge secure">Passwortgeschützt</span>
|
||||
{/if}
|
||||
{#if share.maxDownloads}
|
||||
<span class="badge">{share.downloadCount}/{share.maxDownloads} Downloads</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="share-expires">
|
||||
{share.expiresAt ? `Läuft ab: ${formatDate(share.expiresAt)}` : 'Kein Ablaufdatum'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="share-actions">
|
||||
<button class="copy-btn" onclick={() => copyShareLink(share.shareToken)}>
|
||||
<Copy size={16} />
|
||||
Link kopieren
|
||||
</button>
|
||||
<button class="delete-btn" onclick={() => deleteShare(share.id)}>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shared-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state .spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgb(var(--color-border));
|
||||
border-top-color: rgb(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state p,
|
||||
.error-state p {
|
||||
margin-top: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.error-state button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-primary));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shares-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.share-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.share-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.share-icon {
|
||||
padding: 0.5rem;
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: var(--radius-md);
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.share-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.share-type {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.share-meta {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: rgb(var(--color-surface));
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.badge.secure {
|
||||
background: rgba(var(--color-success), 0.1);
|
||||
color: rgb(var(--color-success));
|
||||
}
|
||||
|
||||
.share-expires {
|
||||
font-size: 0.75rem;
|
||||
color: rgb(var(--color-text-tertiary));
|
||||
}
|
||||
|
||||
.share-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgb(var(--color-primary));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
color: rgb(var(--color-error));
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: rgba(var(--color-error), 0.1);
|
||||
border-color: rgb(var(--color-error));
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.share-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.share-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
135
apps-archived/storage/apps/web/src/routes/themes/+page.svelte
Normal file
135
apps-archived/storage/apps/web/src/routes/themes/+page.svelte
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<script lang="ts">
|
||||
import { Palette, Check } from 'lucide-svelte';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { THEME_DEFINITIONS, THEME_VARIANTS } from '@manacore/shared-theme';
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Themes - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="themes-page">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<Palette size={24} />
|
||||
Themes
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="themes-grid">
|
||||
{#each THEME_VARIANTS as variant}
|
||||
{@const def = THEME_DEFINITIONS[variant]}
|
||||
<button
|
||||
class="theme-card"
|
||||
class:active={theme.variant === variant}
|
||||
onclick={() => theme.setVariant(variant)}
|
||||
>
|
||||
<div
|
||||
class="theme-preview"
|
||||
style="background: linear-gradient(135deg, {def.colors.primary}, {def.colors.accent})"
|
||||
>
|
||||
{#if theme.variant === variant}
|
||||
<div class="check-badge">
|
||||
<Check size={16} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="theme-info">
|
||||
<span class="theme-name">{def.label}</span>
|
||||
<span class="theme-icon">{def.icon}</span>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.themes-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.themes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 2px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-card:hover {
|
||||
border-color: rgb(var(--color-primary));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.theme-card.active {
|
||||
border-color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
position: relative;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.check-badge {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
color: rgb(var(--color-primary));
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.theme-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.themes-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
359
apps-archived/storage/apps/web/src/routes/trash/+page.svelte
Normal file
359
apps-archived/storage/apps/web/src/routes/trash/+page.svelte
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Trash2, RotateCcw, AlertTriangle } from 'lucide-svelte';
|
||||
import { trashApi } from '$lib/api/client';
|
||||
import type { StorageFile, StorageFolder } from '$lib/api/client';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
|
||||
let files = $state<StorageFile[]>([]);
|
||||
let folders = $state<StorageFolder[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
await loadTrash();
|
||||
});
|
||||
|
||||
async function loadTrash() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
const result = await trashApi.list();
|
||||
if (result.error) {
|
||||
error = result.error;
|
||||
} else if (result.data) {
|
||||
files = result.data.files;
|
||||
folders = result.data.folders;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function handleRestore(id: string, type: 'file' | 'folder') {
|
||||
const result = await trashApi.restore(id, type);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
if (type === 'file') {
|
||||
files = files.filter((f) => f.id !== id);
|
||||
} else {
|
||||
folders = folders.filter((f) => f.id !== id);
|
||||
}
|
||||
toast.success('Wiederhergestellt');
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePermanentDelete(id: string, type: 'file' | 'folder') {
|
||||
if (!confirm('Endgültig löschen? Dies kann nicht rückgängig gemacht werden.')) return;
|
||||
|
||||
const result = await trashApi.permanentDelete(id, type);
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
if (type === 'file') {
|
||||
files = files.filter((f) => f.id !== id);
|
||||
} else {
|
||||
folders = folders.filter((f) => f.id !== id);
|
||||
}
|
||||
toast.success('Endgültig gelöscht');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEmptyTrash() {
|
||||
if (!confirm('Papierkorb leeren? Alle Elemente werden endgültig gelöscht.')) return;
|
||||
|
||||
const result = await trashApi.empty();
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
files = [];
|
||||
folders = [];
|
||||
toast.success('Papierkorb geleert');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr) return '—';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Papierkorb - Storage</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="trash-page">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
<Trash2 size={24} />
|
||||
Papierkorb
|
||||
</h1>
|
||||
|
||||
{#if files.length > 0 || folders.length > 0}
|
||||
<button class="empty-btn" onclick={handleEmptyTrash}>
|
||||
<AlertTriangle size={16} />
|
||||
Papierkorb leeren
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="error-state">
|
||||
<p>Fehler: {error}</p>
|
||||
<button onclick={loadTrash}>Erneut versuchen</button>
|
||||
</div>
|
||||
{:else if files.length === 0 && folders.length === 0}
|
||||
<div class="empty-state">
|
||||
<Trash2 size={48} />
|
||||
<h2>Papierkorb ist leer</h2>
|
||||
<p>Gelöschte Dateien und Ordner erscheinen hier.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="trash-list">
|
||||
{#each folders as folder (folder.id)}
|
||||
<div class="trash-item">
|
||||
<div class="item-info">
|
||||
<span class="item-icon folder">📁</span>
|
||||
<div class="item-details">
|
||||
<span class="item-name">{folder.name}</span>
|
||||
<span class="item-meta">Gelöscht am {formatDate(folder.deletedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="restore-btn" onclick={() => handleRestore(folder.id, 'folder')}>
|
||||
<RotateCcw size={16} />
|
||||
Wiederherstellen
|
||||
</button>
|
||||
<button class="delete-btn" onclick={() => handlePermanentDelete(folder.id, 'folder')}>
|
||||
Endgültig löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#each files as file (file.id)}
|
||||
<div class="trash-item">
|
||||
<div class="item-info">
|
||||
<span class="item-icon file">📄</span>
|
||||
<div class="item-details">
|
||||
<span class="item-name">{file.name}</span>
|
||||
<span class="item-meta">Gelöscht am {formatDate(file.deletedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="restore-btn" onclick={() => handleRestore(file.id, 'file')}>
|
||||
<RotateCcw size={16} />
|
||||
Wiederherstellen
|
||||
</button>
|
||||
<button class="delete-btn" onclick={() => handlePermanentDelete(file.id, 'file')}>
|
||||
Endgültig löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.trash-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-error));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.empty-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state .spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgb(var(--color-border));
|
||||
border-top-color: rgb(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state p,
|
||||
.error-state p {
|
||||
margin-top: 1rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.error-state button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgb(var(--color-primary));
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.trash-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.trash-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: rgb(var(--color-surface-elevated));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
font-size: 0.75rem;
|
||||
color: rgb(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.restore-btn,
|
||||
.delete-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.restore-btn {
|
||||
background: rgb(var(--color-surface));
|
||||
border: 1px solid rgb(var(--color-border));
|
||||
color: rgb(var(--color-text-primary));
|
||||
}
|
||||
|
||||
.restore-btn:hover {
|
||||
border-color: rgb(var(--color-primary));
|
||||
color: rgb(var(--color-primary));
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgb(var(--color-error));
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.trash-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.restore-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue