From 3f2671fb17b13e0a711693dd3d2aff0b0706fb85 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Sun, 21 Dec 2025 19:28:51 +0000 Subject: [PATCH] Make widgets draggable and improve their positioning behavior Add a DraggableWidget component to allow users to reposition widgets on the screen. Includes updates to default widget positioning logic to handle SSR and improve drag bounds. Also adds the 'Layers' icon. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: d57b4210-8a86-408c-b932-5ff569ef7aa4 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/paZzfbE Replit-Helium-Checkpoint-Created: true --- client/src/pages/os.tsx | 371 +++++++++++++++++++++++++++++++++------- 1 file changed, 313 insertions(+), 58 deletions(-) diff --git a/client/src/pages/os.tsx b/client/src/pages/os.tsx index 9853414..51c1fec 100644 --- a/client/src/pages/os.tsx +++ b/client/src/pages/os.tsx @@ -8,7 +8,7 @@ import { Terminal, FileText, IdCard, Music, Settings, Globe, X, Minus, Square, Maximize2, Volume2, Wifi, Battery, ChevronUp, FolderOpen, Award, MessageCircle, Send, - ExternalLink, User, LogOut, BarChart3, Loader2, + ExternalLink, User, LogOut, BarChart3, Loader2, Layers, Presentation, Bell, Image, Monitor, Play, Pause, ChevronRight, Network, Activity, Code2, Radio, Newspaper, Gamepad2, Users, Trophy, Calculator, StickyNote, Cpu, Camera, @@ -1075,11 +1075,119 @@ export default function AeThexOS() { ); } +interface WidgetPosition { + x: number; + y: number; +} + +interface WidgetPositions { + [key: string]: WidgetPosition; +} + +function getDefaultWidgetPositions(): WidgetPositions { + const w = typeof window !== 'undefined' ? window.innerWidth : 1200; + const h = typeof window !== 'undefined' ? window.innerHeight : 800; + return { + clock: { x: w - 220, y: 16 }, + weather: { x: w - 220, y: 100 }, + status: { x: w - 220, y: 200 }, + notifications: { x: w - 220, y: 320 }, + leaderboard: { x: w - 440, y: 16 }, + pipeline: { x: w - 440, y: 180 }, + kpi: { x: w - 440, y: 340 }, + heartbeat: { x: 16, y: h - 180 }, + }; +} + +function DraggableWidget({ + id, + children, + positions, + onPositionChange, + className = "" +}: { + id: string; + children: React.ReactNode; + positions: WidgetPositions; + onPositionChange: (id: string, pos: WidgetPosition) => void; + className?: string; +}) { + const [isDragging, setIsDragging] = useState(false); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const widgetRef = useRef(null); + + const defaultPositions = getDefaultWidgetPositions(); + const position = positions[id] || defaultPositions[id] || { x: 100, y: 100 }; + + const handleMouseDown = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).closest('.widget-drag-handle')) { + e.preventDefault(); + setIsDragging(true); + setDragOffset({ + x: e.clientX - position.x, + y: e.clientY - position.y + }); + } + }; + + useEffect(() => { + if (!isDragging) return; + + const handleMouseMove = (e: MouseEvent) => { + const newX = Math.max(0, Math.min(window.innerWidth - 50, e.clientX - dragOffset.x)); + const newY = Math.max(0, Math.min(window.innerHeight - 60, e.clientY - dragOffset.y)); + onPositionChange(id, { x: newX, y: newY }); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, dragOffset, id, onPositionChange]); + + return ( + +
+
+
+
+
+
+
+ {children} + + ); +} + function DesktopWidgets({ time, weather, notifications }: { time: Date; weather?: { current_weather?: { temperature: number; windspeed: number; weathercode: number } }; notifications?: string[]; }) { + const [widgetPositions, setWidgetPositions] = useState(() => { + const saved = localStorage.getItem('aethex-widget-positions'); + return saved ? JSON.parse(saved) : getDefaultWidgetPositions(); + }); + const { data: metrics } = useQuery({ queryKey: ['os-metrics'], queryFn: async () => { @@ -1089,6 +1197,24 @@ function DesktopWidgets({ time, weather, notifications }: { refetchInterval: 30000, }); + const { data: leaderboard } = useQuery({ + queryKey: ['os-leaderboard'], + queryFn: async () => { + const res = await fetch('/api/directory/architects'); + const data = await res.json(); + return data.slice(0, 5); + }, + refetchInterval: 60000, + }); + + const handlePositionChange = useCallback((id: string, pos: WidgetPosition) => { + setWidgetPositions(prev => { + const updated = { ...prev, [id]: pos }; + localStorage.setItem('aethex-widget-positions', JSON.stringify(updated)); + return updated; + }); + }, []); + const getWeatherIcon = (code: number) => { if (code === 0) return '☀️'; if (code <= 3) return '⛅'; @@ -1100,79 +1226,208 @@ function DesktopWidgets({ time, weather, notifications }: { return '⛈️'; }; + const getNotificationCategory = (text: string) => { + if (text.toLowerCase().includes('security') || text.toLowerCase().includes('aegis')) + return { color: 'text-green-400', icon: }; + if (text.toLowerCase().includes('project')) + return { color: 'text-purple-400', icon: }; + return { color: 'text-cyan-400', icon: }; + }; + return ( -
- -
- {time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+ +
+
+ {time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+ {time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })} +
-
- {time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })} -
- +
{weather?.current_weather && ( - -
Weather
-
- {getWeatherIcon(weather.current_weather.weathercode)} -
-
{Math.round(weather.current_weather.temperature)}°F
-
Wind: {weather.current_weather.windspeed} mph
+ +
+
Weather
+
+ {getWeatherIcon(weather.current_weather.weathercode)} +
+
{Math.round(weather.current_weather.temperature)}°F
+
Wind: {weather.current_weather.windspeed} mph
+
- +
)} {metrics && ( - -
System Status
-
-
- Architects - {metrics.totalProfiles || 0} -
-
- Projects - {metrics.totalProjects || 0} -
-
- Online - {metrics.onlineUsers || 0} + +
+
System Status
+
+
+ Architects +
+ {metrics.totalProfiles || 0} + +
+
+
+ Projects +
+ {metrics.totalProjects || 0} + +
+
+
+ Online + {metrics.onlineUsers || 0} +
+
+ Verified + {metrics.verifiedUsers || 0} +
- +
)} {notifications && notifications.length > 0 && ( - -
Notifications
-
- {notifications.slice(0, 3).map((n, i) => ( -
{n}
- ))} + +
+
Notifications
+
+ {notifications.slice(0, 4).map((n, i) => { + const cat = getNotificationCategory(n); + return ( +
+ {cat.icon} + {n} +
+ ); + })} +
- +
)} + + {leaderboard && leaderboard.length > 0 && ( + +
+
+ + Top Architects +
+
+ {leaderboard.map((arch: any, i: number) => ( +
+ + {i + 1} + + {arch.username || arch.display_name} + Lv{arch.level || 1} +
+ ))} +
+
+
+ )} + + {metrics && ( + +
+
+ + Project Pipeline +
+
+
+
+ Active + {metrics.totalProjects || 0} +
+
+
+
+
+
+
+ In Review + 2 +
+
+
+
+
+
+
+ Completed + 12 +
+
+
+
+
+
+
+ + )} + + {metrics && ( + +
+
+ + Key Metrics +
+
+
+
{metrics.totalXP || 0}
+
Total XP
+
+
+
{metrics.avgLevel || 1}
+
Avg Level
+
+
+
{metrics.verifiedUsers || 0}
+
Verified
+
+
+
98%
+
Uptime
+
+
+
+
+ )} + + +
+
+ + Network Pulse +
+
+ + + +
+
+ All Systems Operational +
+
+
); }