Sharing fixes, live preview
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m19s

This commit is contained in:
Elijah 2026-05-22 19:19:15 -07:00
parent 27fd6fa4f5
commit a52f1d63f7
4 changed files with 350 additions and 92 deletions

View file

@ -1,9 +1,17 @@
'use client';
import { use, useState, useEffect } from 'react';
import { Shield, Download, Lock, File, Folder, Loader2 } from 'lucide-react';
import { Shield, Download, Lock, File, Folder, Loader2, ChevronRight, Home, Image as ImageIcon, Film, Music, FileText, Archive } from 'lucide-react';
import api from '@/lib/api';
interface FileInfo {
name: string;
size: number;
is_dir: boolean;
mime_type?: string;
files?: FileInfo[];
}
export default function PublicSharePage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params);
const id = resolvedParams.id;
@ -11,17 +19,18 @@ export default function PublicSharePage({ params }: { params: Promise<{ id: stri
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; mime_type?: string } | null>(null);
const [fileInfo, setFileInfo] = useState<FileInfo | null>(null);
const [currentPath, setCurrentPath] = useState('');
useEffect(() => {
fetchShareInfo();
}, []);
fetchShareInfo(password, currentPath);
}, [currentPath]);
async function fetchShareInfo(pwd?: string) {
async function fetchShareInfo(pwd?: string, path?: string) {
setLoading(true);
setError(null);
try {
const data = await api.getPublicShare(id, pwd);
const data = await api.getPublicShare(id, pwd, path);
if (data.require_password) {
setRequirePassword(true);
} else {
@ -41,11 +50,12 @@ export default function PublicSharePage({ params }: { params: Promise<{ id: stri
function handlePasswordSubmit(e: React.FormEvent) {
e.preventDefault();
fetchShareInfo(password);
fetchShareInfo(password, currentPath);
}
function handleDownload() {
const url = api.getPublicDownloadUrl(id, password);
function handleDownload(specificPath?: string) {
const downloadPath = specificPath ? (currentPath ? `${currentPath}/${specificPath}` : specificPath) : currentPath;
const url = api.getPublicDownloadUrl(id, password, downloadPath);
window.location.href = url;
}
@ -56,21 +66,31 @@ export default function PublicSharePage({ params }: { params: Promise<{ id: stri
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
}
if (loading) {
function getFileIcon(isDir: boolean, mimeType?: string) {
if (isDir) return <Folder className="w-5 h-5 text-blue-500" />;
if (mimeType?.startsWith('image/')) return <ImageIcon className="w-5 h-5 text-green-500" />;
if (mimeType?.startsWith('video/')) return <Film className="w-5 h-5 text-purple-500" />;
if (mimeType?.startsWith('audio/')) return <Music className="w-5 h-5 text-yellow-500" />;
if (mimeType === 'application/pdf') return <FileText className="w-5 h-5 text-red-500" />;
if (mimeType === 'application/zip' || mimeType === 'application/x-gzip') return <Archive className="w-5 h-5 text-orange-500" />;
return <File className="w-5 h-5 text-gray-400" />;
}
if (loading && !fileInfo) {
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 className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#0f172a] to-[#1e1b4b]">
<Loader2 className="w-8 h-8 animate-spin text-white/50" />
</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 className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-[#0f172a] to-[#1e1b4b]">
<div className="max-w-md w-full p-8 rounded-3xl text-center bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl">
<Shield className="w-16 h-16 mx-auto mb-6 text-red-400" />
<h1 className="text-2xl font-bold mb-3 text-white">Access Denied</h1>
<p className="text-white/70">{error}</p>
</div>
</div>
);
@ -78,84 +98,188 @@ export default function PublicSharePage({ params }: { params: Promise<{ id: stri
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="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-[#0f172a] to-[#1e1b4b]">
<div className="max-w-md w-full p-8 rounded-3xl bg-white/10 backdrop-blur-xl border border-white/20 shadow-2xl">
<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 className="w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 bg-white/5 border border-white/10">
<Lock className="w-10 h-10 text-white/80" />
</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>
<h1 className="text-2xl font-bold mb-2 text-white">Protected File</h1>
<p className="text-white/60">This shared link is password protected.</p>
</div>
<form onSubmit={handlePasswordSubmit} className="space-y-4">
<form onSubmit={handlePasswordSubmit} className="space-y-6">
<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)' }}
className="w-full px-5 py-4 rounded-xl outline-none bg-black/20 border border-white/10 text-white placeholder-white/40 focus:border-white/30 transition-colors"
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)' }}
className="w-full py-4 rounded-xl text-white font-semibold transition-all hover:scale-[1.02] active:scale-[0.98] shadow-lg"
style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }}
>
Unlock
Unlock Access
</button>
</form>
</div>
</div>
);
}
const isImage = fileInfo?.mime_type?.startsWith('image/');
const isVideo = fileInfo?.mime_type?.startsWith('video/');
const isAudio = fileInfo?.mime_type?.startsWith('audio/');
const isPdf = fileInfo?.mime_type === 'application/pdf';
const downloadUrl = fileInfo ? api.getPublicDownloadUrl(id, password) : '';
const isImage = !fileInfo?.is_dir && fileInfo?.mime_type?.startsWith('image/');
const isVideo = !fileInfo?.is_dir && fileInfo?.mime_type?.startsWith('video/');
const isAudio = !fileInfo?.is_dir && fileInfo?.mime_type?.startsWith('audio/');
const isPdf = !fileInfo?.is_dir && fileInfo?.mime_type === 'application/pdf';
const downloadUrl = fileInfo ? api.getPublicDownloadUrl(id, password, currentPath) : '';
// Breadcrumbs
const pathParts = currentPath.split('/').filter(Boolean);
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>
) : isImage ? (
<img src={downloadUrl} alt={fileInfo?.name} className="w-full max-h-48 object-contain rounded-xl" />
) : isVideo ? (
<video src={downloadUrl} controls className="w-full max-h-48 rounded-xl" />
) : isAudio ? (
<audio src={downloadUrl} controls className="w-full mt-4" />
) : isPdf ? (
<iframe src={downloadUrl} className="w-full h-64 rounded-xl border-none" />
) : (
<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 className="min-h-screen p-4 md:p-8 bg-gradient-to-br from-[#0f172a] to-[#1e1b4b] text-white flex flex-col items-center">
<div className={`w-full ${fileInfo?.is_dir ? 'max-w-4xl' : 'max-w-md'} transition-all duration-500`}>
{/* Header/Card */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-3xl shadow-2xl overflow-hidden">
<div className="p-8">
{/* Top Bar for Directories */}
{fileInfo?.is_dir && (
<div className="flex items-center justify-between mb-8 pb-6 border-b border-white/10">
<div className="flex items-center gap-2 text-white/80 overflow-x-auto whitespace-nowrap scrollbar-hide">
<button onClick={() => setCurrentPath('')} className="hover:text-white transition-colors flex items-center gap-2">
<Home className="w-5 h-5" />
<span className="font-medium">Shared Folder</span>
</button>
{pathParts.map((part, idx) => {
const pathToHere = pathParts.slice(0, idx + 1).join('/');
return (
<div key={pathToHere} className="flex items-center gap-2">
<ChevronRight className="w-4 h-4 text-white/40" />
<button
onClick={() => setCurrentPath(pathToHere)}
className="hover:text-white transition-colors font-medium truncate max-w-[150px]"
>
{part}
</button>
</div>
);
})}
</div>
<button
onClick={() => handleDownload()}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl font-medium transition-all hover:bg-white/10 bg-white/5 border border-white/10 text-white shadow-lg"
>
<Download className="w-4 h-4" />
Download ZIP
</button>
</div>
)}
{!fileInfo?.is_dir ? (
// Single File View
<div className="text-center">
<div className="mb-8 flex justify-center">
{isImage ? (
<img src={downloadUrl} alt={fileInfo?.name} className="w-full max-h-64 object-contain rounded-2xl shadow-lg border border-white/10 bg-black/20" />
) : isVideo ? (
<video src={downloadUrl} controls className="w-full max-h-64 rounded-2xl shadow-lg border border-white/10 bg-black/20" />
) : isAudio ? (
<div className="w-full bg-black/20 p-6 rounded-2xl border border-white/10 shadow-lg">
<Music className="w-16 h-16 mx-auto mb-6 text-yellow-400 opacity-80" />
<audio src={downloadUrl} controls className="w-full" />
</div>
) : isPdf ? (
<iframe src={downloadUrl} className="w-full h-80 rounded-2xl border border-white/10 shadow-lg bg-white" />
) : (
<div className="w-32 h-32 rounded-3xl flex items-center justify-center bg-gradient-to-br from-indigo-500/20 to-purple-500/20 border border-white/10 shadow-inner">
<File className="w-16 h-16 text-indigo-400" />
</div>
)}
</div>
<h1 className="text-2xl font-bold mb-2 truncate px-4 text-white" title={fileInfo?.name}>
{fileInfo?.name}
</h1>
<p className="text-white/60 mb-8 font-medium">
{formatSize(fileInfo?.size || 0)}
</p>
<button
onClick={() => handleDownload()}
className="w-full flex items-center justify-center gap-2 py-4 rounded-xl text-white font-semibold transition-all hover:scale-[1.02] active:scale-[0.98] shadow-[0_0_20px_rgba(99,102,241,0.4)]"
style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }}
>
<Download className="w-5 h-5" />
Download File
</button>
</div>
) : (
// Directory Listing View
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-white/10 text-white/50 text-sm">
<th className="pb-4 font-medium pl-4">Name</th>
<th className="pb-4 font-medium">Size</th>
<th className="pb-4 font-medium text-right pr-4">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{fileInfo.files?.length === 0 ? (
<tr>
<td colSpan={3} className="py-12 text-center text-white/40">
This folder is empty.
</td>
</tr>
) : (
fileInfo.files?.map((f) => (
<tr
key={f.name}
className={`group transition-colors ${f.is_dir ? 'cursor-pointer hover:bg-white/5' : 'hover:bg-white/5'}`}
onClick={() => {
if (f.is_dir) {
setCurrentPath(currentPath ? `${currentPath}/${f.name}` : f.name);
}
}}
>
<td className="py-4 pl-4 flex items-center gap-4">
{getFileIcon(f.is_dir, f.mime_type)}
<span className="font-medium text-white/90 group-hover:text-white truncate max-w-[200px] sm:max-w-[400px]">
{f.name}
</span>
</td>
<td className="py-4 text-white/50 text-sm">
{f.is_dir ? '--' : formatSize(f.size)}
</td>
<td className="py-4 pr-4 text-right">
<button
onClick={(e) => {
e.stopPropagation();
handleDownload(f.name);
}}
className="p-2 rounded-lg bg-white/5 hover:bg-white/10 text-white/70 hover:text-white transition-colors ml-auto flex items-center justify-center"
title={`Download ${f.is_dir ? 'folder as ZIP' : 'file'}`}
>
<Download className="w-4 h-4" />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</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>
);