feat(App): add Profile and AboutMeatloaf pages with navigation integration
This commit is contained in:
parent
a276fe20a9
commit
4b2456859f
|
|
@ -1,5 +1,5 @@
|
|||
import { lazy, Suspense, useEffect, useState } from 'react';
|
||||
import { Cpu, Settings, Wifi, Network, HardDrive, Activity, Search, Wrench, User, LogOut, Bell, FileText, AppWindow, Folder, Edit, Eye, Database, Upload, Download, Code2, LayoutList, Image, ChevronLeft, Loader2, Terminal, Link, Printer, Maximize2, Minimize2, Info } from 'lucide-react';
|
||||
import { Cpu, Wifi, Network, HardDrive, Activity, Search, Wrench, User, FileText, AppWindow, Folder, Edit, Eye, Database, Upload, Download, Code2, LayoutList, Image, ChevronLeft, Loader2, Terminal, Link, Printer, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { Toaster, toast } from 'sonner';
|
||||
import StatusPage from './components/StatusPage';
|
||||
import DevicesPage from './components/DevicesPage';
|
||||
|
|
@ -10,6 +10,8 @@ import ToolsPage from './components/ToolsPage';
|
|||
import SearchOverlay from './components/SearchOverlay';
|
||||
import RealityOverrideAdminPage from './components/RealityOverrideAdminPage';
|
||||
import MediaManager from './components/MediaManager';
|
||||
import ProfilePage from './components/ProfilePage';
|
||||
import AboutMeatloafPage from './components/AboutMeatloafPage';
|
||||
import logoSvg from '../imports/logo.svg';
|
||||
import { useSettings } from './settings';
|
||||
import { WsProvider } from './ws';
|
||||
|
|
@ -22,7 +24,7 @@ import { LazyLoader } from './components/ui/lazy-loader';
|
|||
const RealityOverridePage = lazy(() => import('./components/RealityOverridePage'));
|
||||
const SerialConsolePage = lazy(() => import('./components/SerialConsolePage'));
|
||||
|
||||
type Page = 'status' | 'devices' | 'iec' | 'network' | 'general' | 'tools' | 'apps' | AppId;
|
||||
type Page = 'status' | 'devices' | 'iec' | 'network' | 'general' | 'tools' | 'apps' | 'profile' | 'about-meatloaf' | AppId;
|
||||
|
||||
type AppId =
|
||||
| 'file-manager'
|
||||
|
|
@ -53,7 +55,6 @@ export default function App() {
|
|||
const [currentPage, setCurrentPage] = useState<Page>('status');
|
||||
const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings();
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||
const [devicesOpenId, setDevicesOpenId] = useState<string | null>(null);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
|
|
@ -104,6 +105,8 @@ export default function App() {
|
|||
network: <NetworkPage config={config} setConfig={setConfig} />,
|
||||
general: <GeneralPage config={config} setConfig={setConfig} />,
|
||||
tools: <ToolsPage config={config} setConfig={setConfig} />,
|
||||
profile: <ProfilePage onNavigate={p => setCurrentPage(p as Page)} />,
|
||||
'about-meatloaf': <AboutMeatloafPage />,
|
||||
apps: (
|
||||
<div className="max-w-3xl mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center">Apps</h1>
|
||||
|
|
@ -255,54 +258,13 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
|||
>
|
||||
<AppWindow className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
||||
onClick={() => setCurrentPage('profile')}
|
||||
className="p-2 hover:bg-[#5e5e5e] rounded-lg"
|
||||
aria-label="Profile"
|
||||
>
|
||||
<User className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
{showProfileMenu && (
|
||||
<div className="absolute right-0 top-12 bg-white rounded-lg shadow-lg border border-neutral-200 py-2 min-w-[200px] z-20">
|
||||
<button
|
||||
onClick={() => { setShowProfileMenu(false); setCurrentPage('general'); }}
|
||||
className="w-full px-4 py-2 text-left hover:bg-neutral-50 flex items-center gap-2"
|
||||
>
|
||||
<Settings className="w-4 h-4 text-[#4d4d4d]" />
|
||||
Preferences
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowProfileMenu(false)}
|
||||
className="w-full px-4 py-2 text-left hover:bg-neutral-50 flex items-center gap-2"
|
||||
>
|
||||
<Bell className="w-4 h-4 text-[#4d4d4d]" />
|
||||
Notifications
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowProfileMenu(false)}
|
||||
className="w-full px-4 py-2 text-left hover:bg-neutral-50 flex items-center gap-2"
|
||||
>
|
||||
<FileText className="w-4 h-4 text-[#4d4d4d]" />
|
||||
Documentation
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowProfileMenu(false)}
|
||||
className="w-full px-4 py-2 text-left hover:bg-neutral-50 flex items-center gap-2"
|
||||
>
|
||||
<Info className="w-4 h-4 text-[#4d4d4d]" />
|
||||
About Meatloaf
|
||||
</button>
|
||||
<div className="border-t border-neutral-200 my-2" />
|
||||
<button
|
||||
onClick={() => setShowProfileMenu(false)}
|
||||
className="w-full px-4 py-2 text-left hover:bg-neutral-50 flex items-center gap-2 text-red-600"
|
||||
>
|
||||
<LogOut className="w-4 h-4 text-[#4d4d4d]" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
33
src/app/components/AboutMeatloafPage.tsx
Normal file
33
src/app/components/AboutMeatloafPage.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import logoSvg from '../../imports/logo.svg';
|
||||
|
||||
export default function AboutMeatloafPage() {
|
||||
return (
|
||||
<div className="max-w-lg mx-auto py-12 px-4 flex flex-col items-center gap-6">
|
||||
<img src={logoSvg} alt="Meatloaf" className="w-24 h-24" />
|
||||
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold">Meatloaf</h1>
|
||||
<p className="text-neutral-500 text-sm mt-1">Multi-device Commodore 64 Emulator</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full rounded-xl overflow-hidden border border-neutral-200 divide-y divide-neutral-200 text-sm">
|
||||
{[
|
||||
['Project', 'Meatloaf Manipulator'],
|
||||
['Platform', 'Commodore 64 / C128'],
|
||||
['Website', 'meatloaf.cc'],
|
||||
['GitHub', 'github.com/idolpx/meatloaf'],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="flex items-center px-4 py-3 bg-white gap-3">
|
||||
<span className="text-neutral-500 w-24 flex-shrink-0">{label}</span>
|
||||
<span className="font-medium text-neutral-900">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-neutral-400 text-center">
|
||||
Meatloaf is open-source firmware for retro computing enthusiasts.<br />
|
||||
Released under the GPL3 License.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
src/app/components/ProfilePage.tsx
Normal file
59
src/app/components/ProfilePage.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Bell, BookOpen, ChevronRight, Info, LogOut, Settings } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
onNavigate: (page: string) => void;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
page?: string;
|
||||
destructive?: boolean;
|
||||
}
|
||||
|
||||
const ITEMS: Item[][] = [
|
||||
[
|
||||
{ icon: <Settings className="w-5 h-5" />, label: 'Preferences', sublabel: 'General settings', page: 'general' },
|
||||
{ icon: <Bell className="w-5 h-5" />, label: 'Notifications' },
|
||||
],
|
||||
[
|
||||
{ icon: <BookOpen className="w-5 h-5" />, label: 'Documentation' },
|
||||
{ icon: <Info className="w-5 h-5" />, label: 'About Meatloaf', page: 'about-meatloaf' },
|
||||
],
|
||||
[
|
||||
{ icon: <LogOut className="w-5 h-5" />, label: 'Log Out', destructive: true },
|
||||
],
|
||||
];
|
||||
|
||||
export default function ProfilePage({ onNavigate }: Props) {
|
||||
return (
|
||||
<div className="max-w-lg mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold mb-6">Profile</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
{ITEMS.map((group, gi) => (
|
||||
<div key={gi} className="rounded-xl overflow-hidden border border-neutral-200 divide-y divide-neutral-200">
|
||||
{group.map((item, ii) => (
|
||||
<button
|
||||
key={ii}
|
||||
onClick={() => item.page && onNavigate(item.page)}
|
||||
disabled={!item.page}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 text-left bg-white hover:bg-neutral-50 disabled:opacity-50 disabled:cursor-default transition-colors ${item.destructive ? 'text-red-600' : 'text-neutral-900'}`}
|
||||
>
|
||||
<span className={item.destructive ? 'text-red-500' : 'text-neutral-500'}>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className="flex-1 min-w-0">
|
||||
<span className="block text-sm font-medium">{item.label}</span>
|
||||
{item.sublabel && <span className="block text-xs text-neutral-400">{item.sublabel}</span>}
|
||||
</span>
|
||||
{item.page && <ChevronRight className="w-4 h-4 text-neutral-400 flex-shrink-0" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user