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 (
);
};
return (
{/* Top Banner */}
Start a new {type === 'docs' ? 'document' : type === 'slides' ? 'presentation' : 'spreadsheet'}
{/* 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()}
>
)}
);
}