Fix UI issues and add Pins and Templates
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m38s

This commit is contained in:
Elijah 2026-05-24 19:30:17 -07:00
parent a361848216
commit 617f8160b2
10 changed files with 281 additions and 59 deletions

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { Plus, Pencil, Trash2 } from 'lucide-react';
import { Plus, Pencil, Trash2, Pin } from 'lucide-react';
import api from '@/lib/api';
interface FileItem {
@ -45,6 +45,7 @@ interface ContextMenuState {
export default function OfficeHome({ type, onFileSelect, onCreateBlank }: OfficeHomeProps) {
const [recentFiles, setRecentFiles] = useState<FileItem[]>([]);
const [pinnedFiles, setPinnedFiles] = useState<FileItem[]>([]);
const [allFiles, setAllFiles] = useState<FileItem[]>([]);
const [loading, setLoading] = useState(true);
const [loadingAll, setLoadingAll] = useState(true);
@ -64,7 +65,7 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
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, 10);
.slice(0, 12);
setRecentFiles(filtered);
setLoading(false);
})
@ -73,6 +74,17 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
setLoading(false);
});
// Fetch pinned files
api.listPinned()
.then(data => {
const files: FileItem[] = data || [];
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 => {
@ -145,14 +157,34 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
setContextMenu({ x: e.clientX, y: e.clientY, file });
};
const handlePin = async (file: FileItem) => {
try {
await api.togglePin(file.path);
fetchFiles();
} catch (err) {
console.error("Failed to pin", err);
}
};
const handleRename = async () => {
if (!renaming || !renameValue.trim()) {
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, renameValue);
await api.rename(renaming, trimmed);
setRenaming(null);
fetchFiles(); // Refresh
} catch (err) {
@ -176,13 +208,15 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
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 (
<div key={file.path} className="flex flex-col gap-2 group">
<button
onClick={() => !isRenaming && onFileSelect(file)}
onContextMenu={(e) => handleContextMenu(e, file)}
className="w-full aspect-[1/1.4] bg-white rounded-lg border border-gray-200 group-hover:border-blue-400 transition-colors flex items-center justify-center shadow-sm overflow-hidden relative"
className="w-full aspect-[1/1.4] bg-white dark:bg-[#1e1e1e] rounded-lg border border-gray-200 dark:border-gray-800 group-hover:border-blue-400 dark:group-hover:border-blue-500 transition-colors flex items-center justify-center shadow-sm overflow-hidden relative"
>
<Icon className="w-16 h-16 text-3xl opacity-30 group-hover:opacity-60 transition-opacity" />
</button>
@ -207,7 +241,7 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
style={{ color: 'var(--color-text-primary)', wordBreak: 'break-word' }}
title={file.name}
>
{file.name}
{baseName}
</span>
)}
</div>
@ -225,16 +259,73 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
<div className="flex flex-col gap-3">
<button
onClick={handleCreateBlank}
className="w-40 h-52 bg-white rounded-lg border border-gray-200 hover:border-blue-500 transition-colors flex items-center justify-center shadow-sm group"
className="w-40 h-52 bg-white dark:bg-[#1e1e1e] rounded-lg border border-gray-200 dark:border-gray-800 hover:border-blue-500 transition-colors flex items-center justify-center shadow-sm group"
>
<Plus className="w-12 h-12 text-gray-300 group-hover:text-blue-500 transition-colors" />
</button>
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>Blank</span>
</div>
{type === 'docs' && (
<>
<div className="flex flex-col gap-3">
<button
onClick={async () => {
try {
const res = await api.createFromTemplate('apa.docx');
const newFile: FileItem = { name: res.name, path: res.path, is_dir: false, size: 0, mime_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', is_pinned: false, is_trashed: false, mod_time: new Date().toISOString() };
onCreateBlank(newFile);
} catch (err) { console.error("Failed to create template", err); }
}}
className="w-40 h-52 bg-white dark:bg-[#1e1e1e] rounded-lg border border-gray-200 dark:border-gray-800 hover:border-blue-500 transition-colors flex items-center justify-center shadow-sm relative overflow-hidden"
>
<div className="absolute inset-x-0 top-0 h-16 bg-blue-50 border-b border-blue-100 flex items-center px-4">
<div className="w-1/2 h-2 bg-blue-200 rounded"></div>
</div>
<div className="w-full h-full pt-20 px-4 flex flex-col gap-2">
<div className="w-full h-1.5 bg-gray-100 rounded"></div>
<div className="w-full h-1.5 bg-gray-100 rounded"></div>
<div className="w-3/4 h-1.5 bg-gray-100 rounded"></div>
</div>
</button>
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>APA Paper</span>
</div>
<div className="flex flex-col gap-3">
<button
onClick={async () => {
try {
const res = await api.createFromTemplate('resume.docx');
const newFile: FileItem = { name: res.name, path: res.path, is_dir: false, size: 0, mime_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', is_pinned: false, is_trashed: false, mod_time: new Date().toISOString() };
onCreateBlank(newFile);
} catch (err) { console.error("Failed to create template", err); }
}}
className="w-40 h-52 bg-white dark:bg-[#1e1e1e] rounded-lg border border-gray-200 dark:border-gray-800 hover:border-blue-500 transition-colors flex flex-col items-center justify-center shadow-sm relative overflow-hidden p-4"
>
<div className="w-12 h-12 bg-gray-200 rounded-full mb-4"></div>
<div className="w-full h-1.5 bg-gray-200 rounded mb-2"></div>
<div className="w-2/3 h-1.5 bg-gray-200 rounded mb-4"></div>
<div className="w-full h-1.5 bg-gray-100 rounded mb-1"></div>
<div className="w-full h-1.5 bg-gray-100 rounded mb-1"></div>
</button>
<span className="text-sm font-medium" style={{ color: 'var(--color-text-primary)' }}>Resume</span>
</div>
</>
)}
</div>
</div>
</div>
{/* Pinned Documents */}
{pinnedFiles.length > 0 && (
<div className="py-8 px-10 border-b border-gray-100 dark:border-gray-800">
<div className="max-w-6xl mx-auto">
<h2 className="text-lg font-medium mb-6" style={{ color: 'var(--color-text-primary)' }}>Pinned {title.toLowerCase()}</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{pinnedFiles.map(file => renderFileCard(file))}
</div>
</div>
</div>
)}
{/* Recent Documents */}
<div className="py-8 px-10">
<div className="max-w-6xl mx-auto">
@ -285,8 +376,21 @@ export default function OfficeHome({ type, onFileSelect, onCreateBlank }: Office
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-black/5 transition-colors"
style={{ color: 'var(--color-text-primary)' }}
onClick={() => {
handlePin(contextMenu.file);
setContextMenu(null);
}}
>
<Pin className="w-4 h-4" style={{ color: 'var(--color-text-secondary)' }} />
{contextMenu.file.is_pinned || pinnedFiles.find(f => f.path === contextMenu.file.path) ? 'Unpin' : 'Pin'}
</button>
<button
className="w-full px-4 py-2 text-left text-sm flex items-center gap-3 hover:bg-black/5 transition-colors"
style={{ color: 'var(--color-text-primary)' }}
onClick={() => {
const extMatch = contextMenu.file.name.match(/\.[^.]+$/);
const baseName = extMatch ? contextMenu.file.name.slice(0, -extMatch[0].length) : contextMenu.file.name;
setRenaming(contextMenu.file.path);
setRenameValue(contextMenu.file.name);
setRenameValue(baseName);
setContextMenu(null);
}}
>