musealpha / src /components /StarterHub.tsx
asdf98's picture
Upload 112 files
3d7d9b5 verified
import { Plus, FolderOpen, Layout, Layers, Settings, Trash2, Minus, Square, X } from 'lucide-react';
import { useAppStore } from '../store';
import { useRef, useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
interface ProjectEntry { id: string; title: string; element_count: number; saved_at: number; }
export const StarterHub = () => {
const { setCurrentScreen, setIsSettingsOpen, setImages, setTextNotes, setAnnotations, setPalettes, setZoom, setPan, setActiveProjectId, setBoardTitle } = useAppStore();
const [projects, setProjects] = useState<ProjectEntry[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
const loadProjects = () => invoke<ProjectEntry[]>('projects_list').then(setProjects).catch(() => {});
useEffect(() => { loadProjects(); }, []);
const handleOpenFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const d = JSON.parse(ev.target?.result as string);
if (d.images) setImages(d.images);
if (d.textNotes) setTextNotes(d.textNotes);
if (d.annotations) setAnnotations(d.annotations);
if (d.palettes) setPalettes(d.palettes);
if (d.zoom) setZoom(d.zoom);
if (d.pan) setPan(d.pan);
if (d.title) setBoardTitle(d.title);
setCurrentScreen('board');
} catch {}
};
reader.readAsText(file);
if (e.target) e.target.value = '';
};
const handleNewBoard = async () => {
try {
const entry = await invoke<any>('project_create', { title: null });
setActiveProjectId(entry.id);
setBoardTitle(entry.title);
} catch {}
setImages([]); setTextNotes([]); setAnnotations([]); setPalettes([]);
setZoom(1); setPan({ x: 0, y: 0 });
setCurrentScreen('board');
};
const handleOpenProject = async (project: ProjectEntry) => {
try {
const json = await invoke<string>('project_load', { id: project.id });
const d = JSON.parse(json);
setImages(d.images || []); setTextNotes(d.textNotes || []); setAnnotations(d.annotations || []); setPalettes(d.palettes || []);
setZoom(d.zoom || 1); setPan(d.pan || { x: 0, y: 0 });
setBoardTitle(d.title || project.title);
setActiveProjectId(project.id);
} catch {
setImages([]); setTextNotes([]); setAnnotations([]); setPalettes([]); setZoom(1); setPan({ x: 0, y: 0 });
setBoardTitle(project.title); setActiveProjectId(project.id);
}
setCurrentScreen('board');
};
const handleDeleteProject = async (id: string, e: React.MouseEvent) => {
e.stopPropagation();
try { await invoke('project_delete', { id }); } catch {}
loadProjects();
};
const timeAgo = (ts: number) => {
const diff = Math.floor(Date.now() / 1000) - ts;
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
return new Date(ts * 1000).toLocaleDateString();
};
return (
<div className="w-screen h-screen bg-[#0A0A0B] flex flex-col font-sans text-white select-none overflow-hidden">
{/* Top bar */}
<div className="w-full h-12 flex items-center justify-between px-5 bg-[#0A0A0B] shrink-0 z-10 border-b border-white/[0.04]" data-tauri-drag-region>
{/* Left: Logo */}
<div className="flex items-center gap-2.5">
<div className="w-[22px] h-[22px] bg-white rounded-[5px] flex items-center justify-center text-black">
<Layout size={11} />
</div>
<span className="font-semibold text-[12px] text-white/80 tracking-tight">Refstudio</span>
</div>
{/* Right: Settings + Window controls */}
<div className="flex items-center gap-1">
<button onClick={() => setIsSettingsOpen(true)} className="w-7 h-7 rounded-md flex items-center justify-center text-white/30 hover:text-white/70 hover:bg-white/5 transition-colors" title="Settings">
<Settings size={14} />
</button>
<div className="w-px h-4 bg-white/[0.06] mx-1" />
<button onClick={() => appWindow.minimize()} className="w-7 h-7 rounded-md flex items-center justify-center text-white/30 hover:text-white/70 hover:bg-white/5 transition-colors">
<Minus size={14} />
</button>
<button onClick={() => appWindow.toggleMaximize()} className="w-7 h-7 rounded-md flex items-center justify-center text-white/30 hover:text-white/70 hover:bg-white/5 transition-colors">
<Square size={11} />
</button>
<button onClick={() => appWindow.close()} className="w-7 h-7 rounded-md flex items-center justify-center text-white/30 hover:text-[#FF453A] hover:bg-[#FF453A]/10 transition-colors">
<X size={14} />
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 flex flex-col items-center pt-14 pb-12 px-8 overflow-y-auto">
{/* Action buttons */}
<div className="flex gap-3 mb-10 w-full max-w-md">
<button onClick={handleNewBoard} className="flex-1 h-[72px] rounded-xl bg-[#141415] border border-white/[0.06] hover:border-[#0A84FF]/30 flex items-center gap-3 px-5 transition-all group">
<div className="w-8 h-8 rounded-full bg-white/[0.04] group-hover:bg-[#0A84FF] flex items-center justify-center text-white/50 group-hover:text-white transition-colors shrink-0">
<Plus size={15} />
</div>
<span className="text-[13px] text-white/70 font-medium">New Board</span>
</button>
<button onClick={() => fileInputRef.current?.click()} className="flex-1 h-[72px] rounded-xl bg-[#141415] border border-white/[0.06] hover:border-white/15 flex items-center gap-3 px-5 transition-all group">
<div className="w-8 h-8 rounded-full bg-white/[0.04] group-hover:bg-white group-hover:text-black flex items-center justify-center text-white/50 transition-colors shrink-0">
<FolderOpen size={15} />
</div>
<span className="text-[13px] text-white/70 font-medium">Open File</span>
</button>
<input type="file" accept=".json" className="hidden" ref={fileInputRef} onChange={handleOpenFile} />
</div>
{/* Recent projects */}
<div className="w-full max-w-md">
<div className="flex items-center justify-between mb-3 px-1">
<span className="text-[11px] font-medium text-white/25 uppercase tracking-wider">Recent</span>
</div>
{projects.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-white/20">
<Layers size={22} className="mb-2.5 opacity-50" />
<p className="text-[12px] font-medium">No projects yet</p>
</div>
) : (
<div className="flex flex-col gap-1">
{projects.map(p => (
<button key={p.id} onClick={() => handleOpenProject(p)} className="group w-full flex items-center gap-3 px-3.5 py-3 rounded-lg hover:bg-white/[0.03] transition-colors text-left">
<div className="w-8 h-8 rounded-lg bg-white/[0.04] flex items-center justify-center text-white/20 shrink-0">
<Layers size={13} />
</div>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-medium text-white/80 truncate">{p.title}</div>
<div className="text-[10px] text-white/25 mt-0.5">{p.element_count} elements · {timeAgo(p.saved_at)}</div>
</div>
<button onClick={(e) => handleDeleteProject(p.id, e)} className="w-6 h-6 rounded flex items-center justify-center text-white/10 hover:text-[#FF453A] hover:bg-[#FF453A]/10 opacity-0 group-hover:opacity-100 transition-all shrink-0">
<Trash2 size={12} />
</button>
</button>
))}
</div>
)}
</div>
</div>
</div>
);
};