Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created November 21, 2025 09:53
Show Gist options
  • Select an option

  • Save shricodev/3f82d6037608b5212df462ea993ba231 to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/3f82d6037608b5212df462ea993ba231 to your computer and use it in GitHub Desktop.
Google's Gemini 3 Pro UI Test (Windows 11) - Blog Demo
'use client';
import React, { useState, useEffect, useCallback } from 'react';
export default function Calculator() {
const [display, setDisplay] = useState('0');
const [firstOperand, setFirstOperand] = useState<number | null>(null);
const [operator, setOperator] = useState<string | null>(null);
const [waitingForSecondOperand, setWaitingForSecondOperand] = useState(false);
const inputDigit = useCallback((digit: string) => {
if (waitingForSecondOperand) {
setDisplay(digit);
setWaitingForSecondOperand(false);
} else {
setDisplay(display === '0' ? digit : display + digit);
}
}, [waitingForSecondOperand, display]);
const inputDot = useCallback(() => {
if (!display.includes('.')) {
setDisplay(display + '.');
setWaitingForSecondOperand(false);
}
}, [display]);
const clear = useCallback(() => {
setDisplay('0');
setFirstOperand(null);
setOperator(null);
setWaitingForSecondOperand(false);
}, []);
const backspace = useCallback(() => {
if (waitingForSecondOperand) return;
setDisplay(display.length > 1 ? display.slice(0, -1) : '0');
}, [display, waitingForSecondOperand]);
const performCalculation = (op: string, first: number, second: number) => {
switch (op) {
case '+': return first + second;
case '-': return first - second;
case '*': return first * second;
case '/': return first / second;
case '=': return second;
default: return second;
}
};
const handleOperator = useCallback((nextOperator: string) => {
const inputValue = parseFloat(display);
if (operator && waitingForSecondOperand) {
setOperator(nextOperator);
return;
}
if (firstOperand === null) {
setFirstOperand(inputValue);
} else if (operator) {
const result = performCalculation(operator, firstOperand, inputValue);
// Prevent overflow or long decimals
const formattedResult = parseFloat(result.toFixed(8));
setDisplay(String(formattedResult));
setFirstOperand(formattedResult);
}
setWaitingForSecondOperand(true);
setOperator(nextOperator);
}, [display, operator, waitingForSecondOperand, firstOperand]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const key = e.key;
if (/[0-9]/.test(key)) {
inputDigit(key);
} else if (key === '.') {
inputDot();
} else if (key === 'Enter' || key === '=') {
e.preventDefault();
handleOperator('=');
} else if (key === 'Backspace') {
backspace();
} else if (key === 'Escape') {
clear();
} else if (key === '+' || key === '-' || key === '*' || key === '/') {
handleOperator(key);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [inputDigit, inputDot, handleOperator, clear, backspace]);
const Button = ({ label, onClick, className = "" }: { label: string, onClick: () => void, className?: string }) => (
<button
onClick={onClick}
className={`flex-1 rounded-lg text-lg font-medium hover:bg-opacity-80 active:scale-95 active:bg-opacity-60 transition-all shadow-sm border border-black/5 dark:border-white/5 ${className}`}
>
{label}
</button>
);
return (
<div className="flex flex-col h-full bg-[#f3f3f3] dark:bg-[#202020] p-1">
<div className="h-16 flex items-end justify-end px-4 pb-2 text-4xl font-semibold text-black dark:text-white overflow-hidden select-all">
{display}
</div>
<div className="grid grid-cols-4 grid-rows-5 gap-1 p-1 flex-1">
<Button label="C" onClick={clear} className="bg-white/50 dark:bg-white/5 hover:bg-white/80 dark:hover:bg-white/10 text-black dark:text-white" />
<Button label="⌫" onClick={backspace} className="bg-white/50 dark:bg-white/5 hover:bg-white/80 dark:hover:bg-white/10 text-black dark:text-white" />
<Button label="%" onClick={() => {}} className="bg-white/50 dark:bg-white/5 hover:bg-white/80 dark:hover:bg-white/10 text-black dark:text-white" />
<Button label="÷" onClick={() => handleOperator('/')} className="bg-orange-400/10 dark:bg-orange-400/20 hover:bg-orange-400/20 text-orange-600 dark:text-orange-400" />
<Button label="7" onClick={() => inputDigit('7')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="8" onClick={() => inputDigit('8')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="9" onClick={() => inputDigit('9')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="×" onClick={() => handleOperator('*')} className="bg-orange-400/10 dark:bg-orange-400/20 hover:bg-orange-400/20 text-orange-600 dark:text-orange-400" />
<Button label="4" onClick={() => inputDigit('4')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="5" onClick={() => inputDigit('5')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="6" onClick={() => inputDigit('6')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="-" onClick={() => handleOperator('-')} className="bg-orange-400/10 dark:bg-orange-400/20 hover:bg-orange-400/20 text-orange-600 dark:text-orange-400" />
<Button label="1" onClick={() => inputDigit('1')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="2" onClick={() => inputDigit('2')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="3" onClick={() => inputDigit('3')} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="+" onClick={() => handleOperator('+')} className="bg-orange-400/10 dark:bg-orange-400/20 hover:bg-orange-400/20 text-orange-600 dark:text-orange-400" />
<Button label="0" onClick={() => inputDigit('0')} className="bg-white dark:bg-gray-700 text-black dark:text-white col-span-2" />
<Button label="." onClick={inputDot} className="bg-white dark:bg-gray-700 text-black dark:text-white" />
<Button label="=" onClick={() => handleOperator('=')} className="bg-blue-500 hover:bg-blue-600 text-white" />
</div>
</div>
);
}
'use client';
import React from 'react';
import { useOS, APPS, AppId } from '@/app/context/OSContext';
import { getWallpaperUrl } from '@/app/utils/wallpapers';
import Taskbar from './Taskbar';
import StartMenu from './StartMenu';
import WindowFrame from './WindowFrame';
import Calculator from '../apps/Calculator';
import Notepad from '../apps/Notepad';
import Explorer from '../apps/Explorer';
import Settings from '../apps/Settings';
import { CalculatorIcon, NotepadIcon, FolderIcon, SettingsIcon, BrowserIcon, TrashIcon } from '../icons';
const DesktopIcon = ({ id, label, onClick }: { id: string, label: string, onClick: () => void }) => {
const Icon = () => {
switch(id) {
case 'calculator': return <CalculatorIcon size={32} className="text-orange-500 drop-shadow-sm" />;
case 'notepad': return <NotepadIcon size={32} className="text-blue-400 drop-shadow-sm" />;
case 'explorer': return <FolderIcon size={32} className="text-yellow-400 drop-shadow-sm" />;
case 'settings': return <SettingsIcon size={32} className="text-gray-500 drop-shadow-sm" />;
case 'browser': return <BrowserIcon size={32} className="text-blue-600 drop-shadow-sm" />;
case 'recycle-bin': return <TrashIcon size={32} className="text-gray-500 drop-shadow-sm" />;
default: return <div className="w-8 h-8 bg-gray-400 rounded"></div>;
}
};
return (
<button
className="flex flex-col items-center gap-1 p-2 w-24 hover:bg-white/10 hover:backdrop-blur-sm rounded border border-transparent hover:border-white/20 transition-all text-shadow group"
onDoubleClick={onClick}
onClick={onClick} // Allow single click for simplicity on web, usually double click on desktop
>
<Icon />
<span className="text-xs text-white font-medium text-center drop-shadow-md line-clamp-2 group-hover:text-white">
{label}
</span>
</button>
);
};
const AppContainer = ({ appId }: { appId: AppId }) => {
switch (appId) {
case 'calculator': return <Calculator />;
case 'notepad': return <Notepad />;
case 'explorer': return <Explorer />;
case 'settings': return <Settings />;
case 'browser': return <div className="w-full h-full flex items-center justify-center bg-white dark:bg-gray-900 text-gray-500">Browser Placeholder</div>;
case 'recycle-bin': return <div className="w-full h-full flex items-center justify-center bg-white dark:bg-gray-900 text-gray-500">Recycle Bin is empty</div>;
default: return <div className="p-4">App not found</div>;
}
};
export default function Desktop() {
const { wallpaper, windows, openWindow, closeStartMenu, theme } = useOS();
const bgUrl = getWallpaperUrl(wallpaper);
const desktopIcons: AppId[] = ['recycle-bin', 'explorer', 'browser', 'notepad', 'calculator', 'settings'];
return (
<div
className={`relative h-screen w-screen overflow-hidden flex flex-col select-none ${theme === 'dark' ? 'dark' : ''}`}
style={{
backgroundImage: `url("${bgUrl}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundColor: '#1a1a1a'
}}
onClick={closeStartMenu}
>
{/* Desktop Icons Area */}
<div className="flex-1 relative p-4 grid grid-flow-col grid-rows-[repeat(auto-fill,100px)] gap-4 content-start items-start w-min">
{desktopIcons.map(appId => (
<DesktopIcon
key={appId}
id={appId}
label={APPS[appId].title}
onClick={() => openWindow(appId)}
/>
))}
</div>
{/* Windows Layer */}
{windows.map(win => (
<WindowFrame key={win.id} windowState={win}>
<AppContainer appId={win.appId} />
</WindowFrame>
))}
{/* UI Layer */}
<StartMenu />
<Taskbar />
</div>
);
}
'use client';
import React from 'react';
import { FolderIcon, BrowserIcon, NotepadIcon } from '../icons';
export default function Explorer() {
return (
<div className="flex h-full bg-[#f0f4f9] dark:bg-[#191919] text-sm text-black dark:text-white select-none">
{/* Sidebar */}
<div className="w-48 flex flex-col gap-1 p-2 border-r border-gray-200 dark:border-gray-800 overflow-y-auto">
<div className="px-2 py-1.5 font-semibold text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">Quick Access</div>
<div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default">
<FolderIcon className="text-blue-400" size={16} /> Desktop
</div>
<div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default">
<FolderIcon className="text-blue-400" size={16} /> Downloads
</div>
<div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default">
<FolderIcon className="text-blue-400" size={16} /> Documents
</div>
<div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default">
<FolderIcon className="text-blue-400" size={16} /> Pictures
</div>
<div className="mt-4 px-2 py-1.5 font-semibold text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wider">This PC</div>
<div className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default">
<div className="w-4 h-4 bg-gray-400 rounded-sm"></div> Local Disk (C:)
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col bg-white dark:bg-[#202020]">
{/* Toolbar */}
<div className="h-10 flex items-center gap-4 px-4 border-b border-gray-200 dark:border-gray-800">
<button className="px-3 py-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex items-center gap-2">
<span className="text-blue-500 text-lg">+</span> New
</button>
<div className="h-4 w-[1px] bg-gray-300 dark:bg-gray-700"></div>
<button className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">Sort</button>
<button className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">View</button>
</div>
{/* File Grid */}
<div className="p-4 grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-4 overflow-y-auto">
<div className="flex flex-col items-center gap-2 p-2 hover:bg-blue-50 dark:hover:bg-white/5 hover:ring-1 hover:ring-blue-200 rounded cursor-default group">
<FolderIcon size={48} className="text-yellow-400 drop-shadow-sm" />
<span className="text-center truncate w-full group-hover:text-blue-600">Work</span>
</div>
<div className="flex flex-col items-center gap-2 p-2 hover:bg-blue-50 dark:hover:bg-white/5 hover:ring-1 hover:ring-blue-200 rounded cursor-default group">
<FolderIcon size={48} className="text-yellow-400 drop-shadow-sm" />
<span className="text-center truncate w-full group-hover:text-blue-600">Personal</span>
</div>
<div className="flex flex-col items-center gap-2 p-2 hover:bg-blue-50 dark:hover:bg-white/5 hover:ring-1 hover:ring-blue-200 rounded cursor-default group">
<NotepadIcon size={48} className="text-blue-500 drop-shadow-sm" />
<span className="text-center truncate w-full group-hover:text-blue-600">notes.txt</span>
</div>
<div className="flex flex-col items-center gap-2 p-2 hover:bg-blue-50 dark:hover:bg-white/5 hover:ring-1 hover:ring-blue-200 rounded cursor-default group">
<div className="w-12 h-12 bg-blue-500 rounded flex items-center justify-center text-white font-bold text-xs">IMG</div>
<span className="text-center truncate w-full group-hover:text-blue-600">vacation.jpg</span>
</div>
</div>
</div>
</div>
);
}
import React from 'react';
export const IconProps = {
className: '',
size: 24,
};
type IconComponent = React.FC<{ className?: string; size?: number }>;
export const StartIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm10 0h8v8h-8v-8z" />
</svg>
);
export const SearchIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
);
export const CalculatorIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<rect x="4" y="2" width="16" height="20" rx="2" ry="2" />
<line x1="8" y1="6" x2="16" y2="6" />
<line x1="16" y1="14" x2="16" y2="14" />
<line x1="16" y1="18" x2="16" y2="18" />
<line x1="12" y1="14" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="18" />
<line x1="8" y1="14" x2="8" y2="14" />
<line x1="8" y1="18" x2="8" y2="18" />
</svg>
);
export const NotepadIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
);
export const FolderIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
);
export const SettingsIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
);
export const BrowserIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="4" />
<line x1="21.17" y1="8" x2="12" y2="8" />
<line x1="3.95" y1="6.06" x2="8.54" y2="14" />
<line x1="10.88" y1="21.94" x2="15.46" y2="14" />
</svg>
);
export const TrashIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
);
export const WifiIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M5 12.55a11 11 0 0 1 14.08 0" />
<path d="M1.42 9a16 16 0 0 1 21.16 0" />
<path d="M8.53 16.11a6 6 0 0 1 6.95 0" />
<line x1="12" y1="20" x2="12.01" y2="20" />
</svg>
);
export const VolumeIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
</svg>
);
export const BatteryIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<rect x="1" y="6" width="18" height="12" rx="2" ry="2" />
<line x1="23" y1="13" x2="23" y2="11" />
</svg>
);
export const UserIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
);
export const CloseIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
);
export const MinusIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
);
export const MaximizeIcon: IconComponent = ({ className, size = 24 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
</svg>
);
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
'use client';
import React, { useState, useEffect } from 'react';
import { useOS } from '@/app/context/OSContext';
import { UserIcon } from '../icons';
export default function LoginScreen() {
const { login } = useOS();
const [username, setUsername] = useState('User');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [isLocked, setIsLocked] = useState(true);
const [time, setTime] = useState<Date | null>(null);
useEffect(() => {
setTime(new Date());
const timer = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (username.trim()) {
setLoading(true);
setTimeout(() => {
login(username);
setLoading(false);
}, 1500);
}
};
const formatTime = (date: Date) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
};
const formatDate = (date: Date) => {
return date.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
};
// Lock Screen
if (isLocked) {
return (
<div
className="relative h-screen w-screen overflow-hidden bg-cover bg-center flex flex-col items-center justify-center text-white select-none cursor-default"
style={{
backgroundImage: 'url("https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1920&auto=format&fit=crop")',
backgroundColor: '#1a1a1a'
}}
onClick={() => setIsLocked(false)}
>
<div className="flex flex-col items-center gap-4 transform -translate-y-32">
{time && (
<>
<div className="text-9xl font-semibold tracking-tighter drop-shadow-md">{formatTime(time)}</div>
<div className="text-3xl font-medium drop-shadow-md">{formatDate(time)}</div>
</>
)}
</div>
<div className="absolute bottom-12 text-sm font-medium animate-bounce opacity-80">Click to sign in</div>
</div>
);
}
// Login Form
return (
<div className="relative h-screen w-screen overflow-hidden bg-cover bg-center flex items-center justify-center animate-in fade-in zoom-in duration-300"
style={{
backgroundImage: 'url("https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1920&auto=format&fit=crop")',
backgroundColor: '#1a1a1a'
}}
>
{/* Acrylic overlay */}
<div className="absolute inset-0 bg-black/10 backdrop-blur-sm transition-all duration-500"></div>
<div className="relative z-10 w-full max-w-sm flex flex-col items-center animate-in slide-in-from-bottom-8 duration-500">
<div className="w-48 h-48 bg-gray-200/20 rounded-full flex items-center justify-center mb-6 shadow-2xl">
<UserIcon size={96} className="text-white drop-shadow-lg" />
</div>
<h1 className="text-2xl font-semibold text-white mb-8 drop-shadow-md">{username}</h1>
{loading ? (
<div className="flex flex-col items-center gap-4">
<div className="w-8 h-8 border-4 border-white/30 border-t-white rounded-full animate-spin"></div>
<span className="text-white font-medium">Welcome</span>
</div>
) : (
<form onSubmit={handleLogin} className="w-full flex flex-col items-center gap-4">
<div className="w-full bg-black/30 backdrop-blur-md rounded-md border-b-2 border-white/50 hover:border-white focus-within:border-white focus-within:bg-white/90 group transition-colors">
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 bg-transparent text-white placeholder-gray-300 focus:text-black focus:outline-none text-center transition-colors group-focus-within:text-black group-focus-within:placeholder-gray-500"
/>
</div>
<button
type="submit"
className="mt-2 px-8 py-2 bg-white/20 hover:bg-white/30 backdrop-blur-md text-white rounded-md border border-white/10 transition font-medium shadow-lg"
>
Sign in
</button>
<div onClick={() => setIsLocked(true)} className="mt-8 text-white/80 text-sm hover:text-white cursor-pointer flex items-center gap-2">
<span>← Lock Screen</span>
</div>
</form>
)}
</div>
</div>
);
}
'use client';
import React, { useState } from 'react';
export default function Notepad() {
const [text, setText] = useState('');
return (
<div className="flex flex-col h-full bg-white dark:bg-[#202020] text-black dark:text-white">
<div className="flex items-center gap-2 px-2 py-1 text-sm border-b border-gray-200 dark:border-gray-700">
<span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">File</span>
<span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">Edit</span>
<span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">Format</span>
<span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">View</span>
<span className="hover:bg-gray-200 dark:hover:bg-gray-700 px-2 py-0.5 rounded cursor-default">Help</span>
</div>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
className="flex-1 w-full h-full p-4 resize-none outline-none bg-transparent font-mono text-sm"
spellCheck={false}
placeholder="Type something..."
/>
<div className="h-6 bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 flex items-center px-2 text-xs text-gray-500">
<span className="ml-auto">Ln 1, Col {text.length + 1}</span>
<span className="ml-4">UTF-8</span>
<span className="ml-4">Windows (CRLF)</span>
</div>
</div>
);
}
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
export type AppId = 'calculator' | 'notepad' | 'explorer' | 'settings' | 'browser' | 'recycle-bin';
export interface AppConfig {
id: AppId;
title: string;
icon: string; // We'll use string keys for icons
defaultWidth: number;
defaultHeight: number;
}
export const APPS: Record<AppId, AppConfig> = {
calculator: { id: 'calculator', title: 'Calculator', icon: 'calculator', defaultWidth: 320, defaultHeight: 480 },
notepad: { id: 'notepad', title: 'Notepad', icon: 'notepad', defaultWidth: 600, defaultHeight: 400 },
explorer: { id: 'explorer', title: 'File Explorer', icon: 'folder', defaultWidth: 800, defaultHeight: 500 },
settings: { id: 'settings', title: 'Settings', icon: 'settings', defaultWidth: 700, defaultHeight: 500 },
browser: { id: 'browser', title: 'Edge', icon: 'browser', defaultWidth: 900, defaultHeight: 600 },
'recycle-bin': { id: 'recycle-bin', title: 'Recycle Bin', icon: 'trash', defaultWidth: 600, defaultHeight: 400 },
};
export interface WindowState {
id: string; // unique instance id
appId: AppId;
title: string;
isMinimized: boolean;
isMaximized: boolean;
zIndex: number;
position: { x: number; y: number };
size: { width: number; height: number };
}
interface OSContextType {
isLoggedIn: boolean;
username: string;
login: (username: string) => void;
logout: () => void;
windows: WindowState[];
activeWindowId: string | null;
openWindow: (appId: AppId) => void;
closeWindow: (id: string) => void;
minimizeWindow: (id: string) => void;
maximizeWindow: (id: string) => void; // Toggle
focusWindow: (id: string) => void;
updateWindowPosition: (id: string, position: { x: number; y: number }) => void;
isStartMenuOpen: boolean;
toggleStartMenu: () => void;
closeStartMenu: () => void;
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
wallpaper: string;
setWallpaper: (url: string) => void;
}
const OSContext = createContext<OSContextType | undefined>(undefined);
export const OSProvider = ({ children }: { children: ReactNode }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [username, setUsername] = useState('');
const [windows, setWindows] = useState<WindowState[]>([]);
const [activeWindowId, setActiveWindowId] = useState<string | null>(null);
const [isStartMenuOpen, setIsStartMenuOpen] = useState(false);
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [wallpaper, setWallpaper] = useState('default'); // 'default' or other keys
const [nextZIndex, setNextZIndex] = useState(100);
const login = (user: string) => {
setUsername(user);
setIsLoggedIn(true);
// Optional: Save to local storage
localStorage.setItem('os_username', user);
};
const logout = () => {
setIsLoggedIn(false);
setUsername('');
setWindows([]);
localStorage.removeItem('os_username');
};
useEffect(() => {
const savedUser = localStorage.getItem('os_username');
if (savedUser) {
login(savedUser);
}
}, []);
const openWindow = (appId: AppId) => {
const app = APPS[appId];
// Check if app allows multiple instances. For simplicity, let's say Calculator does, others maybe not?
// Actually, standard windows behavior is multiple instances usually, but for a web sim,
// single instance for some apps might be easier. Let's allow multiples for now.
const id = `${appId}-${Date.now()}`;
// Center the window roughly
const startX = 100 + (windows.length * 20);
const startY = 100 + (windows.length * 20);
const newWindow: WindowState = {
id,
appId,
title: app.title,
isMinimized: false,
isMaximized: false,
zIndex: nextZIndex,
position: { x: startX, y: startY },
size: { width: app.defaultWidth, height: app.defaultHeight },
};
setWindows([...windows, newWindow]);
setActiveWindowId(id);
setNextZIndex(prev => prev + 1);
setIsStartMenuOpen(false);
};
const closeWindow = (id: string) => {
setWindows(prev => prev.filter(w => w.id !== id));
if (activeWindowId === id) {
setActiveWindowId(null);
}
};
const minimizeWindow = (id: string) => {
setWindows(prev => prev.map(w => w.id === id ? { ...w, isMinimized: true } : w));
if (activeWindowId === id) {
setActiveWindowId(null);
}
};
const maximizeWindow = (id: string) => {
setWindows(prev => prev.map(w => w.id === id ? { ...w, isMaximized: !w.isMaximized } : w));
focusWindow(id);
};
const focusWindow = (id: string) => {
setActiveWindowId(id);
setWindows(prev => prev.map(w => w.id === id ? { ...w, isMinimized: false, zIndex: nextZIndex } : w));
setNextZIndex(prev => prev + 1);
};
const updateWindowPosition = (id: string, position: { x: number; y: number }) => {
setWindows(prev => prev.map(w => w.id === id ? { ...w, position } : w));
};
const toggleStartMenu = () => setIsStartMenuOpen(prev => !prev);
const closeStartMenu = () => setIsStartMenuOpen(false);
return (
<OSContext.Provider value={{
isLoggedIn,
username,
login,
logout,
windows,
activeWindowId,
openWindow,
closeWindow,
minimizeWindow,
maximizeWindow,
focusWindow,
updateWindowPosition,
isStartMenuOpen,
toggleStartMenu,
closeStartMenu,
theme,
setTheme,
wallpaper,
setWallpaper
}}>
{children}
</OSContext.Provider>
);
};
export const useOS = () => {
const context = useContext(OSContext);
if (context === undefined) {
throw new Error('useOS must be used within an OSProvider');
}
return context;
};
'use client';
import { OSProvider, useOS } from './context/OSContext';
import LoginScreen from './components/os/LoginScreen';
import Desktop from './components/os/Desktop';
function AppContent() {
const { isLoggedIn } = useOS();
return (
<>
{isLoggedIn ? <Desktop /> : <LoginScreen />}
</>
);
}
export default function Home() {
return (
<OSProvider>
<AppContent />
</OSProvider>
);
}
'use client';
import React from 'react';
import { useOS } from '@/app/context/OSContext';
export default function Settings() {
const { theme, setTheme, wallpaper, setWallpaper } = useOS();
const wallpapers = [
{ id: 'default', url: 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', name: 'Blue Gradient' },
{ id: 'mountain', url: 'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', name: 'Mountains' },
{ id: 'dark', url: 'https://images.unsplash.com/photo-1550684848-fac1c5b4e853?ixlib=rb-4.0.3&auto=format&fit=crop&w=1920&q=80', name: 'Dark Mode' },
];
return (
<div className="flex h-full bg-[#f3f3f3] dark:bg-[#202020] text-black dark:text-white">
<div className="w-64 p-4 border-r border-gray-200 dark:border-gray-700 flex flex-col gap-2">
<div className="text-xl font-semibold mb-4 px-2">Settings</div>
<div className="bg-white dark:bg-white/10 px-3 py-2 rounded-md font-medium shadow-sm">Personalization</div>
<div className="px-3 py-2 rounded-md hover:bg-gray-200 dark:hover:bg-white/5 text-gray-600 dark:text-gray-400">System</div>
<div className="px-3 py-2 rounded-md hover:bg-gray-200 dark:hover:bg-white/5 text-gray-600 dark:text-gray-400">Bluetooth & devices</div>
<div className="px-3 py-2 rounded-md hover:bg-gray-200 dark:hover:bg-white/5 text-gray-600 dark:text-gray-400">Network & internet</div>
</div>
<div className="flex-1 p-8 overflow-y-auto">
<h2 className="text-2xl font-semibold mb-6">Personalization</h2>
<div className="mb-8">
<div className="text-sm font-medium mb-3 text-gray-500">Select a theme</div>
<div className="flex gap-4">
<button
onClick={() => setTheme('light')}
className={`flex flex-col items-center gap-2 p-2 rounded-lg border-2 ${theme === 'light' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-transparent hover:bg-gray-200 dark:hover:bg-white/5'}`}
>
<div className="w-32 h-20 bg-white rounded shadow-md border border-gray-200"></div>
<span className="text-sm">Light</span>
</button>
<button
onClick={() => setTheme('dark')}
className={`flex flex-col items-center gap-2 p-2 rounded-lg border-2 ${theme === 'dark' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-transparent hover:bg-gray-200 dark:hover:bg-white/5'}`}
>
<div className="w-32 h-20 bg-[#202020] rounded shadow-md border border-gray-700"></div>
<span className="text-sm">Dark</span>
</button>
</div>
</div>
<div>
<div className="text-sm font-medium mb-3 text-gray-500">Background</div>
<div className="grid grid-cols-3 gap-4">
{wallpapers.map((wp) => (
<button
key={wp.id}
onClick={() => setWallpaper(wp.id)}
className={`group relative rounded-lg overflow-hidden aspect-video border-2 transition-all ${wallpaper === wp.id ? 'border-blue-500 ring-2 ring-blue-200' : 'border-transparent hover:border-gray-300'}`}
>
<img src={wp.url} alt={wp.name} className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors"></div>
<span className="absolute bottom-2 left-2 text-xs text-white font-medium shadow-black drop-shadow-md">{wp.name}</span>
</button>
))}
</div>
</div>
</div>
</div>
);
}
'use client';
import React, { useState } from 'react';
import { useOS, APPS, AppId } from '@/app/context/OSContext';
import { SearchIcon, CalculatorIcon, NotepadIcon, FolderIcon, SettingsIcon, BrowserIcon, UserIcon, TrashIcon } from '../icons';
const AppIcon = ({ id, size = 32 }: { id: string, size?: number }) => {
switch(id) {
case 'calculator': return <CalculatorIcon size={size} className="text-orange-500" />;
case 'notepad': return <NotepadIcon size={size} className="text-blue-400" />;
case 'explorer': return <FolderIcon size={size} className="text-yellow-400" />;
case 'settings': return <SettingsIcon size={size} className="text-gray-500 dark:text-gray-400" />;
case 'browser': return <BrowserIcon size={size} className="text-blue-600" />;
case 'recycle-bin': return <TrashIcon size={size} className="text-gray-500" />;
default: return <div className="w-8 h-8 bg-gray-400 rounded"></div>;
}
};
export default function StartMenu() {
const { isStartMenuOpen, openWindow, username, logout, closeStartMenu } = useOS();
const [searchText, setSearchText] = useState('');
if (!isStartMenuOpen) return null;
const filteredApps = Object.values(APPS).filter(app =>
app.title.toLowerCase().includes(searchText.toLowerCase())
);
const handleAppClick = (id: AppId) => {
openWindow(id);
};
return (
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 w-[640px] bg-[#f3f3f3]/95 dark:bg-[#1c1c1c]/95 backdrop-blur-2xl rounded-xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-6 flex flex-col gap-6 animate-in slide-in-from-bottom-4 fade-in duration-200 z-50" onClick={(e) => e.stopPropagation()}>
{/* Search */}
<div className="relative">
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none">
<SearchIcon size={18} className="text-gray-500" />
</div>
<input
type="text"
placeholder="Type here to search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-full bg-white dark:bg-[#2c2c2c] border border-gray-200 dark:border-gray-700/50 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 outline-none text-sm shadow-sm transition-all"
/>
</div>
{/* Pinned Section */}
<div className="flex-1">
<div className="flex justify-between items-center mb-4 px-4">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Pinned</span>
<button className="px-2 py-1 text-xs font-medium bg-white dark:bg-white/10 rounded hover:bg-gray-50 dark:hover:bg-white/20 shadow-sm border border-gray-200 dark:border-gray-600">All apps &gt;</button>
</div>
<div className="grid grid-cols-6 gap-4">
{filteredApps.map(app => (
<button
key={app.id}
onClick={() => handleAppClick(app.id)}
className="flex flex-col items-center gap-2 p-2 rounded hover:bg-white/50 dark:hover:bg-white/10 transition-colors"
>
<AppIcon id={app.id} />
<span className="text-xs text-gray-700 dark:text-gray-300 truncate w-full text-center">{app.title}</span>
</button>
))}
</div>
</div>
{/* Recommended Section (Fake) */}
<div>
<div className="flex justify-between items-center mb-4 px-4">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Recommended</span>
<button className="px-2 py-1 text-xs font-medium bg-white dark:bg-white/10 rounded hover:bg-gray-50 dark:hover:bg-white/20 shadow-sm border border-gray-200 dark:border-gray-600">More &gt;</button>
</div>
<div className="grid grid-cols-2 gap-2 px-2">
<div className="flex items-center gap-3 p-2 rounded hover:bg-white/50 dark:hover:bg-white/10 transition-colors cursor-default">
<NotepadIcon size={20} className="text-blue-400" />
<div className="flex flex-col">
<span className="text-xs font-medium">Project_Proposal.txt</span>
<span className="text-[10px] text-gray-500">Yesterday at 4:20 PM</span>
</div>
</div>
<div className="flex items-center gap-3 p-2 rounded hover:bg-white/50 dark:hover:bg-white/10 transition-colors cursor-default">
<BrowserIcon size={20} className="text-blue-600" />
<div className="flex flex-col">
<span className="text-xs font-medium">Next.js Documentation</span>
<span className="text-[10px] text-gray-500">Just now</span>
</div>
</div>
</div>
</div>
{/* Bottom User Bar */}
<div className="h-14 -mx-6 -mb-6 mt-2 bg-[#e9e9e9]/50 dark:bg-[#202020]/50 flex items-center justify-between px-8 border-t border-gray-200/50 dark:border-gray-700/50 rounded-b-xl">
<div className="flex items-center gap-3 hover:bg-white/50 dark:hover:bg-white/10 p-2 rounded cursor-pointer transition-colors">
<div className="w-8 h-8 bg-gradient-to-tr from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-white text-xs font-bold shadow-sm">
{username.substring(0, 1).toUpperCase()}
</div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">{username}</span>
</div>
<button
onClick={logout}
className="p-2 rounded hover:bg-white/50 dark:hover:bg-white/10 text-gray-600 dark:text-gray-400 hover:text-red-500 transition-colors"
title="Sign out"
>
<div className="w-5 h-5 border-2 border-current rounded-full border-t-transparent rotate-45"></div>
</button>
</div>
</div>
);
}
'use client';
import React, { useState, useEffect } from 'react';
import { useOS, APPS, AppId } from '@/app/context/OSContext';
import { StartIcon, WifiIcon, VolumeIcon, BatteryIcon, CalculatorIcon, NotepadIcon, FolderIcon, SettingsIcon, BrowserIcon, TrashIcon } from '../icons';
const AppIcon = ({ id, size = 24 }: { id: string, size?: number }) => {
switch(id) {
case 'calculator': return <CalculatorIcon size={size} className="text-orange-500" />;
case 'notepad': return <NotepadIcon size={size} className="text-blue-400" />;
case 'explorer': return <FolderIcon size={size} className="text-yellow-400" />;
case 'settings': return <SettingsIcon size={size} className="text-gray-500 dark:text-gray-400" />;
case 'browser': return <BrowserIcon size={size} className="text-blue-600" />;
case 'recycle-bin': return <TrashIcon size={size} className="text-gray-500" />;
default: return <div className="w-6 h-6 bg-gray-400 rounded"></div>;
}
};
export default function Taskbar() {
const { toggleStartMenu, isStartMenuOpen, openWindow, windows, activeWindowId, minimizeWindow, focusWindow } = useOS();
const [time, setTime] = useState<Date | null>(null);
useEffect(() => {
setTime(new Date());
const timer = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
const formatTime = (date: Date) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
const formatDate = (date: Date) => {
return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
};
const pinnedApps: AppId[] = ['explorer', 'browser', 'notepad', 'calculator', 'settings'];
const handleAppClick = (appId: AppId) => {
// Check if window exists
const existingWindow = windows.find(w => w.appId === appId && !w.isMinimized);
const existingMinimized = windows.find(w => w.appId === appId && w.isMinimized);
if (existingWindow) {
if (activeWindowId === existingWindow.id) {
minimizeWindow(existingWindow.id);
} else {
focusWindow(existingWindow.id);
}
} else if (existingMinimized) {
focusWindow(existingMinimized.id);
} else {
openWindow(appId);
}
};
const isAppOpen = (appId: AppId) => windows.some(w => w.appId === appId);
const isAppActive = (appId: AppId) => {
const win = windows.find(w => w.id === activeWindowId);
return win?.appId === appId;
};
return (
<div className="h-12 bg-[#f3f3f3]/85 dark:bg-[#202020]/85 backdrop-blur-md border-t border-white/20 dark:border-gray-700 flex items-center justify-between px-4 relative z-50 select-none">
<div className="flex-1"></div> {/* Spacer */}
{/* Center Dock */}
<div className="flex items-center gap-1 h-full">
<button
onClick={toggleStartMenu}
className={`p-2 rounded hover:bg-white/50 dark:hover:bg-white/10 transition-all active:scale-95 ${isStartMenuOpen ? 'bg-white/50 dark:bg-white/10' : ''}`}
title="Start"
>
<StartIcon className="text-blue-600 dark:text-blue-400" />
</button>
{pinnedApps.map(appId => (
<button
key={appId}
onClick={() => handleAppClick(appId)}
className={`relative group p-2 rounded hover:bg-white/50 dark:hover:bg-white/10 transition-all active:scale-95 flex flex-col items-center justify-center h-10 w-10 ${isAppActive(appId) ? 'bg-white/50 dark:bg-white/10' : ''}`}
>
<AppIcon id={appId} />
{isAppOpen(appId) && (
<div className={`absolute bottom-0 w-1.5 h-1 rounded-full transition-all ${isAppActive(appId) ? 'bg-blue-500 w-4' : 'bg-gray-400'}`}></div>
)}
<span className="absolute -top-10 bg-gray-800 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
{APPS[appId].title}
</span>
</button>
))}
</div>
{/* System Tray */}
<div className="flex-1 flex items-center justify-end gap-2 h-full">
<div className="hidden sm:flex items-center gap-1 hover:bg-white/50 dark:hover:bg-white/10 px-2 py-1 rounded transition-colors cursor-default">
<span className="text-xs text-gray-500 dark:text-gray-400">ENG</span>
</div>
<div className="flex items-center gap-3 hover:bg-white/50 dark:hover:bg-white/10 px-2 py-1 rounded transition-colors cursor-default">
<WifiIcon size={16} />
<VolumeIcon size={16} />
<BatteryIcon size={16} />
</div>
<div className="flex flex-col items-end justify-center px-2 py-1 hover:bg-white/50 dark:hover:bg-white/10 rounded transition-colors cursor-default leading-tight">
{time && (
<>
<span className="text-xs font-medium text-gray-800 dark:text-gray-200">{formatTime(time)}</span>
<span className="text-[10px] text-gray-600 dark:text-gray-400">{formatDate(time)}</span>
</>
)}
</div>
<div className="w-1 h-full border-l border-gray-300 dark:border-gray-600 ml-2"></div>
</div>
</div>
);
}
export const WALLPAPERS = [
{ id: 'default', url: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1920&auto=format&fit=crop', name: 'Windows Light' },
{ id: 'dark', url: 'https://images.unsplash.com/photo-1483728642387-6c3bdd6c93e5?q=80&w=1920&auto=format&fit=crop', name: 'Windows Dark' },
{ id: 'mountain', url: 'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?q=80&w=1920&auto=format&fit=crop', name: 'Mountains' },
];
export const getWallpaperUrl = (id: string) => {
return WALLPAPERS.find(w => w.id === id)?.url || WALLPAPERS[0].url;
};
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { useOS, WindowState, APPS } from '@/app/context/OSContext';
import { CloseIcon, MinusIcon, MaximizeIcon } from '../icons';
interface WindowFrameProps {
windowState: WindowState;
children: React.ReactNode;
}
export default function WindowFrame({ windowState, children }: WindowFrameProps) {
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, updateWindowPosition } = useOS();
const [isDragging, setIsDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [currentPos, setCurrentPos] = useState(windowState.position);
const [isMaximized, setIsMaximized] = useState(windowState.isMaximized);
const appConfig = APPS[windowState.appId];
useEffect(() => {
// Sync prop changes to local state if needed (e.g. if changed externally)
setCurrentPos(windowState.position);
setIsMaximized(windowState.isMaximized);
}, [windowState.position, windowState.isMaximized]);
const handleMouseDown = (e: React.MouseEvent) => {
focusWindow(windowState.id);
};
const handleDragStart = (e: React.PointerEvent) => {
if (isMaximized) return;
e.preventDefault();
setIsDragging(true);
const rect = (e.target as HTMLElement).closest('.window-frame')?.getBoundingClientRect();
if (rect) {
setDragOffset({
x: e.clientX - rect.left,
y: e.clientY - rect.top
});
}
(e.target as HTMLElement).setPointerCapture(e.pointerId);
};
const handleDrag = (e: React.PointerEvent) => {
if (!isDragging) return;
e.preventDefault();
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
setCurrentPos({ x: newX, y: newY });
};
const handleDragEnd = (e: React.PointerEvent) => {
if (!isDragging) return;
setIsDragging(false);
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
updateWindowPosition(windowState.id, currentPos);
};
if (windowState.isMinimized) return null;
const style: React.CSSProperties = isMaximized ? {
top: 0,
left: 0,
width: '100%',
height: 'calc(100% - 48px)', // Subtract taskbar height
transform: 'none',
borderRadius: 0,
} : {
transform: `translate(${currentPos.x}px, ${currentPos.y}px)`,
width: windowState.size.width,
height: windowState.size.height,
};
return (
<div
className={`window-frame absolute flex flex-col bg-white dark:bg-gray-800 shadow-2xl rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 transition-shadow animate-in fade-in zoom-in-95 duration-200 ${isMaximized ? '' : 'rounded-lg'}`}
style={{
...style,
zIndex: windowState.zIndex,
}}
onMouseDown={handleMouseDown}
>
{/* Title Bar */}
<div
className="h-10 bg-gray-100 dark:bg-gray-900 flex items-center justify-between px-2 select-none border-b border-gray-200 dark:border-gray-700"
onPointerDown={handleDragStart}
onPointerMove={handleDrag}
onPointerUp={handleDragEnd}
>
<div className="flex items-center gap-2 pl-2">
{/* We could render the app icon here */}
<span className="font-medium text-sm text-gray-700 dark:text-gray-300">{windowState.title}</span>
</div>
<div className="flex items-center h-full">
<button
onClick={(e) => { e.stopPropagation(); minimizeWindow(windowState.id); }}
className="w-10 h-10 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-500 transition-colors"
>
<MinusIcon size={14} />
</button>
<button
onClick={(e) => { e.stopPropagation(); maximizeWindow(windowState.id); }}
className="w-10 h-10 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-500 transition-colors"
>
<MaximizeIcon size={12} />
</button>
<button
onClick={(e) => { e.stopPropagation(); closeWindow(windowState.id); }}
className="w-10 h-10 flex items-center justify-center hover:bg-red-500 hover:text-white text-gray-500 transition-colors"
>
<CloseIcon size={14} />
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto relative">
{children}
</div>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment