This commit is contained in:
parent
fa2be029a2
commit
724d70e58b
3339 changed files with 1075535 additions and 0 deletions
186
frontend/src/app/globals.css
Normal file
186
frontend/src/app/globals.css
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ============================================
|
||||
DESIGN SYSTEM — Drive
|
||||
Premium dark/light theme with CSS variables
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
/* Dark theme (default) */
|
||||
--color-bg-primary: #0a0a0f;
|
||||
--color-bg-secondary: #12121a;
|
||||
--color-bg-tertiary: #1a1a26;
|
||||
--color-bg-elevated: #1e1e2e;
|
||||
--color-bg-hover: #252536;
|
||||
--color-bg-active: #2d2d42;
|
||||
--color-bg-glass: rgba(30, 30, 46, 0.7);
|
||||
|
||||
--color-border: #2a2a3d;
|
||||
--color-border-subtle: #1f1f30;
|
||||
--color-border-focus: #6366f1;
|
||||
|
||||
--color-text-primary: #e8e8ed;
|
||||
--color-text-secondary: #9ca3af;
|
||||
--color-text-tertiary: #6b7280;
|
||||
--color-text-inverse: #0a0a0f;
|
||||
|
||||
--color-accent: #6366f1;
|
||||
--color-accent-hover: #7c7ff7;
|
||||
--color-accent-subtle: rgba(99, 102, 241, 0.15);
|
||||
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-hover: #dc2626;
|
||||
--color-info: #3b82f6;
|
||||
|
||||
--color-sidebar-bg: #0d0d14;
|
||||
--color-sidebar-hover: #1a1a28;
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 24px;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
--shadow-glow: 0 0 20px rgba(99, 102, 241, 0.15);
|
||||
|
||||
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LIGHT THEME OVERRIDES
|
||||
============================================ */
|
||||
[data-theme="light"] {
|
||||
--color-bg-primary: #fafafa;
|
||||
--color-bg-secondary: #ffffff;
|
||||
--color-bg-tertiary: #f5f5f5;
|
||||
--color-bg-elevated: #ffffff;
|
||||
--color-bg-hover: #f0f0f5;
|
||||
--color-bg-active: #e8e8f0;
|
||||
--color-bg-glass: rgba(255, 255, 255, 0.8);
|
||||
|
||||
--color-border: #e2e2ea;
|
||||
--color-border-subtle: #ececf0;
|
||||
--color-border-focus: #6366f1;
|
||||
|
||||
--color-text-primary: #111118;
|
||||
--color-text-secondary: #4b5563;
|
||||
--color-text-tertiary: #9ca3af;
|
||||
--color-text-inverse: #ffffff;
|
||||
|
||||
--color-sidebar-bg: #f0f0f5;
|
||||
--color-sidebar-hover: #e4e4ec;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
--shadow-glow: 0 0 20px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BASE STYLES
|
||||
============================================ */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Smooth transitions for theme switching */
|
||||
body,
|
||||
body * {
|
||||
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SCROLLBAR STYLING
|
||||
============================================ */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SELECTION HIGHLIGHT
|
||||
============================================ */
|
||||
|
||||
::selection {
|
||||
background-color: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FOCUS STYLES
|
||||
============================================ */
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-border-focus);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITY ANIMATIONS
|
||||
============================================ */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from { opacity: 0; transform: translateX(20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: var(--shadow-glow); }
|
||||
50% { box-shadow: 0 0 30px rgba(99, 102, 241, 0.3); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
29
frontend/src/app/layout.tsx
Normal file
29
frontend/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Drive — Your Personal Cloud',
|
||||
description: 'A high-performance, self-hosted file storage and management platform.',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" data-theme="dark" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
76
frontend/src/app/page.tsx
Normal file
76
frontend/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import api from '@/lib/api';
|
||||
import SetupWizard from '@/components/SetupWizard';
|
||||
import LoginPage from '@/components/LoginPage';
|
||||
import FileExplorer from '@/components/FileExplorer';
|
||||
|
||||
type AppState = 'loading' | 'setup' | 'login' | 'app';
|
||||
|
||||
export default function Home() {
|
||||
const [state, setState] = useState<AppState>('loading');
|
||||
|
||||
useEffect(() => {
|
||||
checkState();
|
||||
}, []);
|
||||
|
||||
async function checkState() {
|
||||
try {
|
||||
const { setup_complete } = await api.checkSetup();
|
||||
if (!setup_complete) {
|
||||
setState('setup');
|
||||
return;
|
||||
}
|
||||
|
||||
if (api.isAuthenticated()) {
|
||||
setState('app');
|
||||
} else {
|
||||
setState('login');
|
||||
}
|
||||
} catch {
|
||||
setState('login');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetupComplete() {
|
||||
setState('login');
|
||||
}
|
||||
|
||||
function handleLoginSuccess() {
|
||||
setState('app');
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
api.logout();
|
||||
setState('login');
|
||||
}
|
||||
|
||||
if (state === 'loading') {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full border-2 border-t-transparent"
|
||||
style={{
|
||||
borderColor: 'var(--color-accent)',
|
||||
borderTopColor: 'transparent',
|
||||
animation: 'spin 0.8s linear infinite',
|
||||
}}
|
||||
/>
|
||||
<p style={{ color: 'var(--color-text-secondary)' }}>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'setup') {
|
||||
return <SetupWizard onComplete={handleSetupComplete} />;
|
||||
}
|
||||
|
||||
if (state === 'login') {
|
||||
return <LoginPage onSuccess={handleLoginSuccess} />;
|
||||
}
|
||||
|
||||
return <FileExplorer onLogout={handleLogout} />;
|
||||
}
|
||||
149
frontend/src/app/share/[id]/page.tsx
Normal file
149
frontend/src/app/share/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
'use client';
|
||||
|
||||
import { use, useState, useEffect } from 'react';
|
||||
import { Shield, Download, Lock, File, Folder, Loader2 } from 'lucide-react';
|
||||
import api from '@/lib/api';
|
||||
|
||||
export default function PublicSharePage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = use(params);
|
||||
const id = resolvedParams.id;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [requirePassword, setRequirePassword] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const [fileInfo, setFileInfo] = useState<{ name: string; size: number; is_dir: boolean } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchShareInfo();
|
||||
}, []);
|
||||
|
||||
async function fetchShareInfo(pwd?: string) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await api.getPublicShare(id, pwd);
|
||||
if (data.require_password) {
|
||||
setRequirePassword(true);
|
||||
} else {
|
||||
setRequirePassword(false);
|
||||
setFileInfo(data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.require_password) {
|
||||
setRequirePassword(true);
|
||||
} else {
|
||||
setError(err.error || 'Failed to load share');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePasswordSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
fetchShareInfo(password);
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
const url = api.getPublicDownloadUrl(id, password);
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
<div className="max-w-md w-full p-8 rounded-2xl text-center shadow-xl" style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}>
|
||||
<Shield className="w-12 h-12 mx-auto mb-4" style={{ color: 'var(--color-danger)' }} />
|
||||
<h1 className="text-2xl font-bold mb-2" style={{ color: 'var(--color-text-primary)' }}>Access Denied</h1>
|
||||
<p style={{ color: 'var(--color-text-secondary)' }}>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (requirePassword) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
<div className="max-w-md w-full p-8 rounded-2xl shadow-xl" style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}>
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ backgroundColor: 'var(--color-accent-subtle)' }}>
|
||||
<Lock className="w-8 h-8" style={{ color: 'var(--color-accent)' }} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold mb-2" style={{ color: 'var(--color-text-primary)' }}>Protected File</h1>
|
||||
<p className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>This shared file is password protected.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handlePasswordSubmit} className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter password"
|
||||
className="w-full px-4 py-3 rounded-xl outline-none"
|
||||
style={{ backgroundColor: 'var(--color-bg-tertiary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 rounded-xl text-white font-medium transition-opacity hover:opacity-90 shadow-lg"
|
||||
style={{ background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)' }}
|
||||
>
|
||||
Unlock
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
|
||||
<div className="max-w-md w-full p-8 rounded-2xl text-center shadow-xl" style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}>
|
||||
<div className="mb-6 flex justify-center">
|
||||
{fileInfo?.is_dir ? (
|
||||
<div className="w-24 h-24 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--color-accent-subtle)' }}>
|
||||
<Folder className="w-12 h-12" style={{ color: 'var(--color-accent)' }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-24 h-24 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--color-accent-subtle)' }}>
|
||||
<File className="w-12 h-12" style={{ color: 'var(--color-accent)' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="text-xl font-bold mb-1 truncate px-4" style={{ color: 'var(--color-text-primary)' }} title={fileInfo?.name}>
|
||||
{fileInfo?.name}
|
||||
</h1>
|
||||
<p className="text-sm mb-8" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
{fileInfo?.is_dir ? 'Folder' : formatSize(fileInfo?.size || 0)}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl text-white font-medium transition-opacity hover:opacity-90 shadow-lg"
|
||||
style={{ background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)' }}
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in a new issue