Reubencf's picture
Added messaging appLets Chat
062c414
'use client'
import React, { useState, useEffect, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import Window from './Window'
import { Globe, Clock as ClockIcon, Type, Plus } from 'lucide-react'
interface ClockProps {
onClose: () => void
onMinimize?: () => void
onMaximize?: () => void
onFocus?: () => void
zIndex?: number
}
// Helper to get smooth time
const useSmoothTime = () => {
const [time, setTime] = useState(new Date())
useEffect(() => {
let frameId: number
const update = () => {
setTime(new Date())
frameId = requestAnimationFrame(update)
}
update()
return () => cancelAnimationFrame(frameId)
}, [])
return time
}
const AnalogFace = ({ time, city, offset, isSmall = false }: { time: Date, city?: string, offset?: number, isSmall?: boolean }) => {
// Calculate time with offset if provided
const displayTime = offset !== undefined
? new Date(time.getTime() + (time.getTimezoneOffset() * 60000) + (3600000 * offset))
: time
const seconds = displayTime.getSeconds() + displayTime.getMilliseconds() / 1000
const minutes = displayTime.getMinutes() + seconds / 60
const hours = displayTime.getHours() % 12 + minutes / 60
return (
<div className={`relative flex flex-col items-center ${isSmall ? 'gap-2' : 'gap-8'}`}>
<div className={`relative ${isSmall ? 'w-32 h-32' : 'w-64 h-64'} rounded-full bg-[#1e1e1e] shadow-[inset_0_0_20px_rgba(0,0,0,0.5)] border-[3px] border-[#333]`}>
{/* Clock Face Markings */}
{[...Array(12)].map((_, i) => (
<div
key={i}
className="absolute w-full h-full left-0 top-0"
style={{ transform: `rotate(${i * 30}deg)` }}
>
<div className={`absolute left-1/2 -translate-x-1/2 top-2 bg-gray-400 ${isSmall ? 'w-0.5 h-2' : 'w-1 h-3'
}`} />
{!isSmall && (
<span
className="absolute left-1/2 -translate-x-1/2 top-6 text-xl font-medium text-gray-300"
style={{ transform: `rotate(-${i * 30}deg)` }}
>
{i === 0 ? 12 : i}
</span>
)}
</div>
))}
{/* Hands Container */}
<div className="absolute inset-0">
{/* Hour Hand */}
<div
className="absolute left-1/2 top-1/2 bg-white rounded-full origin-bottom shadow-lg z-10"
style={{
width: isSmall ? '3px' : '6px',
height: isSmall ? '25%' : '25%',
transform: `translate(-50%, -100%) rotate(${hours * 30}deg)`,
}}
/>
{/* Minute Hand */}
<div
className="absolute left-1/2 top-1/2 bg-white rounded-full origin-bottom shadow-lg z-10"
style={{
width: isSmall ? '2px' : '4px',
height: isSmall ? '35%' : '38%',
transform: `translate(-50%, -100%) rotate(${minutes * 6}deg)`,
}}
/>
{/* Second Hand (Orange) */}
<div
className="absolute left-1/2 top-1/2 bg-orange-500 rounded-full origin-bottom z-20"
style={{
width: isSmall ? '1px' : '2px',
height: isSmall ? '40%' : '45%',
transform: `translate(-50%, -100%) rotate(${seconds * 6}deg)`,
}}
>
{/* Tail of second hand */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-[20%] w-full h-[20%] bg-orange-500" />
</div>
{/* Center Cap */}
<div className={`absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-black border-2 border-orange-500 z-30 ${isSmall ? 'w-1.5 h-1.5' : 'w-3 h-3'
}`} />
</div>
</div>
{/* Labels */}
<div className="text-center">
<div className={`${isSmall ? 'text-sm' : 'text-2xl'} font-medium text-white`}>
{city || 'Local Time'}
</div>
<div className={`${isSmall ? 'text-xs' : 'text-lg'} text-gray-400 font-mono mt-1`}>
{offset !== undefined
? (offset === 0 ? 'Today' : `${offset > 0 ? '+' : ''}${offset}HRS`)
: displayTime.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
}
</div>
{!isSmall && (
<div className="text-4xl font-light text-white mt-4 tracking-wider">
{displayTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })}
</div>
)}
</div>
</div>
)
}
// List of available cities with offsets
const AVAILABLE_CITIES = [
{ city: 'San Francisco', offset: -8, region: 'USA' },
{ city: 'New York', offset: -5, region: 'USA' },
{ city: 'London', offset: 0, region: 'UK' },
{ city: 'Paris', offset: 1, region: 'Europe' },
{ city: 'Berlin', offset: 1, region: 'Europe' },
{ city: 'Moscow', offset: 3, region: 'Russia' },
{ city: 'Dubai', offset: 4, region: 'UAE' },
{ city: 'Mumbai', offset: 5.5, region: 'India' },
{ city: 'Singapore', offset: 8, region: 'Asia' },
{ city: 'Tokyo', offset: 9, region: 'Japan' },
{ city: 'Sydney', offset: 11, region: 'Australia' },
{ city: 'Auckland', offset: 13, region: 'New Zealand' },
{ city: 'Honolulu', offset: -10, region: 'USA' },
{ city: 'Rio de Janeiro', offset: -3, region: 'Brazil' },
]
const ClockContent = () => {
const time = useSmoothTime()
const [activeTab, setActiveTab] = useState<'world' | 'analog' | 'digital'>('world')
const [showAddCity, setShowAddCity] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [worldClocks, setWorldClocks] = useState([
{ city: 'Cupertino', offset: -8 },
{ city: 'New York', offset: -5 },
{ city: 'London', offset: 0 },
{ city: 'Tokyo', offset: 9 },
])
const tabs = [
{ id: 'world', label: 'World Clock', icon: Globe },
{ id: 'analog', label: 'Analog', icon: ClockIcon },
{ id: 'digital', label: 'Digital', icon: Type },
]
const filteredCities = AVAILABLE_CITIES.filter(c =>
c.city.toLowerCase().includes(searchQuery.toLowerCase()) &&
!worldClocks.some(wc => wc.city === c.city)
)
const addCity = (city: typeof AVAILABLE_CITIES[0]) => {
setWorldClocks([...worldClocks, { city: city.city, offset: city.offset }])
setShowAddCity(false)
setSearchQuery('')
}
return (
<div className="flex flex-col h-full text-white relative">
{/* macOS-style Toolbar */}
<div className="flex items-center justify-center pt-4 pb-6 border-b border-white/10 bg-[#252525]/50">
<div className="flex bg-black/20 p-1 rounded-lg backdrop-blur-md">
{tabs.map((tab) => {
const Icon = tab.icon
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`relative px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200 flex items-center gap-2 ${isActive ? 'text-white' : 'text-gray-400 hover:text-gray-200'
}`}
>
{isActive && (
<motion.div
layoutId="activeTab"
className="absolute inset-0 bg-[#3a3a3a] rounded-md shadow-sm"
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
<span className="relative z-10 flex items-center gap-2">
<Icon size={14} />
{tab.label}
</span>
</button>
)
})}
</div>
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto p-8 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2">
<AnimatePresence mode="wait">
{activeTab === 'world' && !showAddCity && (
<motion.div
key="world"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="grid grid-cols-2 gap-8 justify-items-center"
>
{worldClocks.map((clock) => (
<div key={clock.city} className="relative group">
<AnalogFace
time={time}
city={clock.city}
offset={clock.offset}
isSmall={true}
/>
<button
onClick={() => setWorldClocks(worldClocks.filter(c => c.city !== clock.city))}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg"
>
<Plus size={12} className="rotate-45" />
</button>
</div>
))}
{/* Add Button */}
<button
onClick={() => setShowAddCity(true)}
className="w-32 h-32 rounded-full border-2 border-dashed border-gray-600 flex flex-col items-center justify-center text-gray-500 hover:text-orange-500 hover:border-orange-500 transition-colors group"
>
<Plus size={32} className="group-hover:scale-110 transition-transform" />
<span className="text-xs mt-2 font-medium">Add City</span>
</button>
</motion.div>
)}
{activeTab === 'world' && showAddCity && (
<motion.div
key="add-city"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="flex flex-col h-full max-w-md mx-auto"
>
<div className="flex items-center gap-2 mb-4">
<button
onClick={() => setShowAddCity(false)}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<Plus size={20} className="rotate-45" />
</button>
<input
type="text"
placeholder="Search city..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="flex-1 bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:outline-none focus:border-orange-500 transition-colors"
autoFocus
/>
</div>
<div className="flex-1 overflow-y-auto space-y-2 pr-2 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2">
{filteredCities.map((city) => (
<button
key={city.city}
onClick={() => addCity(city)}
className="w-full flex items-center justify-between p-3 rounded-lg hover:bg-white/10 transition-colors group text-left"
>
<div>
<div className="font-medium text-white">{city.city}</div>
<div className="text-xs text-gray-400">{city.region}</div>
</div>
<div className="text-sm font-mono text-gray-500 group-hover:text-orange-400">
UTC{city.offset >= 0 ? '+' : ''}{city.offset}
</div>
</button>
))}
{filteredCities.length === 0 && (
<div className="text-center text-gray-500 mt-8">
No cities found
</div>
)}
</div>
</motion.div>
)}
{activeTab === 'analog' && (
<motion.div
key="analog"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.05 }}
className="flex items-center justify-center h-full"
>
<AnalogFace time={time} />
</motion.div>
)}
{activeTab === 'digital' && (
<motion.div
key="digital"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex flex-col items-center justify-center h-full"
>
<div className="text-[8rem] leading-none font-light tracking-tighter text-white tabular-nums">
{time.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' })}
</div>
<div className="text-4xl font-light text-orange-500 mt-4 tabular-nums">
{time.getSeconds().toString().padStart(2, '0')}
</div>
<div className="text-xl text-gray-400 mt-8 font-medium tracking-wide uppercase">
{time.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
)
}
export function Clock({ onClose, onMinimize, onMaximize, onFocus, zIndex }: ClockProps) {
return (
<Window
id="clock"
title="Clock"
isOpen={true}
onClose={onClose}
onMinimize={onMinimize}
onMaximize={onMaximize}
onFocus={onFocus}
zIndex={zIndex}
width={600}
height={500}
x={window.innerWidth / 2 - 300}
y={window.innerHeight / 2 - 250}
darkMode={true}
className="clock-app-window !bg-[#1e1e1e]/80 !backdrop-blur-2xl border border-white/10 shadow-2xl !rounded-xl overflow-hidden"
contentClassName="!bg-transparent"
headerClassName="!bg-transparent border-b border-white/5"
>
<ClockContent />
</Window>
)
}