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'; 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; } interface OfficeHomeProps { type: 'docs' | 'slides' | 'sheets'; onFileSelect: (file: FileItem) => void; onCreateBlank: (file: FileItem) => void; onPinChange?: () => void; } // Custom Word icon (blue) function WordIcon({ className }: { className?: string }) { return (
); } // Custom PowerPoint icon (orange) function PptIcon({ className }: { className?: string }) { return (
); } // Custom Excel icon (green) function SheetIcon({ className }: { className?: string }) { return (
); } interface ContextMenuState { x: number; y: number; file: FileItem; } export default function OfficeHome({ type, onFileSelect, onCreateBlank, onPinChange }: OfficeHomeProps) { const [recentFiles, setRecentFiles] = useState([]); const [pinnedFiles, setPinnedFiles] = useState([]); const [allFiles, setAllFiles] = useState([]); const [loading, setLoading] = useState(true); const [loadingAll, setLoadingAll] = useState(true); const [contextMenu, setContextMenu] = useState(null); const [renaming, setRenaming] = useState(null); // path of file being renamed const [renameValue, setRenameValue] = useState(''); const renameInputRef = useRef(null); const [downloadToken, setDownloadToken] = useState(''); const ext = type === 'docs' ? '.docx' : type === 'slides' ? '.pptx' : '.xlsx'; const fileTypeParam = type === 'docs' ? 'docx' : type === 'slides' ? 'pptx' : 'xlsx'; const fetchFiles = () => { // Fetch recent files from root directory api.listDirectory('.') .then(data => { const files: FileItem[] = data.files || []; const filtered = files .filter(f => !f.is_dir && f.name.endsWith(ext) && !f.is_trashed) .sort((a, b) => new Date(b.mod_time).getTime() - new Date(a.mod_time).getTime()) .slice(0, 12); setRecentFiles(filtered); setLoading(false); }) .catch(err => { console.error(err); setLoading(false); }); // Fetch pinned files api.listPinned() .then(data => { const files: FileItem[] = data.items || []; const filtered = files .filter(f => f.name.endsWith(ext) && !f.is_trashed) .sort((a, b) => new Date(b.mod_time).getTime() - new Date(a.mod_time).getTime()); setPinnedFiles(filtered); }) .catch(console.error); // Fetch ALL office files recursively api.listOfficeFiles(fileTypeParam) .then(data => { const files = (data.files || []).map((f: any) => ({ ...f, is_dir: false, mime_type: '', is_pinned: false, is_trashed: false, })); // Sort by mod_time descending files.sort((a: FileItem, b: FileItem) => new Date(b.mod_time).getTime() - new Date(a.mod_time).getTime()); setAllFiles(files); setLoadingAll(false); }) .catch(err => { console.error(err); setLoadingAll(false); }); }; useEffect(() => { fetchFiles(); api.createDownloadToken().then(setDownloadToken).catch(console.error); // Refresh files periodically to catch any debounced thumbnail updates from recent edits const timer = setInterval(() => { fetchFiles(); }, 15000); return () => clearInterval(timer); }, [type]); // Close context menu on click elsewhere useEffect(() => { const handleClick = () => setContextMenu(null); if (contextMenu) { window.addEventListener('click', handleClick); return () => window.removeEventListener('click', handleClick); } }, [contextMenu]); // Focus rename input useEffect(() => { if (renaming && renameInputRef.current) { renameInputRef.current.focus(); // Select the name without extension const dotIdx = renameValue.lastIndexOf('.'); renameInputRef.current.setSelectionRange(0, dotIdx > 0 ? dotIdx : renameValue.length); } }, [renaming]); const handleCreateBlank = async () => { try { const res = await api.createOnlyOfficeFile(type === 'docs' ? 'word' : type === 'slides' ? 'slide' : 'cell'); const { path, name } = res; const newFile: FileItem = { name, path, is_dir: false, size: 0, mime_type: type === 'docs' ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' : type === 'slides' ? 'application/vnd.openxmlformats-officedocument.presentationml.presentation' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', is_pinned: false, is_trashed: false, mod_time: new Date().toISOString() }; onCreateBlank(newFile); } catch (err) { console.error("Failed to create document", err); } }; const handleContextMenu = (e: React.MouseEvent, file: FileItem) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY, file }); }; const handlePin = async (file: FileItem) => { try { await api.togglePin(file.path); fetchFiles(); if (onPinChange) onPinChange(); } catch (err) { console.error("Failed to pin", err); } }; const handleRename = async () => { let trimmed = renameValue.trim(); if (!renaming || !trimmed) { setRenaming(null); return; } // Determine the current file's extension to append const currentFile = recentFiles.find(f => f.path === renaming) || allFiles.find(f => f.path === renaming); if (currentFile) { const extMatch = currentFile.name.match(/\.[^.]+$/); const currentExt = extMatch ? extMatch[0] : ''; if (!trimmed.toLowerCase().endsWith(currentExt.toLowerCase())) { trimmed += currentExt; } } try { await api.rename(renaming, trimmed); setRenaming(null); fetchFiles(); // Refresh } catch (err) { console.error("Failed to rename", err); setRenaming(null); } }; const handleDelete = async (file: FileItem) => { try { await api.deleteFile(file.path); fetchFiles(); // Refresh } catch (err) { console.error("Failed to delete", err); } }; const title = type === 'docs' ? 'Documents' : type === 'slides' ? 'Presentations' : 'Spreadsheets'; const Icon = type === 'docs' ? WordIcon : type === 'slides' ? PptIcon : SheetIcon; const accentColor = type === 'docs' ? '#2B579A' : type === 'slides' ? '#D24726' : '#107C41'; const renderFileCard = (file: FileItem) => { const isRenaming = renaming === file.path; const extMatch = file.name.match(/\.[^.]+$/); const baseName = extMatch ? file.name.slice(0, -extMatch[0].length) : file.name; return (
{isRenaming ? ( setRenameValue(e.target.value)} onBlur={handleRename} onKeyDown={(e) => { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') setRenaming(null); }} className="text-sm font-medium w-full bg-transparent border-b border-blue-500 outline-none" style={{ color: 'var(--color-text-primary)' }} /> ) : ( {baseName} )}
); }; return (
{/* Top Banner */}

Start a new {type === 'docs' ? 'document' : type === 'slides' ? 'presentation' : 'spreadsheet'}

Blank
{/* Recent Documents */}

Recent {title.toLowerCase()}

{loading ? (
Loading...
) : recentFiles.length === 0 ? (
No recent {title.toLowerCase()} found.
) : (
{recentFiles.map(file => renderFileCard(file))}
)}
{/* All Documents */}

All {title.toLowerCase()}

{loadingAll ? (
Loading...
) : allFiles.length === 0 ? (
No {title.toLowerCase()} found in your drive.
) : (
{allFiles.map(file => renderFileCard(file))}
)}
{/* Context Menu */} {contextMenu && (
e.stopPropagation()} >
)}
); }