fix(frontend): update Share interface property from download_count to downloads
All checks were successful
Automated Container Build / build-and-push (push) Successful in 3m25s

This commit is contained in:
Elijah 2026-05-26 14:54:50 -07:00
parent ed3eed6bb7
commit 1f83d57942
21 changed files with 241 additions and 137 deletions

5
.gitignore vendored
View file

@ -1,3 +1,8 @@
# OS Files
.DS_Store
Thumbs.db
desktop.ini
# IDE & Editor files
.vscode/
.cursor/

View file

@ -1,7 +0,0 @@
package main
import "fmt"
func main() {
fmt.Println("Running...")
}

View file

@ -722,8 +722,6 @@ func (h *FSHandler) RestoreFromTrash(c *fiber.Ctx) error {
)
}
h.DB.AddAuditLog("restored", originalPath, c.IP())
h.DB.AddAuditLog("restored", fmt.Sprintf("Restored '%s' from trash", originalPath), c.IP())
return c.JSON(fiber.Map{"message": "restored successfully", "path": originalPath})
@ -1024,10 +1022,6 @@ func (h *FSHandler) Download(c *fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "file not found"})
}
// Verify checksum on download if stored
relativePath := c.Locals("relativePath").(string)
var storedChecksum string
h.DB.QueryRow("SELECT checksum FROM files WHERE path = ?", filepath.ToSlash(relativePath)).Scan(&storedChecksum)
// Set appropriate headers for range requests
c.Set("Accept-Ranges", "bytes")

View file

@ -1,9 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [],
theme: {
extend: {},
},
plugins: [],
}

View file

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,32 @@
'use client';
import { useEffect } from 'react';
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="flex h-screen w-screen items-center justify-center bg-[#0a0a0a] text-white">
<div className="text-center p-8 border border-white/10 rounded-2xl bg-black/50">
<h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
<p className="text-white/60 mb-8 max-w-md mx-auto">
We encountered an unexpected error. You can try recovering by clicking the button below.
</p>
<button
onClick={() => reset()}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium transition-colors"
>
Try again
</button>
</div>
</div>
);
}

View file

@ -0,0 +1,21 @@
import Link from 'next/link';
export default function NotFound() {
return (
<div className="flex h-screen w-screen items-center justify-center bg-[#0a0a0a] text-white">
<div className="text-center">
<h1 className="text-6xl font-bold mb-4">404</h1>
<h2 className="text-2xl font-semibold mb-6">Page Not Found</h2>
<p className="text-white/60 mb-8 max-w-md mx-auto">
The page you are looking for doesn't exist or has been moved.
</p>
<Link
href="/"
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium transition-colors"
>
Return Home
</Link>
</div>
</div>
);
}

View file

@ -10,6 +10,8 @@ import {
Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight,
CheckCircle, AlertCircle, Menu, FileText, LayoutTemplate, FileSpreadsheet
} from 'lucide-react';
import { FileItem } from '@/types/files';
import { formatSize } from '@/lib/utils';
import api from '@/lib/api';
import { useIsMobile } from '@/hooks/useIsMobile';
const Folder = ({ className, style }: { className?: string, style?: React.CSSProperties }) => (
@ -30,18 +32,7 @@ import OfficeHome from './OfficeHome';
import OnlyOfficeEditor from './OnlyOfficeEditor';
import ThumbnailImage from './ThumbnailImage';
interface FileItem {
name: string;
path: string;
is_dir: boolean;
size: number;
mime_type: string;
is_pinned: boolean;
is_trashed: boolean;
mod_time: string;
trashed_at?: string;
checksum?: string;
}
// FileItem imported from types
interface FileExplorerProps {
onLogout: () => void;
@ -427,7 +418,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [files, selectedFiles]);
}, [files, selectedFiles, currentPath]);
useEffect(() => {
const handleClickOutside = () => {
@ -556,9 +547,10 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
if (target.closest('button') || target.closest('input') || target.closest('[data-file-item]')) {
return;
}
const rect = mainContainerRef.current!.getBoundingClientRect();
const scrollLeft = mainContainerRef.current!.scrollLeft;
const scrollTop = mainContainerRef.current!.scrollTop;
if (!mainContainerRef.current) return;
const rect = mainContainerRef.current.getBoundingClientRect();
const scrollLeft = mainContainerRef.current.scrollLeft;
const scrollTop = mainContainerRef.current.scrollTop;
setSelectionBox({
startX: e.clientX - rect.left + scrollLeft,
@ -1146,13 +1138,6 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const pathSegments = currentPath === '.' ? [] : currentPath.split('/');
function formatSize(bytes: number): string {
if (bytes === 0) return '—';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
function formatDate(dateStr: string): string {
if (!dateStr) return '';
const date = new Date(dateStr);

View file

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { X, File, Folder, HardDrive, Calendar, RefreshCw, FileType, Info } from 'lucide-react';
import api from '@/lib/api';
import { formatSize } from '@/lib/utils';
interface FileInfoModalProps {
filePath: string;
@ -48,12 +49,7 @@ export default function FileInfoModal({ filePath, onClose }: FileInfoModalProps)
});
}, [filePath]);
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(i === 0 ? 0 : 1)} ${units[i]}`;
}
// formatSize imported from utils
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" onClick={onClose}>

View file

@ -7,6 +7,7 @@ import api from '@/lib/api';
import dynamic from 'next/dynamic';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { formatSize, getFileDisplayName } from '@/lib/utils';
const DynamicPdfViewer = dynamic(() => import('./PdfViewer'), { ssr: false });
@ -21,11 +22,7 @@ interface FilePreviewProps {
downloadToken?: string;
}
function getFileDisplayName(name: string) {
const dotIndex = name.lastIndexOf('.');
if (dotIndex > 0) return name.substring(0, dotIndex);
return name;
}
// getFileDisplayName imported from utils
export default function FilePreview({ file, onClose, downloadToken }: FilePreviewProps) {
const [content, setContent] = useState<string | null>(null);
@ -264,11 +261,6 @@ function getIcon(type: PreviewType) {
}
}
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]}`;
}
// formatSize imported from utils

View file

@ -2,18 +2,9 @@ import React, { useState, useEffect, useRef } from 'react';
import { Plus, Pencil, Trash2, Pin, FileText, LayoutTemplate, FileSpreadsheet } from 'lucide-react';
import api from '@/lib/api';
import ThumbnailImage from './ThumbnailImage';
import { FileItem } from '@/types/files';
interface FileItem {
name: string;
path: string;
is_dir: boolean;
size: number;
mime_type: string;
is_pinned: boolean;
is_trashed: boolean;
mod_time: string;
checksum?: string;
}
// FileItem imported from types
interface OfficeHomeProps {
type: 'docs' | 'slides' | 'sheets';
@ -64,6 +55,7 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinCha
const [renameValue, setRenameValue] = useState('');
const renameInputRef = useRef<HTMLInputElement>(null);
const [downloadToken, setDownloadToken] = useState('');
const [toast, setToast] = useState<{message: string, type: 'error' | 'success'} | null>(null);
const ext = type === 'docs' ? '.docx' : type === 'slides' ? '.pptx' : '.xlsx';
const fileTypeParam = type === 'docs' ? 'docx' : type === 'slides' ? 'pptx' : 'xlsx';
@ -208,8 +200,10 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinCha
await api.rename(renaming, trimmed);
setRenaming(null);
fetchFiles(); // Refresh
} catch (err) {
} catch (err: any) {
console.error("Failed to rename", err);
setToast({ message: err.message || "Failed to rename file", type: "error" });
setTimeout(() => setToast(null), 3000);
setRenaming(null);
}
};
@ -384,6 +378,17 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinCha
</button>
</div>
)}
{/* Toast Notification */}
{toast && (
<div className="fixed bottom-4 right-4 z-[200]">
<div className={`px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 ${
toast.type === 'error' ? 'bg-red-500 text-white' : 'bg-green-500 text-white'
}`}>
<span className="font-medium text-sm">{toast.message}</span>
</div>
</div>
)}
</div>
);
}

View file

@ -1,18 +1,8 @@
import React, { useEffect, useState, useRef } from 'react';
import { DocumentEditor } from '@onlyoffice/document-editor-react';
import { Sun, Moon } from 'lucide-react';
import { Sun, Moon, Loader2, ArrowLeft, RefreshCw, Download } from 'lucide-react';
import api from '@/lib/api';
interface FileItem {
name: string;
path: string;
is_dir: boolean;
size: number;
mime_type: string;
is_pinned: boolean;
is_trashed: boolean;
mod_time: string;
}
import { FileItem } from '@/types/files';
interface OnlyOfficeEditorProps {
file: FileItem;

View file

@ -41,11 +41,22 @@ export default function ShareModal({ filePath, onClose }: ShareModalProps) {
}
}
function handleCopy() {
async function handleCopy() {
if (!shareLink) return;
navigator.clipboard.writeText(shareLink);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
try {
await navigator.clipboard.writeText(shareLink);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
const textArea = document.createElement('textarea');
textArea.value = shareLink;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}
return (

View file

@ -5,8 +5,18 @@ import { motion } from 'framer-motion';
import { Link as LinkIcon, Trash2, Calendar, Download, Shield, Copy, Check, RefreshCw } from 'lucide-react';
import api from '@/lib/api';
interface Share {
id: string;
path: string;
has_password: boolean;
expires_at: string | null;
max_downloads: number;
downloads: number;
created_at: string;
}
export default function SharedFilesPage() {
const [shares, setShares] = useState<any[]>([]);
const [shares, setShares] = useState<Share[]>([]);
const [loading, setLoading] = useState(true);
const [copiedId, setCopiedId] = useState<string | null>(null);
const [shareToRevoke, setShareToRevoke] = useState<string | null>(null);
@ -42,11 +52,22 @@ export default function SharedFilesPage() {
}
}
function handleCopy(id: string) {
async function handleCopy(id: string) {
const url = `${window.location.origin}/share/${id}`;
navigator.clipboard.writeText(url);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
try {
await navigator.clipboard.writeText(url);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
} catch {
const textArea = document.createElement('textarea');
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
}
}
if (loading) {
@ -68,8 +89,8 @@ export default function SharedFilesPage() {
<p className="text-sm font-medium" style={{ color: 'var(--color-text-secondary)' }}>You don't have any active share links.</p>
</div>
) : (
<div className="rounded-2xl border overflow-hidden" style={{ backgroundColor: 'var(--color-bg-elevated)', borderColor: 'var(--color-border)' }}>
<table className="w-full text-sm text-left">
<div className="rounded-2xl border overflow-x-auto" style={{ backgroundColor: 'var(--color-bg-elevated)', borderColor: 'var(--color-border)' }}>
<table className="w-full text-sm text-left whitespace-nowrap">
<thead className="border-b" style={{ borderColor: 'var(--color-border-subtle)', backgroundColor: 'var(--color-bg-secondary)' }}>
<tr>
<th className="px-6 py-4 font-semibold" style={{ color: 'var(--color-text-primary)' }}>File Path</th>

View file

@ -4,6 +4,7 @@ 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';
import { formatSize } from '@/lib/utils';
interface CategoryUsage {
category: string;
@ -61,8 +62,6 @@ export default function StorageDashboard() {
other: { label: 'Other', color: '#9ca3af', icon: File },
};
const totalDriveSize = 100 * 1024 * 1024 * 1024; // Dummy 100GB capacity for visualization, could be dynamic
const sortedCategories = [...data.categories].sort((a, b) => b.size - a.size);
return (
@ -139,9 +138,4 @@ export default function StorageDashboard() {
);
}
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]}`;
}
// formatSize imported from utils

View file

@ -21,7 +21,7 @@ export default function TaskManager() {
const [visible, setVisible] = useState(false);
useEffect(() => {
let interval: NodeJS.Timeout;
let interval: NodeJS.Timeout | null = null;
async function pollTasks() {
try {
@ -29,22 +29,39 @@ export default function TaskManager() {
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);
if (interval) {
clearInterval(interval);
interval = null;
}
}
} catch {
// silently fail polling
}
}
// Always fetch once on mount/update
pollTasks();
interval = setInterval(pollTasks, 2000); // Poll every 2 seconds
return () => clearInterval(interval);
}, []);
const hasRunningTasks = tasks.some(t => t.status === 'running' || t.status === 'pending');
if (hasRunningTasks) {
interval = setInterval(pollTasks, 2000);
}
// Listen for activity that might spawn tasks
const handleActivity = () => {
if (!hasRunningTasks) pollTasks();
};
window.addEventListener('api_activity', handleActivity);
return () => {
if (interval) clearInterval(interval);
window.removeEventListener('api_activity', handleActivity);
};
}, [tasks.some(t => t.status === 'running' || t.status === 'pending')]);
if (!visible || tasks.length === 0) return null;
@ -58,9 +75,8 @@ export default function TaskManager() {
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"
<div
className="flex items-center justify-between px-4 py-3 border-b transition-colors"
style={{ borderColor: 'var(--color-border-subtle)', backgroundColor: 'var(--color-bg-secondary)' }}
>
<div className="flex items-center gap-2">
@ -74,15 +90,22 @@ export default function TaskManager() {
</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); }}
<button
onClick={() => setExpanded(!expanded)}
className="p-1 rounded hover:bg-white/10"
aria-label={expanded ? "Collapse tasks" : "Expand tasks"}
>
{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)' }} />}
</button>
<button
onClick={() => setVisible(false)}
className="p-1 rounded hover:bg-white/10"
aria-label="Close task manager"
>
<X className="w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
</div>
</button>
</div>
</button>
</div>
{/* Task List */}
<AnimatePresence>

View file

@ -2,23 +2,20 @@ import { useState, useEffect } from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
export function useIsMobile(breakpoint = 768) {
const [isMobile, setIsMobile] = useState<boolean>(false);
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
// Initial check
const checkIsMobile = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
checkIsMobile();
setHasMounted(true);
const mql = window.matchMedia(`(max-width: ${breakpoint}px)`);
setIsMobile(mql.matches);
// Add resize listener
window.addEventListener('resize', checkIsMobile);
// Cleanup
return () => window.removeEventListener('resize', checkIsMobile);
}, []);
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, [breakpoint]);
if (!hasMounted) return false;
return isMobile;
}

View file

@ -91,6 +91,12 @@ class ApiClient {
} catch {}
throw new Error(errorMsg);
}
// Notify TaskManager to poll if a task might have been created
if (typeof window !== 'undefined' && endpoint !== '/api/tasks' && endpoint !== '/api/auth/refresh') {
window.dispatchEvent(new Event('api_activity'));
}
return response;
}

11
frontend/src/lib/utils.ts Normal file
View file

@ -0,0 +1,11 @@
export function formatSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
export function getFileDisplayName(path: string): string {
return path.split('/').pop() || path;
}

View file

@ -0,0 +1,12 @@
export interface FileItem {
name: string;
path: string;
is_dir: boolean;
size: number;
mime_type: string;
is_pinned: boolean;
is_trashed: boolean;
trashed_at?: string;
mod_time: string;
checksum?: string;
}