diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..00f59c1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run typecheck diff --git a/src/app/page.tsx b/src/app/page.tsx index 6d50df7..12a40a5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,491 +1,241 @@ "use client"; -import React from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Dock } from '@/components/desktop/dock'; -import dynamic from 'next/dynamic'; -import { - Settings as SettingsIcon, - Terminal, - Trash2, - Home as HomeIcon, - FileText, - Folder, - FolderPlus, - FilePlus, - Pencil, - Trash, - Music, + +import * as React from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import dynamic from "next/dynamic"; +import { + Settings as SettingsIcon, + Terminal, + Trash2, + Home as HomeIcon, + FileText, + Folder, + FolderPlus, + FilePlus, + Pencil, + Trash, + Music, Globe, Users, Network, Calculator, Calendar, Mail, - MessageCircle -} from 'lucide-react'; -import type { LucideProps } from 'lucide-react'; -import { Settings } from '@/components/desktop/settings'; -import { TopBar } from '@/components/desktop/top-bar'; -import { AppGallery } from '@/components/desktop/app-gallery'; -import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; -import { LockScreen } from '@/components/desktop/lock-screen'; -import { - initialFileSystem, - getPath, - getUniqueName, - type Directory, - type File as FileType, - deleteItem as deleteFsItem, - renameItem as renameFsItem -} from '@/lib/file-system'; -import { TextEditor } from '@/components/desktop/text-editor'; -import { produce } from 'immer'; -import { useToast } from '@/hooks/use-toast'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle -} from '@/components/ui/alert-dialog'; -import { Input } from '@/components/ui/input'; + MessageCircle, +} from "lucide-react"; +import type { LucideProps } from "lucide-react"; + +import { + DesktopProvider, + useFileSystemContext, + useWindowManagerContext, + type AppId, + type App, + type WindowInstance, +} from "@/context/DesktopContext"; +import { Settings } from "@/components/desktop/settings"; +import { TopBar } from "@/components/desktop/top-bar"; +import { AppGallery } from "@/components/desktop/app-gallery"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { LockScreen } from "@/components/desktop/lock-screen"; +import { + getPath, + type Directory, + type File as FileType, +} from "@/lib/file-system"; +import { TextEditor } from "@/components/desktop/text-editor"; +import { useToast } from "@/hooks/use-toast"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Input } from "@/components/ui/input"; +import { Dock } from "@/components/desktop/dock"; // Dynamic imports for better performance -const Window = dynamic(() => import('@/components/desktop/window').then(mod => mod.Window), { - ssr: false, - loading: () => null, -}); - -const TerminalComponent = dynamic(() => import('@/components/desktop/terminal'), { - ssr: false, - loading: () => null, -}); - -const FileExplorer = dynamic(() => import('@/components/desktop/file-explorer').then(mod => mod.FileExplorer), { - ssr: false, - loading: () => null, -}); - -const MusicPlayer = dynamic(() => import('@/components/desktop/music-player').then(mod => mod.MusicPlayer), { - ssr: false, - loading: () => null, -}); - -const Browser = dynamic(() => import('@/components/desktop/browser').then(mod => mod.Browser), { - ssr: false, - loading: () => null, -}); +const Window = dynamic( + () => import("@/components/desktop/window").then((mod) => mod.Window), + { + ssr: false, + loading: () => null, + }, +); + +const TerminalComponent = dynamic( + () => import("@/components/desktop/terminal"), + { + ssr: false, + loading: () => null, + }, +); + +const FileExplorer = dynamic( + () => + import("@/components/desktop/file-explorer").then( + (mod) => mod.FileExplorer, + ), + { + ssr: false, + loading: () => null, + }, +); + +const MusicPlayer = dynamic( + () => + import("@/components/desktop/music-player").then((mod) => mod.MusicPlayer), + { + ssr: false, + loading: () => null, + }, +); + +const Browser = dynamic( + () => import("@/components/desktop/browser").then((mod) => mod.Browser), + { + ssr: false, + loading: () => null, + }, +); // Import the new Calculator and Calendar components -const CalculatorApp = dynamic(() => import('@/components/desktop/calculator').then(mod => mod.Calculator), { - ssr: false, - loading: () => null, -}); - -const CalendarApp = dynamic(() => import('@/components/desktop/calendar').then(mod => mod.Calendar), { - ssr: false, - loading: () => null, -}); - -export type AppId = 'terminal' | 'settings' | 'home' | 'editor' | 'music' | 'browser' | 'calculator' | 'calendar' | 'mail' | 'messages'; - -export interface App { - id: AppId; - title: string; - icon: React.ForwardRefExoticComponent & React.RefAttributes>; - component?: React.ReactNode; - defaultSize: { width: number; height: number }; - category?: string; - description?: string; -} - -export interface WindowInstance { - id: string; - appId: AppId; - title: string; - isMinimized: boolean; - isMaximized: boolean; - zIndex: number; - path?: string; -} +const CalculatorApp = dynamic( + () => import("@/components/desktop/calculator").then((mod) => mod.Calculator), + { + ssr: false, + loading: () => null, + }, +); + +const CalendarApp = dynamic( + () => import("@/components/desktop/calendar").then((mod) => mod.Calendar), + { + ssr: false, + loading: () => null, + }, +); + +export type { AppId, App, WindowInstance }; let windowCount = 0; export default function Desktop() { - const [windows, setWindows] = React.useState([]); - const [isGalleryOpen, setIsGalleryOpen] = React.useState(false); - const [isLocked, setIsLocked] = React.useState(true); const [isMounted, setIsMounted] = React.useState(false); - const [fileSystem, setFileSystem] = React.useState(initialFileSystem); - const { toast } = useToast(); - - const [itemToDelete, setItemToDelete] = React.useState(null); - const [itemToRename, setItemToRename] = React.useState(null); - const [renameValue, setRenameValue] = React.useState(""); - - // Enhanced Apps configuration with working Calculator and Calendar - const APPS: Record = React.useMemo(() => ({ - terminal: { - id: 'terminal', - title: 'Terminal', - icon: Terminal, - component: , - defaultSize: { width: 800, height: 500 }, - category: 'system', - description: 'Command line interface' - }, - settings: { - id: 'settings', - title: 'System Settings', - icon: SettingsIcon, - component: , - defaultSize: { width: 500, height: 600 }, - category: 'system', - description: 'System configuration' - }, - home: { - id: 'home', - title: 'File Manager', - icon: HomeIcon, - defaultSize: { width: 700, height: 500 }, - category: 'utilities', - description: 'Browse and manage files' - }, - editor: { - id: 'editor', - title: 'Text Editor', - icon: FileText, - defaultSize: { width: 600, height: 450 }, - category: 'utilities', - description: 'Edit text files' - }, - music: { - id: 'music', - title: 'Music Player', - icon: Music, - component: openWindow('music', path)} />, - defaultSize: { width: 400, height: 550 }, - category: 'multimedia', - description: 'Play audio files' - }, - browser: { - id: 'browser', - title: 'Web Browser', - icon: Globe, - component: , - defaultSize: { width: 1024, height: 768 }, - category: 'internet', - description: 'Browse the web' - }, - calculator: { - id: 'calculator', - title: 'Calculator', - icon: Calculator, - component: , - defaultSize: { width: 320, height: 480 }, - category: 'utilities', - description: 'Scientific calculator' - }, - calendar: { - id: 'calendar', - title: 'Calendar', - icon: Calendar, - component: , - defaultSize: { width: 800, height: 600 }, - category: 'utilities', - description: 'Schedule and events' - }, - mail: { - id: 'mail', - title: 'Email', - icon: Mail, - component:
Email Client - Coming Soon
, - defaultSize: { width: 800, height: 600 }, - category: 'internet', - description: 'Email client' - }, - messages: { - id: 'messages', - title: 'Messages', - icon: MessageCircle, - component:
Messaging App - Coming Soon
, - defaultSize: { width: 400, height: 600 }, - category: 'internet', - description: 'Instant messaging' - }, - }), [fileSystem]); - - // File system operations - const handleCreateItem = React.useCallback((path: string, type: 'file' | 'folder') => { - setFileSystem(produce(draft => { - const pathParts = path.startsWith('~/') ? path.split('/').slice(1) : path === '~' ? [] : path.split('/'); - const parentNode = getPath(pathParts, draft); - - if (parentNode && parentNode.type === 'directory') { - const baseName = type === 'file' ? 'Untitled' : 'New Folder'; - const newName = getUniqueName(parentNode, baseName, type === 'file' ? 'file' : 'directory'); - - if (type === 'file') { - parentNode.children[newName] = { type: 'file', name: newName, content: '' }; - } else { - parentNode.children[newName] = { type: 'directory', name: newName, children: {} }; - } - } - })); - }, []); - - const handleDeleteItem = React.useCallback(() => { - if (!itemToDelete) return; - - setFileSystem(produce(draft => { - deleteFsItem(itemToDelete, draft); - })); - - toast({ - title: "Item Deleted", - description: `"${itemToDelete.split('/').pop()}" was moved to Trash.` - }); - setItemToDelete(null); - }, [itemToDelete, toast]); - - const handleRename = React.useCallback((path: string, name: string) => { - setItemToRename(path); - setRenameValue(name); - }, []); - - const handleRenameSubmit = React.useCallback((e?: React.FormEvent) => { - e?.preventDefault(); - - if (!itemToRename || !renameValue.trim()) { - setItemToRename(null); - return; - } - - const parentPath = itemToRename.substring(0, itemToRename.lastIndexOf('/')) || '~'; - const parentNode = getPath(parentPath === '~' ? [] : parentPath.split('/').slice(1), fileSystem); - - if (parentNode?.type === 'directory' && parentNode.children[renameValue] && itemToRename !== `${parentPath}/${renameValue}`) { - toast({ - variant: 'destructive', - title: "Rename Error", - description: "An item with this name already exists." - }); - return; - } - - setFileSystem(produce(draft => { - renameFsItem(itemToRename, renameValue, draft); - })); - - toast({ - title: "Item Renamed", - description: "Successfully renamed item." - }); - setItemToRename(null); - setRenameValue(""); - }, [itemToRename, renameValue, fileSystem, toast]); - - // Window management - const openWindow = React.useCallback((appId: AppId, path?: string) => { - const app = APPS[appId]; - if (!app) return; - - // Check for existing windows - if ((appId === 'home' || appId === 'editor') && path) { - const existingWindow = windows.find(w => w.appId === appId && w.path === path); - if (existingWindow) { - bringToFront(existingWindow.id); - return; - } - } - - const maxZIndex = windows.reduce((max, w) => Math.max(max, w.zIndex), 0); - - let title = app.title; - if ((appId === 'editor' || appId === 'home') && path) { - const parts = path.split('/'); - title = parts[parts.length - 1] || (appId === 'home' ? 'Home' : app.title); - } - - const newWindow: WindowInstance = { - id: `${appId}-${++windowCount}`, - appId, - title, - isMinimized: false, - isMaximized: false, - zIndex: maxZIndex + 1, - path - }; - - setWindows(prev => [...prev, newWindow]); - }, [APPS, windows]); - - const closeWindow = React.useCallback((id: string) => { - setWindows(prev => prev.filter(w => w.id !== id)); - }, []); - - const bringToFront = React.useCallback((id: string) => { - setWindows(prev => { - const maxZIndex = prev.reduce((max, w) => Math.max(max, w.zIndex), 0); - const targetWindow = prev.find(w => w.id === id); - - if (targetWindow && targetWindow.zIndex === maxZIndex && !targetWindow.isMinimized) { - return prev; - } - - return prev.map(w => - w.id === id - ? { ...w, zIndex: maxZIndex + 1, isMinimized: false } - : w - ); - }); - }, []); - const toggleMinimize = React.useCallback((id: string) => { - setWindows(prev => prev.map(w => - w.id === id ? { ...w, isMinimized: !w.isMinimized } : w - )); + React.useEffect(() => { + setIsMounted(true); }, []); - const toggleMaximize = React.useCallback((id: string) => { - setWindows(prev => prev.map(w => - w.id === id ? { ...w, isMaximized: !w.isMaximized } : w - )); - - if (!windows.find(w => w.id === id)?.isMaximized) { - bringToFront(id); - } - }, [bringToFront, windows]); - - const toggleGallery = React.useCallback(() => { - setIsGalleryOpen(prev => !prev); - }, []); + if (!isMounted) return null; - const handleLock = React.useCallback(() => { - setIsLocked(true); - }, []); + return ( + + + + ); +} - const handleUnlock = React.useCallback(() => { - setIsLocked(false); - }, []); +const DesktopContent = () => { + const wm = useWindowManagerContext(); + const fs = useFileSystemContext(); + + const { + windows, + openWindow, + closeWindow, + bringToFront, + toggleMinimize, + toggleMaximize, + toggleGallery, + handleLock, + handleUnlock, + isGalleryOpen, + isLocked, + apps: contextApps, + } = wm; + + const { + fileSystem, + handleCreateItem, + setItemToDelete, + itemToDelete, + handleDeleteItem, + handleRename, + itemToRename, + setItemToRename, + renameValue, + setRenameValue, + handleRenameSubmit, + handleSaveFile, + } = fs; + + // This part needs to remain here because it depends on openWindow and other context actions + const APPS_WITH_COMPONENTS: Record = React.useMemo( + () => ({ + terminal: { ...BASE_APPS.terminal, component: }, + settings: { ...BASE_APPS.settings, component: }, + home: BASE_APPS.home, + editor: BASE_APPS.editor, + music: { + ...BASE_APPS.music, + component: , + }, + browser: { ...BASE_APPS.browser, component: }, + calculator: { ...BASE_APPS.calculator, component: }, + calendar: { ...BASE_APPS.calendar, component: }, + mail: { + ...BASE_APPS.mail, + component: ( +
+ Email Client - Coming Soon +
+ ), + }, + messages: { + ...BASE_APPS.messages, + component: ( +
+ Messaging App - Coming Soon +
+ ), + }, + }), + [], + ); - const handleSaveFile = React.useCallback((path: string, newContent: string) => { - setFileSystem(produce(draft => { - const pathParts = path.startsWith('~/') ? path.split('/').slice(1) : path.split('/'); - const fileNode = getPath(pathParts, draft); - - if (fileNode && fileNode.type === 'file') { - fileNode.content = newContent; - } - })); - - toast({ - title: "File Saved", - description: `Saved ${path}` - }); - }, [toast]); - - // System icons including Calculator and Calendar const systemIcons = [ - { - id: 'home-icon', - name: 'Home', - icon: HomeIcon, - path: '~/Home', - action: () => openWindow('home', '~/Home'), - color: 'text-white' + { + id: "home-icon", + name: "Home", + icon: HomeIcon, + path: "~/Home", + action: () => openWindow("home", "~/Home"), + color: "text-white", }, - // { - // id: 'calculator', - // name: 'Calculator', - // icon: Calculator, - // path: '', - // action: () => openWindow('calculator'), - // color: 'text-white' - // }, - // { - // id: 'calendar', - // name: 'Calendar', - // icon: Calendar, - // path: '', - // action: () => openWindow('calendar'), - // color: 'text-white' - // }, - // { - // id: 'trash', - // name: 'Trash', - // icon: Trash2, - // path: '~/Trash', - // action: () => console.log('Open Trash'), - // color: 'text-white' - // }, - // { - // id: 'network', - // name: 'Network', - // icon: Network, - // path: '~/Network', - // action: () => console.log('Open Network'), - // color: 'text-white' - // }, - // { - // id: 'users', - // name: 'Users', - // icon: Users, - // path: '~/Users', - // action: () => console.log('Open Users'), - // color: 'text-white' - // }, ]; - const allDesktopIcons = systemIcons; - - // Context menus - const renderDesktopContextMenu = () => ( - - handleCreateItem('~/Home', 'folder')} className="hover:bg-gray-700/50"> - - New Folder - - handleCreateItem('~/Home', 'file')} className="hover:bg-gray-700/50"> - - New Text File - - - ); - - const renderItemContextMenu = (item: { id: string; name: string; path: string }) => ( - - handleRename(item.path, item.name)} className="hover:bg-gray-700/50"> - - Rename - - setItemToDelete(item.path)} - className="text-red-400 hover:text-red-300 hover:bg-red-500/20" - > - - Delete - - - ); - - // Focus management const focusedWindow = React.useMemo(() => { - if (!isMounted || windows.length === 0) return null; - const activeWindows = windows.filter(w => !w.isMinimized); - return activeWindows.reduce((top, w) => (w.zIndex > top.zIndex ? w : top), activeWindows[0]); - }, [windows, isMounted]); - - const focusedApp = focusedWindow ? APPS[focusedWindow.appId] : null; - const apps = React.useMemo(() => Object.values(APPS).filter(app => app.id !== 'editor'), [APPS]); - - // Effects - React.useEffect(() => { - setIsMounted(true); - }, []); + if (windows.length === 0) return null; + const activeWindows = windows.filter((w: WindowInstance) => !w.isMinimized); + return activeWindows.reduce( + (top: WindowInstance, w: WindowInstance) => (w.zIndex > top.zIndex ? w : top), + activeWindows[0], + ); + }, [windows]); + + const focusedApp = focusedWindow + ? APPS_WITH_COMPONENTS[focusedWindow.appId] + : null; return ( <> @@ -497,47 +247,43 @@ export default function Desktop() { > - - - {/* System Icons including Calculator and Calendar */} + +
- {allDesktopIcons.map(item => { + {systemIcons.map((item) => { const Icon = item.icon; const isRenaming = itemToRename === item.path; - const iconColor = 'text-white'; return ( - - {/* Enhanced Icon Container */}
- +
- - {/* Selection Indicator */}
- - {/* Label */} + {isRenaming ? ( -
- + setRenameValue(e.target.value)} onBlur={handleRenameSubmit} onKeyDown={(e) => { - if (e.key === 'Escape') { + if (e.key === "Escape") { setItemToRename(null); setRenameValue(""); } @@ -553,116 +299,115 @@ export default function Desktop() { )} - {item.id !== 'trash' && item.id !== 'home-icon' && item.id !== 'calculator' && item.id !== 'calendar' && renderItemContextMenu(item)} ); })}
- {/* Windows */} - {isMounted && windows.filter(win => !win.isMinimized).map((win) => { - let component; - const app = APPS[win.appId]; - - if (win.appId === 'home') { - component = ( - handleCreateItem(path, 'file')} - onCreateFolder={(path) => handleCreateItem(path, 'folder')} - onDeleteItem={setItemToDelete} - onRenameItem={handleRename} - itemToRename={itemToRename} - renameValue={renameValue} - setRenameValue={setRenameValue} - onRenameSubmit={handleRenameSubmit} - setItemToRename={setItemToRename} - /> - ); - } else if (win.appId === 'editor' && win.path) { - const pathParts = win.path.startsWith('~/') ? win.path.split('/').slice(1) : win.path.split('/'); - const fileNode = getPath(pathParts, fileSystem); - if (fileNode && fileNode.type === 'file') { + {windows + .filter((win: WindowInstance) => !win.isMinimized) + .map((win: WindowInstance) => { + let component; + const app = APPS_WITH_COMPONENTS[win.appId]; + + if (win.appId === "home") { component = ( - handleSaveFile(win.path!, newContent)} - fileName={fileNode.name} - /> + ); + } else if (win.appId === "editor" && win.path) { + const pathParts = win.path.startsWith("~/") + ? win.path.split("/").slice(1) + : win.path.split("/"); + const fileNode = getPath(pathParts, fileSystem); + if (fileNode && fileNode.type === "file") { + component = ( + + handleSaveFile(win.path!, newContent) + } + fileName={fileNode.name} + /> + ); + } + } else { + component = app.component; } - } else { - component = app.component; - } - - if (!component) return null; - return ( - closeWindow(win.id)} - onFocus={() => bringToFront(win.id)} - onMinimize={() => toggleMinimize(win.id)} - onMaximize={() => toggleMaximize(win.id)} - isMaximized={win.isMaximized} - defaultSize={app.defaultSize} - zIndex={win.zIndex} - > - {React.cloneElement(component as React.ReactElement, { key: win.id })} - - ); - })} + if (!component) return null; + + return ( + closeWindow(win.id)} + onFocus={() => bringToFront(win.id)} + onMinimize={() => toggleMinimize(win.id)} + onMaximize={() => toggleMaximize(win.id)} + isMaximized={win.isMaximized} + defaultSize={app.defaultSize} + zIndex={win.zIndex} + > + {React.cloneElement(component as React.ReactElement, { + key: win.id, + })} + + ); + })}
- {renderDesktopContextMenu()} + + handleCreateItem("~/Home", "folder")} + className="hover:bg-gray-700/50" + > + + New Folder + + handleCreateItem("~/Home", "file")} + className="hover:bg-gray-700/50" + > + + New Text File + +
- - {/* Overlays */} - - - + + + - {/* Lock Screen */} {isLocked && } - {/* Enhanced Delete Confirmation Dialog */} - !isOpen && setItemToDelete(null)}> + !isOpen && setItemToDelete(null)} + > - Delete Item + + Delete Item + - Are you sure you want to delete "{itemToDelete?.split('/').pop()}"? This action cannot be undone. + Are you sure you want to delete "{itemToDelete?.split("/").pop()} + "? This action cannot be undone. - setItemToDelete(null)} className="bg-gray-700/50 border-gray-600/50 hover:bg-gray-600/50 text-white" > Cancel - Delete @@ -672,4 +417,87 @@ export default function Desktop() { ); -} \ No newline at end of file +}; + +const BASE_APPS: Record = { + terminal: { + id: "terminal", + title: "Terminal", + icon: Terminal, + defaultSize: { width: 800, height: 500 }, + category: "system", + description: "Command line interface", + }, + settings: { + id: "settings", + title: "System Settings", + icon: SettingsIcon, + defaultSize: { width: 500, height: 600 }, + category: "system", + description: "System configuration", + }, + home: { + id: "home", + title: "File Manager", + icon: HomeIcon, + defaultSize: { width: 700, height: 500 }, + category: "utilities", + description: "Browse and manage files", + }, + editor: { + id: "editor", + title: "Text Editor", + icon: FileText, + defaultSize: { width: 600, height: 450 }, + category: "utilities", + description: "Edit text files", + }, + music: { + id: "music", + title: "Music Player", + icon: Music, + defaultSize: { width: 400, height: 550 }, + category: "multimedia", + description: "Play audio files", + }, + browser: { + id: "browser", + title: "Web Browser", + icon: Globe, + defaultSize: { width: 1024, height: 768 }, + category: "internet", + description: "Browse the web", + }, + calculator: { + id: "calculator", + title: "Calculator", + icon: Calculator, + defaultSize: { width: 320, height: 480 }, + category: "utilities", + description: "Scientific calculator", + }, + calendar: { + id: "calendar", + title: "Calendar", + icon: Calendar, + defaultSize: { width: 800, height: 600 }, + category: "utilities", + description: "Schedule and events", + }, + mail: { + id: "mail", + title: "Email", + icon: Mail, + defaultSize: { width: 800, height: 600 }, + category: "internet", + description: "Email client", + }, + messages: { + id: "messages", + title: "Messages", + icon: MessageCircle, + defaultSize: { width: 400, height: 600 }, + category: "internet", + description: "Instant messaging", + }, +}; diff --git a/src/components/desktop/app-gallery.tsx b/src/components/desktop/app-gallery.tsx index b4e630d..c70e79b 100644 --- a/src/components/desktop/app-gallery.tsx +++ b/src/components/desktop/app-gallery.tsx @@ -1,29 +1,23 @@ "use client"; import { motion, AnimatePresence } from "framer-motion"; -import { App, AppId } from "@/app/page"; +import { useWindowManagerContext } from "@/context/DesktopContext"; +import type { AppId, App } from "@/hooks/use-window-manager"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Search, X, Grid3X3, List, Clock, Filter } from "lucide-react"; import { useState, useMemo } from "react"; -type AppGalleryProps = { - isOpen: boolean; - onClose: () => void; - apps: App[]; - openWindow: (appId: AppId) => void; - recentApps?: AppId[]; -}; - type ViewMode = "grid" | "list"; -export function AppGallery({ isOpen, onClose, apps, openWindow, recentApps = [] }: AppGalleryProps) { +export function AppGallery({ recentApps = [] }: { recentApps?: AppId[] }) { + const { isGalleryOpen: isOpen, toggleGallery: onClose, apps, openWindow } = useWindowManagerContext(); const [searchQuery, setSearchQuery] = useState(""); const [viewMode, setViewMode] = useState("grid"); const [selectedCategory, setSelectedCategory] = useState("all"); const categories = useMemo(() => { - const allCategories = apps.map(app => app.category).filter(Boolean); + const allCategories = apps.map((app: App) => app.category).filter(Boolean); return ["all", ...Array.from(new Set(allCategories))] as string[]; }, [apps]); @@ -31,21 +25,21 @@ export function AppGallery({ isOpen, onClose, apps, openWindow, recentApps = [] let filtered = apps; if (searchQuery) { - filtered = filtered.filter(app => + filtered = filtered.filter((app: App) => app.title.toLowerCase().includes(searchQuery.toLowerCase()) || app.description?.toLowerCase().includes(searchQuery.toLowerCase()) ); } if (selectedCategory !== "all") { - filtered = filtered.filter(app => app.category === selectedCategory); + filtered = filtered.filter((app: App) => app.category === selectedCategory); } return filtered; }, [apps, searchQuery, selectedCategory]); const recentAppsList = useMemo(() => { - return apps.filter(app => recentApps.includes(app.id)); + return apps.filter((app: App) => recentApps.includes(app.id)); }, [apps, recentApps]); const handleAppClick = (appId: AppId) => { @@ -251,7 +245,7 @@ export function AppGallery({ isOpen, onClose, apps, openWindow, recentApps = [] ) : ( // List View - {filteredApps.map((app) => { + {filteredApps.map((app: App) => { const Icon = app.icon; return ( void; - onDockIconClick: (windowId: string) => void; - toggleGallery: () => void; -}; -type MenuItem = { - icon: React.ComponentType; - label: string; - description?: string; - action: () => void; - category: string; - isFeatured?: boolean; -}; - -export function Dock({ apps, windows, openWindow, onDockIconClick, toggleGallery }: DockProps) { +export const Dock = React.memo(() => { + const { windows, apps, openWindow, bringToFront, toggleMinimize, isGalleryOpen, toggleGallery } = useWindowManagerContext(); + const [hoveredApp, setHoveredApp] = React.useState(null); + const [dockWidth, setDockWidth] = React.useState(0); + const dockRef = React.useRef(null); + const [isMenuOpen, setIsMenuOpen] = useState(false); const [activeCategory, setActiveCategory] = useState("favorites"); const [searchQuery, setSearchQuery] = useState(""); @@ -95,7 +85,7 @@ export function Dock({ apps, windows, openWindow, onDockIconClick, toggleGallery icon: FileText, label: "Files", description: "File manager", - action: () => openWindow('files'), + action: () => openWindow('home'), category: "favorites", isFeatured: true }, @@ -111,15 +101,15 @@ export function Dock({ apps, windows, openWindow, onDockIconClick, toggleGallery icon: Laptop, label: "System Monitor", description: "Resource usage", - action: () => openWindow('system-monitor'), + action: () => openWindow('settings'), // Assuming settings for now category: "favorites", isFeatured: true }, ], applications: [ - { icon: Camera, label: "Camera", action: () => openWindow('camera'), category: "applications" }, + { icon: Camera, label: "Camera", action: () => openWindow('browser'), category: "applications" }, // Placeholder action { icon: Music, label: "Music", action: () => openWindow('music'), category: "applications" }, - { icon: Video, label: "Video", action: () => openWindow('video'), category: "applications" }, + { icon: Video, label: "Video", action: () => openWindow('browser'), category: "applications" }, // Placeholder action ], system: [ { icon: Shield, label: "Security", action: () => console.log("Security"), category: "system" }, @@ -412,15 +402,15 @@ export function Dock({ apps, windows, openWindow, onDockIconClick, toggleGallery
{/* App Icons */} - {apps.map(app => { - const openInstances = windows.filter(w => w.appId === app.id); + {apps.map((app) => { + const openInstances = windows.filter((w) => w.appId === app.id); const isAppOpen = openInstances.length > 0; - const isMinimized = isAppOpen && openInstances.every(w => w.isMinimized); + const isMinimized = isAppOpen && openInstances.every((w) => w.isMinimized); const handleClick = () => { if (isAppOpen) { - const activeWindow = openInstances.find(w => !w.isMinimized) || openInstances[0]; - onDockIconClick(activeWindow.id); + const activeWindow = openInstances.find((w) => !w.isMinimized) || openInstances[0]; + bringToFront(activeWindow.id); } else { openWindow(app.id); } @@ -499,4 +489,4 @@ export function Dock({ apps, windows, openWindow, onDockIconClick, toggleGallery
); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/components/desktop/file-explorer.tsx b/src/components/desktop/file-explorer.tsx index df41603..15e19ec 100644 --- a/src/components/desktop/file-explorer.tsx +++ b/src/components/desktop/file-explorer.tsx @@ -9,35 +9,29 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } import type { AppId } from "@/app/page"; import { Input } from "@/components/ui/input"; -type FileExplorerProps = { - fileSystem: Directory; - currentPath: string; - onOpenFile: (appId: AppId, path: string) => void; - onCreateFile: (path: string) => void; - onCreateFolder: (path: string) => void; - onDeleteItem: (path: string) => void; - onRenameItem: (path: string, currentName: string) => void; - itemToRename: string | null; - renameValue: string; - setRenameValue: (value: string) => void; - onRenameSubmit: (e?: React.FormEvent) => void; - setItemToRename: (path: string | null) => void; -}; +import { useFileSystemContext, useWindowManagerContext } from "@/context/DesktopContext"; export function FileExplorer({ - fileSystem, - onOpenFile, currentPath: initialPath, - onCreateFile, - onCreateFolder, - onDeleteItem, - onRenameItem, - itemToRename, - renameValue, - setRenameValue, - onRenameSubmit, - setItemToRename -}: FileExplorerProps) { +}: { currentPath: string }) { + const { + fileSystem, + handleCreateItem, + handleDeleteItem, + handleRename, + handleRenameSubmit, + setItemToRename, + itemToRename, + renameValue, + setRenameValue + } = useFileSystemContext(); + + const { openWindow, bringToFront, windows } = useWindowManagerContext(); + + const onCreateFolder = (path: string) => handleCreateItem(path, 'folder'); + const onCreateFileWrapper = (path: string) => handleCreateItem(path, 'file'); + const onRenameItem = (path: string, name: string) => handleRename(path, name); + const onRenameSubmitWrapper = (e?: React.FormEvent) => handleRenameSubmit(e); const [currentPath, setCurrentPath] = useState(initialPath); const navigateTo = (path: string) => { @@ -65,7 +59,7 @@ export function FileExplorer({ if (item.type === 'directory') { navigateTo(newPath); } else { - onOpenFile('editor', newPath); + openWindow('editor', newPath); } }; @@ -82,7 +76,7 @@ export function FileExplorer({ New Folder - onCreateFile(currentPath)}> + onCreateFileWrapper(currentPath)}> New Text File @@ -95,7 +89,7 @@ export function FileExplorer({ Rename - onDeleteItem(itemPath)} className="text-destructive focus:text-destructive"> + handleDeleteItem()} className="text-destructive focus:text-destructive"> Delete @@ -141,15 +135,15 @@ export function FileExplorer({ )}
{isRenaming ? ( - - setRenameValue(e.target.value)} - onBlur={() => onRenameSubmit()} - onKeyDown={(e) => { - if (e.key === 'Escape') setItemToRename(null); - }} + + setRenameValue(e.target.value)} + onBlur={() => onRenameSubmitWrapper()} + onKeyDown={(e) => { + if (e.key === 'Escape') setItemToRename(null); + }} className="text-xs h-6 px-1" autoFocus /> diff --git a/src/components/desktop/music-player.tsx b/src/components/desktop/music-player.tsx index 8072442..efbc54a 100644 --- a/src/components/desktop/music-player.tsx +++ b/src/components/desktop/music-player.tsx @@ -5,20 +5,17 @@ import { Play, Pause, Rewind, FastForward, ListMusic, Volume2 } from 'lucide-rea import { Button } from '@/components/ui/button'; import { Slider } from '@/components/ui/slider'; import { Card, CardContent } from '@/components/ui/card'; -import { getPath, type Directory, type File } from '@/lib/file-system'; +import { useDesktop } from '@/context/DesktopContext'; +import { getPath, type File } from '@/lib/file-system'; import { cn } from '@/lib/utils'; -type MusicPlayerProps = { - fileSystem: Directory; - onOpenFile: (path: string) => void; -}; - type Song = { name: string; url: string; }; -export function MusicPlayer({ fileSystem }: MusicPlayerProps) { +export function MusicPlayer() { + const { fileSystem } = useDesktop(); const [playlist, setPlaylist] = useState([]); const [currentTrackIndex, setCurrentTrackIndex] = useState(0); const [isPlaying, setIsPlaying] = useState(false); diff --git a/src/components/desktop/settings.tsx b/src/components/desktop/settings.tsx index ac08af0..930ab66 100644 --- a/src/components/desktop/settings.tsx +++ b/src/components/desktop/settings.tsx @@ -5,6 +5,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { useDesktop } from "@/context/DesktopContext"; +import { Trash2, AlertTriangle } from "lucide-react"; const defaultTheme = { background: "0 0% 16%", @@ -56,12 +58,14 @@ export function Settings() { applyTheme(defaultTheme); }; + const { resetFileSystem } = useDesktop(); + if (!isMounted) { return null; // Avoid rendering until localStorage is read } return ( -
+
Theme Customization @@ -99,8 +103,36 @@ export function Settings() {
- + +
+ + + + + + + + Danger Zone + + + Actions here are permanent and cannot be undone. + + + +
+

Reset System Data

+

+ Clear your custom files, folders, and settings to return to a fresh installation state. +

+
diff --git a/src/components/desktop/terminal.tsx b/src/components/desktop/terminal.tsx index 64f9f64..ebfce3f 100644 --- a/src/components/desktop/terminal.tsx +++ b/src/components/desktop/terminal.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect, useRef } from 'react'; import gsap from 'gsap'; +import { useCommandHandler } from '@/hooks/use-command-handler'; const Terminal = () => { const [input, setInput] = useState(''); @@ -79,234 +80,7 @@ useEffect(() => { ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ `; - const commands: Record any }> = { - help: { - description: 'Show available commands', - action: () => [ - { type: 'output', content: 'Available commands:', class: 'terminal-bright' }, - { type: 'output', content: '' }, - { type: 'output', content: ' help - Show this help message' }, - { type: 'output', content: ' about - Learn about me' }, - { type: 'output', content: ' skills - View my technical skills' }, - { type: 'output', content: ' projects - See my projects' }, - { type: 'output', content: ' contact - Get my contact information' }, - { type: 'output', content: ' neofetch - Display system information' }, - { type: 'output', content: ' ls - List directory contents' }, - { type: 'output', content: ' cat - Read file contents' }, - { type: 'output', content: ' whoami - Display current user' }, - { type: 'output', content: ' date - Show current date and time' }, - { type: 'output', content: ' clear - Clear the terminal' }, - { type: 'output', content: ' banner - Show welcome banner' }, - ] - }, - about: { - description: 'Display professional information about me', - action: () => [ - { type: 'output', content: '┌─────────────────────────────────────────────────────────┐', class: 'terminal-bright' }, - { type: 'output', content: '│ ZAIN UL ABIDEEN │', class: 'terminal-bright' }, - { type: 'output', content: '│ FULL-STACK DEVELOPER │', class: 'terminal-bright' }, - { type: 'output', content: '└─────────────────────────────────────────────────────────┘', class: 'terminal-bright' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🚀 Passionate Full-Stack Developer specializing in the MERN stack' }, - { type: 'output', content: ' and Next.js, with expertise in creating dynamic, high-performance' }, - { type: 'output', content: ' web applications.' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🎯 TECHNICAL EXPERTISE:' }, - { type: 'output', content: ' • MERN Stack (MongoDB, Express.js, React, Node.js)' }, - { type: 'output', content: ' • Next.js & Modern React Ecosystem' }, - { type: 'output', content: ' • RESTful API Design & Development' }, - { type: 'output', content: ' • Interactive Animations (GSAP, Three.js)' }, - { type: 'output', content: ' • Responsive UI/UX with Tailwind CSS' }, - { type: 'output', content: ' • Database Architecture & Optimization' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 💼 PROFESSIONAL HIGHLIGHTS:' }, - { type: 'output', content: ' • 1+ years of professional development experience' }, - { type: 'output', content: ' • Delivered 40% increase in user engagement' }, - { type: 'output', content: ' • Built applications for international clients' }, - { type: 'output', content: ' • Full-stack project lifecycle management' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🔄 Constantly evolving with emerging technologies and' }, - { type: 'output', content: ' best practices in web development.' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 📍 Based in Pakistan | Open to Opportunities' }, - ] - }, - skills: { - description: 'Display my technical skills and proficiencies', - action: () => [ - { type: 'output', content: '┌─────────────────────────────────────────────────────┐', class: 'terminal-bright' }, - { type: 'output', content: '│ TECHNICAL SKILLS │', class: 'terminal-bright' }, - { type: 'output', content: '└─────────────────────────────────────────────────────┘', class: 'terminal-bright' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🎯 FRONTEND DEVELOPMENT:' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▰▰ 95% React.js / Next.js' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▰▱ 90% JavaScript (ES6+) / TypeScript' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▰▰ 95% HTML5 / CSS3 / Tailwind CSS' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▱▱ 85% GSAP / Three.js / Framer Motion' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▱▱ 85% Redux / Zustand State Management' }, - { type: 'output', content: '' }, - { type: 'output', content: ' ⚙️ BACKEND DEVELOPMENT:' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▰▱ 90% Node.js / Express.js' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▱▱ 85% MongoDB / Mongoose ODM' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▱▱▱ 75% MySQL / PostgreSQL' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▰▱ 90% RESTful API Design' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▱▱ 80% JWT Authentication' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🛠️ TOOLS & PLATFORMS:' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▰▰ 95% Git / GitHub' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▰▱ 90% Vercel / Netlify' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▱▱ 85% VS Code / Linux Terminal' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▱▱▱ 70% Docker / CI/CD' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▱▱ 80% Postman / API Testing' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🎨 DESIGN & CMS:' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▱▱ 85% Figma / UI Design' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▱▱▱ 75% WordPress / Elementor' }, - { type: 'output', content: ' ▰▰▰▰▰▰▰▰▱▱ 80% Responsive Web Design' }, - ] - }, - projects: { - description: 'Explore my projects portfolio', - action: () => [ - { type: 'output', content: '┌─────────────────────────────────────────────────────┐', class: 'terminal-bright' }, - { type: 'output', content: '│ PROJECTS │', class: 'terminal-bright' }, - { type: 'output', content: '└─────────────────────────────────────────────────────┘', class: 'terminal-bright' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🖥️ [1] TERMINAL PORTFOLIO', class: 'terminal-cyan' }, - { type: 'output', content: ' Interactive Linux-terminal themed portfolio', class: 'terminal-dim' }, - { type: 'output', content: ' Tech: React, TypeScript, Vite, CSS3', class: 'terminal-dim' }, - { type: 'output', content: ' Status: 🟢 Live', class: 'terminal-green' }, - { type: 'output', content: '' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 💼 [2] JOBSYNC - JOB PORTAL', class: 'terminal-cyan' }, - { type: 'output', content: ' Full-stack job board with role-based authentication', class: 'terminal-dim' }, - { type: 'output', content: ' Tech: MERN Stack, Redux, JWT, Shadcn/UI', class: 'terminal-dim' }, - { type: 'output', content: ' Status: 🟢 Live', class: 'terminal-green' }, - { type: 'output', content: '' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🎨 [3] THIS IS MEGMA', class: 'terminal-cyan' }, - { type: 'output', content: ' 3D animated website with scroll-triggered animations', class: 'terminal-dim' }, - { type: 'output', content: ' Tech: GSAP, Three.js, JavaScript, HTML5/CSS3', class: 'terminal-dim' }, - { type: 'output', content: ' Status: 🟢 Live', class: 'terminal-green' }, - { type: 'output', content: '' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🤖 [4] CHATPANDA CLONE', class: 'terminal-cyan' }, - { type: 'output', content: ' Animated AI chat interface with 3D elements', class: 'terminal-dim' }, - { type: 'output', content: ' Tech: Next.js, GSAP, Three.js, Tailwind CSS', class: 'terminal-dim' }, - { type: 'output', content: ' Status: 🟢 Live', class: 'terminal-green' }, - { type: 'output', content: '' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🎓 [5] LMS PLATFORM', class: 'terminal-cyan' }, - { type: 'output', content: ' Learning management system with video streaming', class: 'terminal-dim' }, - { type: 'output', content: ' Tech: MERN Stack, Cloudinary, Redux, JWT', class: 'terminal-dim' }, - { type: 'output', content: ' Status: 🔵 Completed', class: 'terminal-blue' }, - { type: 'output', content: '' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 📊 [6] CUSTOM CMS PORTFOLIO', class: 'terminal-cyan' }, - { type: 'output', content: ' Dynamic portfolio with admin content management', class: 'terminal-dim' }, - { type: 'output', content: ' Tech: Next.js, MongoDB, Nodemailer, CRUD', class: 'terminal-dim' }, - { type: 'output', content: ' Status: 🔵 Deployed', class: 'terminal-blue' }, - { type: 'output', content: '' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 💡 Type project [number] for detailed view', class: 'terminal-yellow' }, - { type: 'output', content: ' 💡 Example: project 2', class: 'terminal-yellow' }, - ] - }, - - contact: { - description: 'Get in touch with me', - action: () => [ - { type: 'output', content: '┌─────────────────────────────────────────────────────┐', class: 'terminal-bright' }, - { type: 'output', content: '│ CONTACT │', class: 'terminal-bright' }, - { type: 'output', content: '└─────────────────────────────────────────────────────┘', class: 'terminal-bright' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 📧 EMAIL', class: 'terminal-cyan' }, - { type: 'output', content: ' zaynobusiness@gmail.com', class: 'terminal-dim' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 💼 LINKEDIN', class: 'terminal-cyan' }, - { type: 'output', content: ' linkedin.com/in/zayn-butt', class: 'terminal-dim' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🔗 GITHUB', class: 'terminal-cyan' }, - { type: 'output', content: ' github.com/hey-Zayn', class: 'terminal-dim' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🌐 PORTFOLIO', class: 'terminal-cyan' }, - { type: 'output', content: ' my-portfolio-zayn.vercel.app', class: 'terminal-dim' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 📱 PHONE', class: 'terminal-cyan' }, - { type: 'output', content: ' +92 300-3636-186', class: 'terminal-dim' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 🗺️ LOCATION', class: 'terminal-cyan' }, - { type: 'output', content: ' Pakistan', class: 'terminal-dim' }, - { type: 'output', content: '' }, - { type: 'output', content: ' 💬 Feel free to reach out for:' }, - { type: 'output', content: ' • Job opportunities and collaborations', class: 'terminal-dim' }, - { type: 'output', content: ' • Technical discussions and projects', class: 'terminal-dim' }, - { type: 'output', content: ' • Open source contributions', class: 'terminal-dim' }, - { type: 'output', content: '' }, - { type: 'output', content: ' ⚡ I typically respond within 24 hours', class: 'terminal-green' }, - ] - }, - - neofetch: { - description: 'Display system information', - action: () => [ - { type: 'ascii', content: neofetchArt }, - ] - }, - ls: { - description: 'List directory contents', - action: () => [ - { type: 'output', content: 'drwxr-xr-x 2 guest guest 4096 Oct 17 2025 about/' }, - { type: 'output', content: 'drwxr-xr-x 2 guest guest 4096 Oct 17 2025 projects/' }, - { type: 'output', content: 'drwxr-xr-x 2 guest guest 4096 Oct 17 2025 skills/' }, - { type: 'output', content: '-rw-r--r-- 1 guest guest 1337 Oct 17 2025 contact.txt' }, - { type: 'output', content: '-rw-r--r-- 1 guest guest 2048 Oct 17 2025 resume.pdf' }, - { type: 'output', content: '-rwxr-xr-x 1 guest guest 512 Oct 17 2025 run.sh' }, - ] - }, - cat: { - description: 'Read file contents', - action: (args) => { - if (!args || args.length === 0) { - return [{ type: 'error', content: 'cat: missing file operand' }]; - } - if (args[0] === 'contact.txt') { - return [ - { type: 'output', content: '# Contact Information' }, - { type: 'output', content: '' }, - { type: 'output', content: 'Email: zaynobusiness@gamil.com' }, - { type: 'output', content: 'GitHub: github.com/hey-Zayn' }, - ]; - } - return [{ type: 'error', content: `cat: ${args[0]}: No such file or directory` }]; - } - }, - whoami: { - description: 'Current user', - action: () => [ - { type: 'output', content: 'guest@portfolio.dev' }, - ] - }, - date: { - description: 'Show current date', - action: () => [ - { type: 'output', content: new Date().toString() }, - ] - }, - banner: { - description: 'Show welcome banner', - action: () => [ - { type: 'ascii', content: ASCII_ART }, - { type: 'output', content: '' }, - { type: 'output', content: ' Welcome to my terminal portfolio! Type "help" for commands.', class: 'terminal-bright' }, - { type: 'output', content: '' }, - ] - }, - clear: { - description: 'Clear terminal', - action: () => 'clear' - } - }; + const { execute } = useCommandHandler(); useEffect(() => { // Boot sequence @@ -364,12 +138,10 @@ useEffect(() => { const executeCommand = (cmd: string) => { const trimmedCmd = cmd.trim(); + if (!trimmedCmd) return; const newHistory = [...history, { type: 'command', content: cmd }]; - - if (trimmedCmd) { - setCommandHistory(prev => [trimmedCmd, ...prev]); - } + setCommandHistory(prev => [trimmedCmd, ...prev]); setHistoryIndex(-1); if (trimmedCmd === 'clear') { @@ -377,15 +149,8 @@ useEffect(() => { return; } - const [command, ...args] = trimmedCmd.toLowerCase().split(' '); - - if (commands[command]) { - const result = commands[command].action(args); - newHistory.push(...result); - } else if(trimmedCmd) { - newHistory.push({ type: 'error', content: `bash: ${command}: command not found` }); - newHistory.push({ type: 'output', content: 'Type "help" for available commands.' }); - } + const output = execute(trimmedCmd); + newHistory.push({ type: 'output', content: output }); setHistory(newHistory); }; @@ -412,7 +177,7 @@ useEffect(() => { } } else if (e.key === 'Tab') { e.preventDefault(); - const matchingCommands = Object.keys(commands).filter(cmd => + const matchingCommands = ['help', 'about', 'skills', 'projects', 'contact', 'ls', 'cat', 'whoami', 'date', 'clear', 'neofetch'].filter(cmd => cmd.startsWith(input.toLowerCase()) ); if (matchingCommands.length === 1) { diff --git a/src/components/desktop/top-bar.tsx b/src/components/desktop/top-bar.tsx index 227ff18..528fd01 100644 --- a/src/components/desktop/top-bar.tsx +++ b/src/components/desktop/top-bar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Wifi, Volume2, @@ -36,6 +36,7 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { cn } from '@/lib/utils'; +import { useWindowManagerContext } from '@/context/DesktopContext'; interface SystemStatus { cpu: number; @@ -82,14 +83,14 @@ const NOTIFICATIONS: Notification[] = [ { id: '2', title: 'Backup Complete', message: 'System backup successful', time: '1h', read: true }, ]; -export function TopBar({ +export const TopBar = React.memo(({ appTitle = "arch", onLock, className, currentWorkspace = 1, onWorkspaceChange, onOpenApp -}: TopBarProps) { +}: TopBarProps) => { const [time, setTime] = useState(''); const [date, setDate] = useState(new Date()); const [isPowerMenuOpen, setIsPowerMenuOpen] = useState(false); @@ -481,7 +482,7 @@ export function TopBar({ @@ -579,4 +580,4 @@ export function TopBar({ /> ); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/components/desktop/window.tsx b/src/components/desktop/window.tsx index be98924..2e2114f 100644 --- a/src/components/desktop/window.tsx +++ b/src/components/desktop/window.tsx @@ -3,6 +3,7 @@ import { motion, PanInfo, useDragControls } from 'framer-motion'; import { X, Minus, Maximize2, Minimize2 } from 'lucide-react'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import { cn } from '@/lib/utils'; +import { ErrorBoundary } from '@/components/ui/error-boundary'; interface WindowProps { id: string; @@ -205,7 +206,9 @@ export function Window({ {/* Window content */}
- {children} + + {children} +
{/* Resize handles (only when not maximized) */} diff --git a/src/components/ui/error-boundary.tsx b/src/components/ui/error-boundary.tsx new file mode 100644 index 0000000..b0c4f62 --- /dev/null +++ b/src/components/ui/error-boundary.tsx @@ -0,0 +1,75 @@ +'use client'; + +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { AlertCircle, RotateCcw } from 'lucide-react'; +import { Button } from './button'; + +interface Props { + children?: ReactNode; + fallback?: ReactNode; + name?: string; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error(`Uncaught error in ${this.props.name || 'Component'}:`, error, errorInfo); + } + + private handleReset = () => { + this.setState({ hasError: false, error: undefined }); + }; + + public render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+ +
+

+ {this.props.name ? `${this.props.name} Crashed` : 'Something went wrong'} +

+

+ This application encountered an unexpected error and had to stop. The rest of the system is unaffected. +

+
+ +
+ {process.env.NODE_ENV === 'development' && this.state.error && ( +
+

Stack Trace:

+ {this.state.error.stack} +
+ )} +
+ ); + } + + return this.props.children; + } +} diff --git a/src/context/DesktopContext.tsx b/src/context/DesktopContext.tsx new file mode 100644 index 0000000..583cd5b --- /dev/null +++ b/src/context/DesktopContext.tsx @@ -0,0 +1,93 @@ +'use client'; + +import React, { createContext, useContext, useMemo } from 'react'; +import { useFileSystem } from '@/hooks/use-file-system'; +import { useWindowManager } from '@/hooks/use-window-manager'; +import type { App, AppId, WindowInstance } from '@/hooks/use-window-manager'; +import type { Directory } from '@/lib/file-system'; + +export type { App, AppId, WindowInstance }; + +// 1. File System Context +interface FileSystemContextType { + fileSystem: Directory; + itemToDelete: string | null; + setItemToDelete: (path: string | null) => void; + itemToRename: string | null; + setItemToRename: (path: string | null) => void; + renameValue: string; + setRenameValue: (value: string) => void; + handleCreateItem: (path: string, type: 'file' | 'folder') => void; + handleDeleteItem: () => void; + handleRename: (path: string, name: string) => void; + handleRenameSubmit: (e?: React.FormEvent) => void; + handleSaveFile: (path: string, newContent: string) => void; + resetFileSystem: () => void; +} + +const FileSystemContext = createContext(undefined); + +// 2. Window Manager Context +interface WindowManagerContextType { + windows: WindowInstance[]; + isGalleryOpen: boolean; + isLocked: boolean; + openWindow: (appId: AppId, path?: string) => void; + closeWindow: (id: string) => void; + bringToFront: (id: string) => void; + toggleMinimize: (id: string) => void; + toggleMaximize: (id: string) => void; + toggleGallery: () => void; + handleLock: () => void; + handleUnlock: () => void; + apps: App[]; +} + +const WindowManagerContext = createContext(undefined); + +export const DesktopProvider: React.FC<{ children: React.ReactNode; appsConfig: Record }> = ({ children, appsConfig }) => { + const fs = useFileSystem(); + const wm = useWindowManager(appsConfig); + + const apps = useMemo(() => + Object.values(appsConfig).filter(app => app.id !== 'editor'), + [appsConfig] + ); + + const wmValue = useMemo(() => ({ + ...wm, + apps + }), [wm, apps]); + + return ( + + + {children} + + + ); +}; + +// Specialized Hooks for Performance +export const useFileSystemContext = () => { + const context = useContext(FileSystemContext); + if (context === undefined) { + throw new Error('useFileSystemContext must be used within a DesktopProvider'); + } + return context; +}; + +export const useWindowManagerContext = () => { + const context = useContext(WindowManagerContext); + if (context === undefined) { + throw new Error('useWindowManagerContext must be used within a DesktopProvider'); + } + return context; +}; + +// Backward compatibility hook +export const useDesktop = () => { + const fs = useFileSystemContext(); + const wm = useWindowManagerContext(); + return { ...fs, ...wm }; +}; diff --git a/src/hooks/use-command-handler.ts b/src/hooks/use-command-handler.ts index 9fa699c..a81d3b6 100644 --- a/src/hooks/use-command-handler.ts +++ b/src/hooks/use-command-handler.ts @@ -1,6 +1,6 @@ 'use client'; import { useState } from 'react'; -import { fileSystem, getPath } from '@/lib/file-system'; +import { initialFileSystem as fileSystem, getPath } from '@/lib/file-system'; type CommandOutput = string; @@ -99,6 +99,84 @@ export const useCommandHandler = () => { case 'pwd': return `/${cwd.join('/')}`; + case 'about': + return ` +\x1b[1;38;2;138;173;244m┌─────────────────────────────────────────────────────────┐\x1b[0m +\x1b[1;38;2;138;173;244m│ ZAIN UL ABIDEEN │\x1b[0m +\x1b[1;38;2;138;173;244m│ FULL-STACK DEVELOPER │\x1b[0m +\x1b[1;38;2;138;173;244m└─────────────────────────────────────────────────────────┘\x1b[0m + + 🚀 Passionate Full-Stack Developer specializing in the MERN stack + and Next.js, with expertise in creating dynamic, high-performance + web applications. + + 🎯 TECHNICAL EXPERTISE: + • MERN Stack (MongoDB, Express.js, React, Node.js) + • Next.js & Modern React Ecosystem + • RESTful API Design & Development + • Interactive Animations (GSAP, Three.js) + • Responsive UI/UX with Tailwind CSS + • Database Architecture & Optimization + + 💼 PROFESSIONAL HIGHLIGHTS: + • 1+ years of professional development experience + • Delivered 40% increase in user engagement + • Built applications for international clients + • Full-stack project lifecycle management + + 📍 Based in Pakistan | Open to Opportunities +`; + + case 'skills': + return ` +\x1b[1;38;2;138;173;244m┌─────────────────────────────────────────────────────┐\x1b[0m +\x1b[1;38;2;138;173;244m│ TECHNICAL SKILLS │\x1b[0m +\x1b[1;38;2;138;173;244m└─────────────────────────────────────────────────────┘\x1b[0m + + 🎯 FRONTEND: + ▰▰▰▰▰▰▰▰▰▰ 95% React.js / Next.js + ▰▰▰▰▰▰▰▰▰▱ 90% JS / TS + ▰▰▰▰▰▰▰▰▰▰ 95% Tailwind CSS + ▰▰▰▰▰▰▰▰▱▱ 85% GSAP / Three.js + + ⚙️ BACKEND: + ▰▰▰▰▰▰▰▰▰▱ 90% Node.js / Express.js + ▰▰▰▰▰▰▰▰▱▱ 85% MongoDB / Mongoose + ▰▰▰▰▰▰▰▰▰▱ 90% RESTful APIs + + 🛠️ TOOLS: + ▰▰▰▰▰▰▰▰▰▰ 95% Git / GitHub + ▰▰▰▰▰▰▰▰▰▱ 90% Vercel / Netlify +`; + + case 'projects': + return ` +\x1b[1;38;2;138;173;244m┌─────────────────────────────────────────────────────┐\x1b[0m +\x1b[1;38;2;138;173;244m│ PROJECTS │\x1b[0m +\x1b[1;38;2;138;173;244m└─────────────────────────────────────────────────────┘\x1b[0m + + 🖥️ \x1b[38;2;137;180;250m[1] TERMINAL PORTFOLIO\x1b[0m + Interactive Linux-terminal themed portfolio + 💼 \x1b[38;2;137;180;250m[2] JOBSYNC - JOB PORTAL\x1b[0m + Full-stack job board with role-based auth + 🎨 \x1b[38;2;137;180;250m[3] THIS IS MEGMA\x1b[0m + 3D animated website (GSAP/Three.js) + + 💡 Type \x1b[38;2;249;226;175mproject [number]\x1b[0m for details. +`; + + case 'contact': + return ` +\x1b[1;38;2;138;173;244m┌─────────────────────────────────────────────────────┐\x1b[0m +\x1b[1;38;2;138;173;244m│ CONTACT │\x1b[0m +\x1b[1;38;2;138;173;244m└─────────────────────────────────────────────────────┘\x1b[0m + + 📧 zaynobusiness@gmail.com + 💼 linkedin.com/in/zayn-butt + 🔗 github.com/hey-Zayn + 📱 +92 300-3636-186 +`; + case 'ls': { const targetPathStr = args[0] || '.'; let targetPath: string[]; diff --git a/src/hooks/use-file-system.ts b/src/hooks/use-file-system.ts new file mode 100644 index 0000000..8cf2bce --- /dev/null +++ b/src/hooks/use-file-system.ts @@ -0,0 +1,167 @@ +'use client'; + +import React from 'react'; +import { produce } from 'immer'; +import { useToast } from '@/hooks/use-toast'; +import { + initialFileSystem, + getPath, + getUniqueName, + deleteItem as deleteFsItem, + renameItem as renameFsItem, + DirectorySchema, + type Directory, + type File +} from '@/lib/file-system'; + +export const useFileSystem = () => { + const [fileSystem, setFileSystem] = React.useState(initialFileSystem); + const [isLoaded, setIsLoaded] = React.useState(false); + const { toast } = useToast(); + + const [itemToDelete, setItemToDelete] = React.useState(null); + const [itemToRename, setItemToRename] = React.useState(null); + const [renameValue, setRenameValue] = React.useState(""); + + // Load from localStorage on mount + React.useEffect(() => { + const savedFS = localStorage.getItem('arch-os-fs'); + if (savedFS) { + try { + const parsed = JSON.parse(savedFS); + const validated = DirectorySchema.safeParse(parsed); + + if (validated.success) { + setFileSystem(validated.data); + } else { + console.warn("Invalid file system structure detected, falling back to default", validated.error); + localStorage.removeItem('arch-os-fs'); + } + } catch (e) { + console.error("Failed to parse saved file system", e); + } + } + setIsLoaded(true); + }, []); + + // Debounced save to localStorage on change + React.useEffect(() => { + if (!isLoaded) return; + + const timeoutId = setTimeout(() => { + localStorage.setItem('arch-os-fs', JSON.stringify(fileSystem)); + }, 500); // 500ms debounce + + return () => clearTimeout(timeoutId); + }, [fileSystem, isLoaded]); + + const resetFileSystem = React.useCallback(() => { + setFileSystem(initialFileSystem); + localStorage.removeItem('arch-os-fs'); + toast({ + title: "System Reset", + description: "File system has been restored to default." + }); + }, [toast]); + + const handleCreateItem = React.useCallback((path: string, type: 'file' | 'folder') => { + setFileSystem(produce(draft => { + const pathParts = path.startsWith('~/') ? path.split('/').slice(1) : path === '~' ? [] : path.split('/'); + const parentNode = getPath(pathParts, draft); + + if (parentNode && parentNode.type === 'directory') { + const baseName = type === 'file' ? 'Untitled' : 'New Folder'; + const newName = getUniqueName(parentNode, baseName, type === 'file' ? 'file' : 'directory'); + + if (type === 'file') { + parentNode.children[newName] = { type: 'file', name: newName, content: '' }; + } else { + parentNode.children[newName] = { type: 'directory', name: newName, children: {} }; + } + } + })); + }, []); + + const handleDeleteItem = React.useCallback(() => { + if (!itemToDelete) return; + + setFileSystem(produce(draft => { + deleteFsItem(itemToDelete, draft); + })); + + toast({ + title: "Item Deleted", + description: `"${itemToDelete.split('/').pop()}" was moved to Trash.` + }); + setItemToDelete(null); + }, [itemToDelete, toast]); + + const handleRename = React.useCallback((path: string, name: string) => { + setItemToRename(path); + setRenameValue(name); + }, []); + + const handleRenameSubmit = React.useCallback((e?: React.FormEvent) => { + e?.preventDefault(); + + if (!itemToRename || !renameValue.trim()) { + setItemToRename(null); + return; + } + + const parentPath = itemToRename.substring(0, itemToRename.lastIndexOf('/')) || '~'; + const parentNode = getPath(parentPath === '~' ? [] : parentPath.split('/').slice(1), fileSystem); + + if (parentNode?.type === 'directory' && parentNode.children[renameValue] && itemToRename !== `${parentPath}/${renameValue}`) { + toast({ + variant: 'destructive', + title: "Rename Error", + description: "An item with this name already exists." + }); + return; + } + + setFileSystem(produce(draft => { + renameFsItem(itemToRename, renameValue, draft); + })); + + toast({ + title: "Item Renamed", + description: "Successfully renamed item." + }); + setItemToRename(null); + setRenameValue(""); + }, [itemToRename, renameValue, fileSystem, toast]); + + const handleSaveFile = React.useCallback((path: string, newContent: string) => { + setFileSystem(produce(draft => { + const pathParts = path.startsWith('~/') ? path.split('/').slice(1) : path.split('/'); + const fileNode = getPath(pathParts, draft); + + if (fileNode && fileNode.type === 'file') { + fileNode.content = newContent; + } + })); + + toast({ + title: "File Saved", + description: `Saved ${path}` + }); + }, [toast]); + + return { + fileSystem, + itemToDelete, + setItemToDelete, + itemToRename, + setItemToRename, + renameValue, + setRenameValue, + handleCreateItem, + handleDeleteItem, + handleRename, + handleRenameSubmit, + handleSaveFile, + resetFileSystem + }; +}; diff --git a/src/hooks/use-window-manager.ts b/src/hooks/use-window-manager.ts new file mode 100644 index 0000000..9d3786c --- /dev/null +++ b/src/hooks/use-window-manager.ts @@ -0,0 +1,128 @@ +import React from 'react'; +import { type LucideIcon } from 'lucide-react'; + +export type AppId = 'terminal' | 'settings' | 'home' | 'editor' | 'music' | 'browser' | 'calculator' | 'calendar' | 'mail' | 'messages'; + +export interface App { + id: AppId; + title: string; + icon: LucideIcon; + component?: React.ReactNode; + defaultSize: { width: number; height: number }; + category?: string; + description?: string; +} + +export interface WindowInstance { + id: string; + appId: AppId; + title: string; + isMinimized: boolean; + isMaximized: boolean; + zIndex: number; + path?: string; +} + + +export const useWindowManager = (apps: Record) => { + const [windows, setWindows] = React.useState([]); + const [isGalleryOpen, setIsGalleryOpen] = React.useState(false); + const [isLocked, setIsLocked] = React.useState(true); + + const bringToFront = React.useCallback((id: string) => { + setWindows(prev => { + const maxZIndex = prev.reduce((max, w) => Math.max(max, w.zIndex), 0); + const targetWindow = prev.find(w => w.id === id); + + if (targetWindow && targetWindow.zIndex === maxZIndex && !targetWindow.isMinimized) { + return prev; + } + + return prev.map(w => + w.id === id + ? { ...w, zIndex: maxZIndex + 1, isMinimized: false } + : w + ); + }); + }, []); + + const openWindow = React.useCallback((appId: AppId, path?: string) => { + const app = apps[appId]; + if (!app) return; + + if ((appId === 'home' || appId === 'editor') && path) { + const existingWindow = windows.find(w => w.appId === appId && w.path === path); + if (existingWindow) { + bringToFront(existingWindow.id); + return; + } + } + + const maxZIndex = windows.reduce((max, w) => Math.max(max, w.zIndex), 0); + + let title = app.title; + if ((appId === 'editor' || appId === 'home') && path) { + const parts = path.split('/'); + title = parts[parts.length - 1] || (appId === 'home' ? 'Home' : app.title); + } + + const newWindow: WindowInstance = { + id: crypto.randomUUID(), + appId, + title, + isMinimized: false, + isMaximized: false, + zIndex: maxZIndex + 1, + path + }; + + setWindows(prev => [...prev, newWindow]); + }, [apps, windows, bringToFront]); + + const closeWindow = React.useCallback((id: string) => { + setWindows(prev => prev.filter(w => w.id !== id)); + }, []); + + const toggleMinimize = React.useCallback((id: string) => { + setWindows(prev => prev.map(w => + w.id === id ? { ...w, isMinimized: !w.isMinimized } : w + )); + }, []); + + const toggleMaximize = React.useCallback((id: string) => { + setWindows(prev => { + const maxZIndex = prev.reduce((max, w) => Math.max(max, w.zIndex), 0); + return prev.map(w => + w.id === id + ? { ...w, isMaximized: !w.isMaximized, zIndex: maxZIndex + 1, isMinimized: false } + : w + ); + }); + }, []); + + const toggleGallery = React.useCallback(() => { + setIsGalleryOpen(prev => !prev); + }, []); + + const handleLock = React.useCallback(() => { + setIsLocked(true); + }, []); + + const handleUnlock = React.useCallback(() => { + setIsLocked(false); + }, []); + + return { + windows, + isGalleryOpen, + isLocked, + openWindow, + closeWindow, + bringToFront, + toggleMinimize, + toggleMaximize, + toggleGallery, + handleLock, + handleUnlock + }; +}; diff --git a/src/lib/file-system.ts b/src/lib/file-system.ts index de10804..9b3ebf1 100644 --- a/src/lib/file-system.ts +++ b/src/lib/file-system.ts @@ -1,15 +1,26 @@ +import { z } from 'zod'; -export interface File { - type: 'file'; - name: string; - content: string; -} +export const FileSchema = z.object({ + type: z.literal('file'), + name: z.string(), + content: z.string(), +}); + +export type File = z.infer; -export interface Directory { +export type Directory = { type: 'directory'; name: string; children: { [name: string]: File | Directory }; -} +}; + +export const DirectorySchema: z.ZodType = z.lazy(() => + z.object({ + type: z.literal('directory'), + name: z.string(), + children: z.record(z.union([FileSchema, DirectorySchema])), + }) +); export const initialFileSystem: Directory = { type: 'directory',