Inital build
Some checks failed
Automated Container Build / build-and-push (push) Failing after 12s

This commit is contained in:
Elijah 2026-05-22 12:29:43 -07:00
parent fa2be029a2
commit 724d70e58b
3339 changed files with 1075535 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,239 @@
'use client';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Download, File, Image as ImageIcon, Film, Music, FileText, Code, Loader2 } from 'lucide-react';
import api from '@/lib/api';
interface FilePreviewProps {
file: {
name: string;
path: string;
size: number;
mime_type: string;
} | null;
onClose: () => void;
}
export default function FilePreview({ file, onClose }: FilePreviewProps) {
const [content, setContent] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
if (file) {
window.addEventListener('keydown', handleKeyDown);
}
return () => window.removeEventListener('keydown', handleKeyDown);
}, [file, onClose]);
useEffect(() => {
if (!file) return;
const type = getPreviewType(file);
if (type === 'text' || type === 'code') {
if (file.size > 2 * 1024 * 1024) { // 2MB limit for text preview
setError('File is too large to preview. Please download it.');
return;
}
setLoading(true);
setError(null);
fetch(api.getDownloadUrl(file.path))
.then((res) => {
if (!res.ok) throw new Error('Failed to load file content');
return res.text();
})
.then((text) => setContent(text))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}
}, [file]);
if (!file) return null;
const previewType = getPreviewType(file);
const downloadUrl = api.getDownloadUrl(file.path);
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-8"
style={{ backgroundColor: 'rgba(0, 0, 0, 0.8)', backdropFilter: 'blur(8px)' }}
onClick={onClose}
>
<motion.div
initial={{ scale: 0.95, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0, y: 20 }}
onClick={(e) => e.stopPropagation()}
className="relative flex flex-col w-full max-w-5xl h-full max-h-[90vh] rounded-2xl overflow-hidden shadow-2xl"
style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b shrink-0" style={{ borderColor: 'var(--color-border-subtle)', backgroundColor: 'var(--color-bg-secondary)' }}>
<div className="flex items-center gap-3 min-w-0">
{getIcon(previewType)}
<span className="font-medium truncate" style={{ color: 'var(--color-text-primary)' }} title={file.name}>
{file.name}
</span>
<span className="text-xs px-2 py-1 rounded-md" style={{ backgroundColor: 'var(--color-bg-tertiary)', color: 'var(--color-text-tertiary)' }}>
{formatSize(file.size)}
</span>
</div>
<div className="flex items-center gap-2 shrink-0">
<a
href={downloadUrl}
download={file.name}
target="_blank"
rel="noreferrer"
className="p-2 rounded-lg transition-colors hover:bg-white/10"
style={{ color: 'var(--color-text-secondary)' }}
title="Download"
>
<Download className="w-5 h-5" />
</a>
<button
onClick={onClose}
className="p-2 rounded-lg transition-colors hover:bg-white/10"
style={{ color: 'var(--color-text-secondary)' }}
title="Close (Esc)"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Content Area */}
<div className="flex-1 overflow-auto flex items-center justify-center p-4 relative" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
{previewType === 'image' && (
<img
src={downloadUrl}
alt={file.name}
className="max-w-full max-h-full object-contain rounded-lg"
/>
)}
{previewType === 'video' && (
<video
src={downloadUrl}
controls
autoPlay
className="max-w-full max-h-full rounded-lg outline-none"
/>
)}
{previewType === 'audio' && (
<div className="w-full max-w-md p-6 rounded-xl text-center" style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}>
<Music className="w-16 h-16 mx-auto mb-6" style={{ color: 'var(--color-accent)' }} />
<h3 className="text-lg font-medium mb-4 truncate" style={{ color: 'var(--color-text-primary)' }}>{file.name}</h3>
<audio src={downloadUrl} controls autoPlay className="w-full outline-none" />
</div>
)}
{(previewType === 'text' || previewType === 'code') && (
<div className="absolute inset-0 p-4">
{loading ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-accent)' }} />
</div>
) : error ? (
<div className="flex flex-col items-center justify-center h-full gap-2">
<p style={{ color: 'var(--color-danger)' }}>{error}</p>
<a
href={downloadUrl}
target="_blank"
rel="noreferrer"
className="px-4 py-2 mt-2 rounded-lg text-sm font-medium transition-opacity hover:opacity-90"
style={{ backgroundColor: 'var(--color-accent)', color: 'white' }}
>
Download File
</a>
</div>
) : (
<pre
className="w-full h-full p-4 overflow-auto text-sm rounded-lg"
style={{
fontFamily: 'var(--font-mono)',
backgroundColor: 'var(--color-bg-secondary)',
color: 'var(--color-text-primary)',
border: '1px solid var(--color-border-subtle)',
}}
>
{content}
</pre>
)}
</div>
)}
{previewType === 'unsupported' && (
<div className="flex flex-col items-center justify-center gap-4 text-center">
<File className="w-16 h-16" style={{ color: 'var(--color-text-tertiary)' }} />
<div>
<h3 className="text-lg font-medium mb-1" style={{ color: 'var(--color-text-primary)' }}>No Preview Available</h3>
<p className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>This file type cannot be previewed in the browser.</p>
</div>
<a
href={downloadUrl}
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 px-6 py-2.5 mt-2 rounded-xl text-sm font-medium transition-opacity hover:opacity-90"
style={{ backgroundColor: 'var(--color-accent)', color: 'white', boxShadow: 'var(--shadow-glow)' }}
>
<Download className="w-4 h-4" />
Download File
</a>
</div>
)}
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
}
// --- Helpers ---
type PreviewType = 'image' | 'video' | 'audio' | 'text' | 'code' | 'unsupported';
function getPreviewType(file: { name: string; mime_type: string }): PreviewType {
const ext = file.name.split('.').pop()?.toLowerCase() || '';
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext)) return 'image';
if (['mp4', 'webm', 'ogg'].includes(ext)) return 'video'; // Only browser-supported videos
if (['mp3', 'wav', 'flac', 'm4a', 'aac'].includes(ext)) return 'audio';
if (['txt', 'md', 'csv', 'json', 'log'].includes(ext)) return 'text';
if (['js', 'ts', 'jsx', 'tsx', 'html', 'css', 'go', 'py', 'rs', 'c', 'cpp', 'java', 'sh', 'yml', 'yaml', 'xml'].includes(ext)) return 'code';
if (file.mime_type.startsWith('image/')) return 'image';
if (file.mime_type.startsWith('video/')) return 'video';
if (file.mime_type.startsWith('audio/')) return 'audio';
if (file.mime_type.startsWith('text/')) return 'text';
return 'unsupported';
}
function getIcon(type: PreviewType) {
const props = { className: "w-5 h-5", style: { color: 'var(--color-text-secondary)' } };
switch (type) {
case 'image': return <ImageIcon {...props} />;
case 'video': return <Film {...props} />;
case 'audio': return <Music {...props} />;
case 'text': return <FileText {...props} />;
case 'code': return <Code {...props} />;
default: return <File {...props} />;
}
}
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]}`;
}

View file

@ -0,0 +1,221 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { HardDrive, Lock, User, LogIn, Shield } from 'lucide-react';
import api from '@/lib/api';
interface LoginPageProps {
onSuccess: () => void;
}
export default function LoginPage({ onSuccess }: LoginPageProps) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [totpCode, setTotpCode] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [needs2FA, setNeeds2FA] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setLoading(true);
try {
const { ok, status, data } = await api.login(username, password, needs2FA ? totpCode : undefined);
if (ok) {
onSuccess();
} else if (status === 428 && data.totp_required) {
setNeeds2FA(true);
} else {
setError(data.error || 'Login failed');
}
} catch {
setError('Connection failed. Is the server running?');
} finally {
setLoading(false);
}
}
return (
<div
className="flex items-center justify-center min-h-screen p-4"
style={{ backgroundColor: 'var(--color-bg-primary)' }}
>
{/* Subtle background gradient orbs */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div
className="absolute -top-40 -right-40 w-96 h-96 rounded-full opacity-20 blur-3xl"
style={{ background: 'radial-gradient(circle, var(--color-accent), transparent)' }}
/>
<div
className="absolute -bottom-40 -left-40 w-96 h-96 rounded-full opacity-10 blur-3xl"
style={{ background: 'radial-gradient(circle, #8b5cf6, transparent)' }}
/>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
className="w-full max-w-md relative z-10"
>
{/* Logo & Title */}
<div className="text-center mb-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring', stiffness: 200 }}
className="inline-flex items-center justify-center w-16 h-16 rounded-2xl mb-4"
style={{
background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)',
boxShadow: 'var(--shadow-glow)',
}}
>
<HardDrive className="w-8 h-8 text-white" />
</motion.div>
<h1 className="text-2xl font-bold" style={{ color: 'var(--color-text-primary)' }}>
Welcome Back
</h1>
<p className="mt-2 text-sm" style={{ color: 'var(--color-text-secondary)' }}>
Sign in to access your files
</p>
</div>
{/* Login Card */}
<div
className="rounded-2xl p-8"
style={{
backgroundColor: 'var(--color-bg-elevated)',
border: '1px solid var(--color-border)',
boxShadow: 'var(--shadow-lg)',
backdropFilter: 'blur(20px)',
}}
>
<form onSubmit={handleSubmit} className="space-y-4">
{!needs2FA ? (
<>
<label className="block">
<span className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-secondary)' }}>
Username
</span>
<div className="relative mt-1.5">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
<input
id="login-username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Your username"
autoFocus
className="w-full pl-10 pr-4 py-3 rounded-xl text-sm outline-none transition-all"
style={{
backgroundColor: 'var(--color-bg-tertiary)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-primary)',
}}
/>
</div>
</label>
<label className="block">
<span className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-secondary)' }}>
Password
</span>
<div className="relative mt-1.5">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
<input
id="login-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Your password"
className="w-full pl-10 pr-4 py-3 rounded-xl text-sm outline-none transition-all"
style={{
backgroundColor: 'var(--color-bg-tertiary)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-primary)',
}}
/>
</div>
</label>
</>
) : (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }}>
<div
className="flex items-center gap-2 p-3 rounded-lg mb-4"
style={{
backgroundColor: 'var(--color-accent-subtle)',
border: '1px solid var(--color-accent)',
}}
>
<Shield className="w-4 h-4" style={{ color: 'var(--color-accent)' }} />
<span className="text-xs" style={{ color: 'var(--color-accent)' }}>
Two-factor authentication required
</span>
</div>
<label className="block">
<span className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-secondary)' }}>
Authentication Code
</span>
<input
id="login-totp"
type="text"
inputMode="numeric"
maxLength={6}
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, ''))}
placeholder="000000"
autoFocus
className="w-full mt-1.5 px-4 py-3 rounded-xl text-sm text-center tracking-[0.5em] font-mono outline-none transition-all"
style={{
backgroundColor: 'var(--color-bg-tertiary)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-primary)',
}}
/>
</label>
</motion.div>
)}
{error && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-sm px-3 py-2 rounded-lg"
style={{
color: 'var(--color-danger)',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
}}
>
{error}
</motion.p>
)}
<button
id="login-submit"
type="submit"
disabled={loading}
className="w-full mt-2 py-3 rounded-xl text-sm font-semibold text-white flex items-center justify-center gap-2 transition-all duration-200 hover:opacity-90 disabled:opacity-50"
style={{
background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)',
boxShadow: 'var(--shadow-glow)',
}}
>
{loading ? (
<div className="w-5 h-5 rounded-full border-2 border-white border-t-transparent" style={{ animation: 'spin 0.8s linear infinite' }} />
) : (
<>
<LogIn className="w-4 h-4" />
{needs2FA ? 'Verify' : 'Sign In'}
</>
)}
</button>
</form>
</div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,242 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Settings, Shield, Image as ImageIcon, History, Trash2, Save, Loader2 } from 'lucide-react';
import api from '@/lib/api';
interface SettingsData {
theme: string;
thumbnail_images: string;
thumbnail_videos: string;
max_versions: string;
trash_auto_purge_days: string;
}
export default function SettingsPage() {
const [settings, setSettings] = useState<SettingsData | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
useEffect(() => {
loadSettings();
}, []);
async function loadSettings() {
try {
const data = await api.getSettings();
setSettings({
theme: data.theme || 'dark',
thumbnail_images: data.thumbnail_images || 'true',
thumbnail_videos: data.thumbnail_videos || 'true',
max_versions: data.max_versions || '1',
trash_auto_purge_days: data.trash_auto_purge_days || '14',
});
} catch (err: any) {
setError(err.message || 'Failed to load settings');
} finally {
setLoading(false);
}
}
async function handleSave() {
if (!settings) return;
setSaving(true);
setError(null);
setSuccess(false);
try {
await api.updateSettings(settings);
setSuccess(true);
setTimeout(() => setSuccess(false), 3000);
// Update local theme variable if changed
if (settings.theme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
document.documentElement.classList.remove('dark');
document.documentElement.style.setProperty('color-scheme', 'light');
} else {
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.classList.add('dark');
document.documentElement.style.setProperty('color-scheme', 'dark');
}
} catch (err: any) {
setError(err.message || 'Failed to save settings');
} finally {
setSaving(false);
}
}
if (loading) {
return (
<div className="flex-1 flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-text-tertiary)' }} />
</div>
);
}
if (!settings) return null;
const sections = [
{
id: 'appearance',
title: 'Appearance',
icon: <Settings className="w-5 h-5" />,
content: (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Theme</label>
<select
value={settings.theme}
onChange={(e) => setSettings({ ...settings, theme: e.target.value })}
className="w-full max-w-xs px-3 py-2 rounded-lg text-sm outline-none"
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
>
<option value="dark">Dark Mode</option>
<option value="light">Light Mode</option>
</select>
</div>
</div>
)
},
{
id: 'thumbnails',
title: 'Media & Thumbnails',
icon: <ImageIcon className="w-5 h-5" />,
content: (
<div className="space-y-4">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={settings.thumbnail_images === 'true'}
onChange={(e) => setSettings({ ...settings, thumbnail_images: e.target.checked ? 'true' : 'false' })}
className="w-4 h-4 rounded text-accent focus:ring-accent"
style={{ accentColor: 'var(--color-accent)' }}
/>
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>Generate thumbnails for images</span>
</label>
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={settings.thumbnail_videos === 'true'}
onChange={(e) => setSettings({ ...settings, thumbnail_videos: e.target.checked ? 'true' : 'false' })}
className="w-4 h-4 rounded text-accent focus:ring-accent"
style={{ accentColor: 'var(--color-accent)' }}
/>
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>Generate thumbnails for videos (requires FFmpeg)</span>
</label>
</div>
)
},
{
id: 'versioning',
title: 'File Versioning',
icon: <History className="w-5 h-5" />,
content: (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Maximum Versions to Keep</label>
<p className="text-xs mb-3" style={{ color: 'var(--color-text-secondary)' }}>
Older versions will be automatically deleted when a file is overwritten more times than this limit. Set to 0 to disable versioning.
</p>
<input
type="number"
min="0"
max="100"
value={settings.max_versions}
onChange={(e) => setSettings({ ...settings, max_versions: e.target.value })}
className="w-full max-w-xs px-3 py-2 rounded-lg text-sm outline-none"
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
/>
</div>
</div>
)
},
{
id: 'trash',
title: 'Trash Auto-Purge',
icon: <Trash2 className="w-5 h-5" />,
content: (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Days to Keep Trashed Files</label>
<p className="text-xs mb-3" style={{ color: 'var(--color-text-secondary)' }}>
Files in the trash will be permanently deleted after this many days. Set to 0 to disable auto-purge.
</p>
<input
type="number"
min="0"
max="365"
value={settings.trash_auto_purge_days}
onChange={(e) => setSettings({ ...settings, trash_auto_purge_days: e.target.value })}
className="w-full max-w-xs px-3 py-2 rounded-lg text-sm outline-none"
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
/>
</div>
</div>
)
}
];
return (
<div className="flex-1 overflow-y-auto w-full">
<div className="p-8 max-w-4xl mx-auto w-full">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--color-text-primary)' }}>Settings</h1>
<p style={{ color: 'var(--color-text-secondary)' }}>Manage your Drive preferences and configurations.</p>
</div>
{error && (
<div className="mb-6 p-4 rounded-xl flex items-center gap-3" style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', color: 'var(--color-danger)' }}>
<Shield className="w-5 h-5" />
<span className="text-sm font-medium">{error}</span>
</div>
)}
{success && (
<div className="mb-6 p-4 rounded-xl flex items-center gap-3" style={{ backgroundColor: 'rgba(34, 197, 94, 0.1)', color: 'var(--color-success)' }}>
<Save className="w-5 h-5" />
<span className="text-sm font-medium">Settings saved successfully.</span>
</div>
)}
<div className="space-y-6">
{sections.map((section, idx) => (
<motion.div
key={section.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
className="p-6 rounded-2xl"
style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}
>
<div className="flex items-center gap-3 mb-6" style={{ color: 'var(--color-text-primary)' }}>
<div className="p-2 rounded-lg" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
{section.icon}
</div>
<h2 className="text-xl font-semibold">{section.title}</h2>
</div>
{section.content}
</motion.div>
))}
</div>
<div className="mt-8 flex justify-end">
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-medium text-white transition-all hover:opacity-90 disabled:opacity-50"
style={{ background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)' }}
>
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
Save Settings
</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,258 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { HardDrive, Lock, User, ArrowRight, Check } from 'lucide-react';
import api from '@/lib/api';
interface SetupWizardProps {
onComplete: () => void;
}
export default function SetupWizard({ onComplete }: SetupWizardProps) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [step, setStep] = useState(1);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
if (step === 1) {
if (username.length < 3) {
setError('Username must be at least 3 characters');
return;
}
setStep(2);
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
setLoading(true);
try {
const result = await api.setup(username, password);
if (result.error) {
setError(result.error);
} else {
setStep(3);
setTimeout(onComplete, 1500);
}
} catch {
setError('Failed to complete setup');
} finally {
setLoading(false);
}
}
return (
<div
className="flex items-center justify-center min-h-screen p-4"
style={{ backgroundColor: 'var(--color-bg-primary)' }}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
className="w-full max-w-md"
>
{/* Logo & Title */}
<div className="text-center mb-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring', stiffness: 200 }}
className="inline-flex items-center justify-center w-16 h-16 rounded-2xl mb-4"
style={{
background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)',
boxShadow: 'var(--shadow-glow)',
}}
>
<HardDrive className="w-8 h-8 text-white" />
</motion.div>
<h1 className="text-2xl font-bold" style={{ color: 'var(--color-text-primary)' }}>
Welcome to Drive
</h1>
<p className="mt-2 text-sm" style={{ color: 'var(--color-text-secondary)' }}>
{step === 3 ? 'Setup complete!' : "Let's set up your account"}
</p>
</div>
{/* Progress Indicator */}
<div className="flex items-center justify-center gap-2 mb-8">
{[1, 2, 3].map((s) => (
<div
key={s}
className="h-1.5 rounded-full transition-all duration-300"
style={{
width: step >= s ? '2rem' : '0.75rem',
backgroundColor: step >= s ? 'var(--color-accent)' : 'var(--color-border)',
}}
/>
))}
</div>
{/* Card */}
<div
className="rounded-2xl p-8"
style={{
backgroundColor: 'var(--color-bg-elevated)',
border: '1px solid var(--color-border)',
boxShadow: 'var(--shadow-lg)',
}}
>
{step === 3 ? (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="flex flex-col items-center gap-4 py-8"
>
<div
className="w-16 h-16 rounded-full flex items-center justify-center"
style={{ backgroundColor: 'rgba(34, 197, 94, 0.15)' }}
>
<Check className="w-8 h-8" style={{ color: 'var(--color-success)' }} />
</div>
<p className="font-medium" style={{ color: 'var(--color-text-primary)' }}>
Account created successfully!
</p>
<p className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>
Redirecting to login...
</p>
</motion.div>
) : (
<form onSubmit={handleSubmit}>
<motion.div
key={step}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
{step === 1 && (
<div className="space-y-4">
<label className="block">
<span className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-secondary)' }}>
Username
</span>
<div className="relative mt-1.5">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
<input
id="setup-username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Choose a username"
autoFocus
className="w-full pl-10 pr-4 py-3 rounded-xl text-sm outline-none transition-all"
style={{
backgroundColor: 'var(--color-bg-tertiary)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-primary)',
}}
/>
</div>
</label>
</div>
)}
{step === 2 && (
<div className="space-y-4">
<label className="block">
<span className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-secondary)' }}>
Password
</span>
<div className="relative mt-1.5">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
<input
id="setup-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Create a strong password"
autoFocus
className="w-full pl-10 pr-4 py-3 rounded-xl text-sm outline-none transition-all"
style={{
backgroundColor: 'var(--color-bg-tertiary)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-primary)',
}}
/>
</div>
</label>
<label className="block">
<span className="text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-secondary)' }}>
Confirm Password
</span>
<div className="relative mt-1.5">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
<input
id="setup-confirm-password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm your password"
className="w-full pl-10 pr-4 py-3 rounded-xl text-sm outline-none transition-all"
style={{
backgroundColor: 'var(--color-bg-tertiary)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-primary)',
}}
/>
</div>
</label>
</div>
)}
</motion.div>
{error && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-4 text-sm px-3 py-2 rounded-lg"
style={{
color: 'var(--color-danger)',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
}}
>
{error}
</motion.p>
)}
<button
id="setup-submit"
type="submit"
disabled={loading}
className="w-full mt-6 py-3 rounded-xl text-sm font-semibold text-white flex items-center justify-center gap-2 transition-all duration-200 hover:opacity-90 disabled:opacity-50"
style={{
background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)',
boxShadow: 'var(--shadow-glow)',
}}
>
{loading ? (
<div className="w-5 h-5 rounded-full border-2 border-white border-t-transparent" style={{ animation: 'spin 0.8s linear infinite' }} />
) : (
<>
{step === 1 ? 'Continue' : 'Create Account'}
<ArrowRight className="w-4 h-4" />
</>
)}
</button>
</form>
)}
</div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,173 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Link as LinkIcon, Shield, Clock, Download, Check, Copy } from 'lucide-react';
import api from '@/lib/api';
interface ShareModalProps {
filePath: string;
onClose: () => void;
}
export default function ShareModal({ filePath, onClose }: ShareModalProps) {
const [password, setPassword] = useState('');
const [expiresIn, setExpiresIn] = useState(''); // hours
const [maxDownloads, setMaxDownloads] = useState('');
const [loading, setLoading] = useState(false);
const [shareLink, setShareLink] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleCreate() {
setLoading(true);
setError(null);
try {
const exp = parseInt(expiresIn, 10);
const dl = parseInt(maxDownloads, 10);
const res = await api.createShare(
filePath,
password || undefined,
isNaN(exp) ? undefined : exp,
isNaN(dl) ? undefined : dl
);
const fullUrl = `${window.location.origin}${res.link}`;
setShareLink(fullUrl);
} catch (err: any) {
setError(err.message || 'Failed to create share link');
} finally {
setLoading(false);
}
}
function handleCopy() {
if (!shareLink) return;
navigator.clipboard.writeText(shareLink);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" onClick={onClose}>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
onClick={(e) => e.stopPropagation()}
className="w-full max-w-md rounded-2xl overflow-hidden flex flex-col"
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)' }}
>
<div className="flex items-center justify-between p-4 border-b" style={{ borderColor: 'var(--color-border-subtle)' }}>
<h2 className="text-lg font-semibold" style={{ color: 'var(--color-text-primary)' }}>Share File</h2>
<button onClick={onClose} className="p-1 rounded-lg hover:bg-white/5 transition-colors">
<X className="w-5 h-5" style={{ color: 'var(--color-text-tertiary)' }} />
</button>
</div>
<div className="p-5 space-y-5">
<p className="text-sm truncate" style={{ color: 'var(--color-text-secondary)' }}>
Creating public link for <strong>{filePath.split('/').pop()}</strong>
</p>
{error && (
<div className="p-3 rounded-lg text-sm" style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', color: 'var(--color-danger)' }}>
{error}
</div>
)}
{!shareLink ? (
<>
{/* Configuration */}
<div className="space-y-4">
<div>
<label className="flex items-center gap-2 text-sm font-medium mb-1.5" style={{ color: 'var(--color-text-primary)' }}>
<Shield className="w-4 h-4" /> Password (Optional)
</label>
<input
type="text"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Leave blank for no password"
className="w-full px-3 py-2 rounded-lg text-sm outline-none"
style={{ backgroundColor: 'var(--color-bg-tertiary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="flex items-center gap-2 text-sm font-medium mb-1.5" style={{ color: 'var(--color-text-primary)' }}>
<Clock className="w-4 h-4" /> Expires In (Hours)
</label>
<input
type="number"
min="1"
value={expiresIn}
onChange={(e) => setExpiresIn(e.target.value)}
placeholder="Never"
className="w-full px-3 py-2 rounded-lg text-sm outline-none"
style={{ backgroundColor: 'var(--color-bg-tertiary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
/>
</div>
<div>
<label className="flex items-center gap-2 text-sm font-medium mb-1.5" style={{ color: 'var(--color-text-primary)' }}>
<Download className="w-4 h-4" /> Max Downloads
</label>
<input
type="number"
min="1"
value={maxDownloads}
onChange={(e) => setMaxDownloads(e.target.value)}
placeholder="Unlimited"
className="w-full px-3 py-2 rounded-lg text-sm outline-none"
style={{ backgroundColor: 'var(--color-bg-tertiary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
/>
</div>
</div>
</div>
<button
onClick={handleCreate}
disabled={loading}
className="w-full py-2.5 rounded-xl text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-50 mt-2"
style={{ background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)' }}
>
{loading ? 'Creating...' : 'Create Link'}
</button>
</>
) : (
<>
{/* Success View */}
<div className="flex flex-col items-center justify-center py-4">
<div className="w-12 h-12 rounded-full flex items-center justify-center mb-3" style={{ backgroundColor: 'rgba(34, 197, 94, 0.1)' }}>
<Check className="w-6 h-6" style={{ color: 'var(--color-success)' }} />
</div>
<p className="text-sm font-medium text-center mb-4" style={{ color: 'var(--color-text-primary)' }}>
Share link created successfully!
</p>
<div className="w-full flex items-center gap-2 p-1 rounded-lg" style={{ backgroundColor: 'var(--color-bg-tertiary)', border: '1px solid var(--color-border)' }}>
<input
type="text"
readOnly
value={shareLink}
className="flex-1 bg-transparent px-2 py-1 text-sm outline-none"
style={{ color: 'var(--color-text-primary)' }}
/>
<button
onClick={handleCopy}
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs font-medium transition-colors"
style={{ backgroundColor: 'var(--color-accent)', color: 'white' }}
>
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
{copied ? 'Copied' : 'Copy'}
</button>
</div>
</div>
</>
)}
</div>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,145 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { HardDrive, Loader2, Image as ImageIcon, Film, Music, FileText, Archive, File } from 'lucide-react';
import api from '@/lib/api';
interface CategoryUsage {
category: string;
size: number;
count: number;
}
interface StorageData {
total_size: number;
categories: CategoryUsage[];
}
export default function StorageDashboard() {
const [data, setData] = useState<StorageData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadStorageData();
}, []);
async function loadStorageData() {
try {
const res = await api.getStorageDashboard();
setData(res);
} catch (err: any) {
setError(err.message || 'Failed to load storage data');
} finally {
setLoading(false);
}
}
if (loading) {
return (
<div className="flex-1 flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-text-tertiary)' }} />
</div>
);
}
if (error || !data) {
return (
<div className="flex-1 flex items-center justify-center text-red-500">
{error || 'Failed to load'}
</div>
);
}
const categoryConfig: Record<string, { label: string; color: string; icon: any }> = {
images: { label: 'Images', color: '#f59e0b', icon: ImageIcon },
videos: { label: 'Videos', color: '#ef4444', icon: Film },
audio: { label: 'Audio', color: '#22c55e', icon: Music },
documents: { label: 'Documents', color: '#3b82f6', icon: FileText },
archives: { label: 'Archives', color: '#8b5cf6', icon: Archive },
other: { label: 'Other', color: '#9ca3af', icon: File },
};
const totalDriveSize = 100 * 1024 * 1024 * 1024; // Dummy 100GB capacity for visualization, could be dynamic
return (
<div className="flex-1 overflow-y-auto p-8 max-w-5xl mx-auto w-full">
<div className="mb-8 flex items-center gap-4">
<div className="p-3 rounded-xl" style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}>
<HardDrive className="w-6 h-6" style={{ color: 'var(--color-accent)' }} />
</div>
<div>
<h1 className="text-3xl font-bold mb-1" style={{ color: 'var(--color-text-primary)' }}>Storage Overview</h1>
<p style={{ color: 'var(--color-text-secondary)' }}>View and manage your drive usage</p>
</div>
</div>
<div className="p-6 rounded-2xl mb-8" style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}>
<div className="flex justify-between items-end mb-4">
<div>
<span className="text-4xl font-bold" style={{ color: 'var(--color-text-primary)' }}>{formatSize(data.total_size)}</span>
<span className="text-lg ml-2" style={{ color: 'var(--color-text-secondary)' }}>used</span>
</div>
</div>
{/* Progress Bar */}
<div className="h-4 w-full rounded-full overflow-hidden flex gap-0.5" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
{data.categories.map(cat => {
const pct = (cat.size / Math.max(data.total_size, 1)) * 100;
if (pct === 0) return null;
return (
<motion.div
key={cat.category}
initial={{ width: 0 }}
animate={{ width: `${pct}%` }}
transition={{ duration: 1, ease: 'easeOut' }}
className="h-full"
style={{ backgroundColor: categoryConfig[cat.category]?.color || '#9ca3af' }}
title={`${categoryConfig[cat.category]?.label || cat.category}: ${formatSize(cat.size)}`}
/>
);
})}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.categories.map((cat, idx) => {
const config = categoryConfig[cat.category] || categoryConfig.other;
const Icon = config.icon;
return (
<motion.div
key={cat.category}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.1 }}
className="p-5 rounded-xl flex items-center gap-4"
style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}
>
<div className="p-3 rounded-lg flex-shrink-0" style={{ backgroundColor: `${config.color}20` }}>
<Icon className="w-6 h-6" style={{ color: config.color }} />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate" style={{ color: 'var(--color-text-primary)' }}>{config.label}</p>
<p className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>{cat.count} items</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold" style={{ color: 'var(--color-text-primary)' }}>{formatSize(cat.size)}</p>
<p className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
{((cat.size / Math.max(data.total_size, 1)) * 100).toFixed(1)}%
</p>
</div>
</motion.div>
);
})}
</div>
</div>
);
}
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]}`;
}

View file

@ -0,0 +1,139 @@
'use client';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Loader2, CheckCircle, XCircle, ChevronUp, ChevronDown, X } from 'lucide-react';
import api from '@/lib/api';
interface Task {
id: string;
type: string;
status: 'pending' | 'running' | 'completed' | 'failed';
progress: number;
message: string;
error?: string;
created_at: string;
}
export default function TaskManager() {
const [tasks, setTasks] = useState<Task[]>([]);
const [expanded, setExpanded] = useState(false);
const [visible, setVisible] = useState(false);
useEffect(() => {
let interval: NodeJS.Timeout;
async function pollTasks() {
try {
const data = await api.getTasks();
const activeTasks = data.tasks || [];
setTasks(activeTasks);
// Auto-show if there are new active tasks
if (activeTasks.some((t: Task) => t.status === 'running' || t.status === 'pending')) {
setVisible(true);
} else if (activeTasks.length === 0) {
setVisible(false);
}
} catch {
// silently fail polling
}
}
pollTasks();
interval = setInterval(pollTasks, 2000); // Poll every 2 seconds
return () => clearInterval(interval);
}, []);
if (!visible || tasks.length === 0) return null;
const runningCount = tasks.filter(t => t.status === 'running' || t.status === 'pending').length;
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
className="fixed bottom-6 right-6 z-[100] w-80 shadow-2xl rounded-2xl overflow-hidden flex flex-col"
style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}
>
{/* Header */}
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center justify-between px-4 py-3 border-b transition-colors hover:bg-white/5"
style={{ borderColor: 'var(--color-border-subtle)', backgroundColor: 'var(--color-bg-secondary)' }}
>
<div className="flex items-center gap-2">
{runningCount > 0 ? (
<Loader2 className="w-4 h-4 animate-spin" style={{ color: 'var(--color-accent)' }} />
) : (
<CheckCircle className="w-4 h-4" style={{ color: 'var(--color-success)' }} />
)}
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>
{runningCount > 0 ? `${runningCount} active task${runningCount > 1 ? 's' : ''}` : 'All tasks completed'}
</span>
</div>
<div className="flex items-center gap-1">
{expanded ? <ChevronDown className="w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} /> : <ChevronUp className="w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />}
<div
onClick={(e) => { e.stopPropagation(); setVisible(false); }}
className="p-1 rounded hover:bg-white/10"
>
<X className="w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
</div>
</div>
</button>
{/* Task List */}
<AnimatePresence>
{expanded && (
<motion.div
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: 0 }}
className="overflow-hidden"
>
<div className="max-h-60 overflow-y-auto p-2 space-y-1" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
{tasks.map(task => (
<div key={task.id} className="p-3 rounded-xl" style={{ backgroundColor: 'var(--color-bg-elevated)' }}>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--color-text-primary)' }}>
{task.type}
</span>
<span className="text-xs" style={{ color: 'var(--color-text-tertiary)' }}>
{task.progress}%
</span>
</div>
<div className="h-1.5 w-full rounded-full overflow-hidden mb-2" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
<motion.div
className="h-full rounded-full"
initial={{ width: 0 }}
animate={{ width: `${task.progress}%` }}
style={{
backgroundColor: task.status === 'failed' ? 'var(--color-danger)' : task.status === 'completed' ? 'var(--color-success)' : 'var(--color-accent)'
}}
/>
</div>
<div className="flex items-start gap-2">
{task.status === 'failed' ? (
<XCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" style={{ color: 'var(--color-danger)' }} />
) : task.status === 'completed' ? (
<CheckCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" style={{ color: 'var(--color-success)' }} />
) : (
<Loader2 className="w-3.5 h-3.5 mt-0.5 animate-spin flex-shrink-0" style={{ color: 'var(--color-accent)' }} />
)}
<span className="text-xs leading-snug break-words" style={{ color: 'var(--color-text-secondary)' }}>
{task.status === 'failed' ? task.error : task.message}
</span>
</div>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}