mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 22:37:21 +00:00
Add administrator tools for managing architects and enhance OS widget functionality
Includes service worker registration for offline support, adds bulk actions and filtering to the admin architects page, and implements widget visibility controls and a mobile drawer for the OS. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: ad12b0de-1689-4465-b8e3-8b92d06f17d1 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/4z9y3HV Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
9f20fd9b56
commit
50923682ad
5 changed files with 509 additions and 68 deletions
|
|
@ -28,5 +28,14 @@
|
|||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(reg => console.log('SW registered:', reg.scope))
|
||||
.catch(err => console.error('SW registration failed:', err));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
67
client/public/sw.js
Normal file
67
client/public/sw.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const CACHE_NAME = 'aethex-os-v1';
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/manifest.json',
|
||||
'/favicon.png'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(STATIC_ASSETS);
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name !== CACHE_NAME)
|
||||
.map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method !== 'GET') return;
|
||||
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
const responseClone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(request, responseClone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match(request))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
const fetchPromise = fetch(request).then((response) => {
|
||||
if (response.ok) {
|
||||
const responseClone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(request, responseClone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
return cached || fetchPromise;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -6,7 +6,8 @@ import { useAuth } from "@/lib/auth";
|
|||
import {
|
||||
Users, FileCode, Shield, Activity, LogOut,
|
||||
Home, BarChart3, Settings, User, Search,
|
||||
CheckCircle, XCircle, Eye, Edit, ChevronRight
|
||||
CheckCircle, XCircle, Eye, Edit, ChevronRight,
|
||||
Download, Trash2, Square, CheckSquare
|
||||
} from "lucide-react";
|
||||
|
||||
export default function AdminArchitects() {
|
||||
|
|
@ -14,6 +15,9 @@ export default function AdminArchitects() {
|
|||
const [, setLocation] = useLocation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedProfile, setSelectedProfile] = useState<any>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [roleFilter, setRoleFilter] = useState<string>("all");
|
||||
const [verifiedFilter, setVerifiedFilter] = useState<string>("all");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: profiles, isLoading } = useQuery({
|
||||
|
|
@ -40,10 +44,70 @@ export default function AdminArchitects() {
|
|||
},
|
||||
});
|
||||
|
||||
const filteredProfiles = profiles?.filter((p: any) =>
|
||||
p.username?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.email?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
) || [];
|
||||
const filteredProfiles = useMemo(() => {
|
||||
if (!profiles) return [];
|
||||
return profiles.filter((p: any) => {
|
||||
const matchesSearch = p.username?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.email?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesRole = roleFilter === "all" || p.role === roleFilter;
|
||||
const matchesVerified = verifiedFilter === "all" ||
|
||||
(verifiedFilter === "verified" && p.is_verified) ||
|
||||
(verifiedFilter === "unverified" && !p.is_verified);
|
||||
return matchesSearch && matchesRole && matchesVerified;
|
||||
});
|
||||
}, [profiles, searchQuery, roleFilter, verifiedFilter]);
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedIds.size === filteredProfiles.length) {
|
||||
setSelectedIds(new Set());
|
||||
} else {
|
||||
setSelectedIds(new Set(filteredProfiles.map((p: any) => p.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelect = (id: string) => {
|
||||
const newSet = new Set(selectedIds);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
setSelectedIds(newSet);
|
||||
};
|
||||
|
||||
const exportToCSV = () => {
|
||||
const dataToExport = selectedIds.size > 0
|
||||
? filteredProfiles.filter((p: any) => selectedIds.has(p.id))
|
||||
: filteredProfiles;
|
||||
|
||||
const headers = ["Username", "Email", "Role", "Level", "XP", "Status", "Verified"];
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
...dataToExport.map((p: any) => [
|
||||
p.username || "",
|
||||
p.email || "",
|
||||
p.role || "",
|
||||
p.level || 0,
|
||||
p.total_xp || 0,
|
||||
p.status || "",
|
||||
p.is_verified ? "Yes" : "No"
|
||||
].join(","))
|
||||
].join("\n");
|
||||
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `architects_${new Date().toISOString().split("T")[0]}.csv`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
const bulkVerify = async (verify: boolean) => {
|
||||
const ids = Array.from(selectedIds);
|
||||
for (const id of ids) {
|
||||
await updateProfileMutation.mutateAsync({ id, updates: { is_verified: verify } });
|
||||
}
|
||||
setSelectedIds(new Set());
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
|
|
@ -100,35 +164,115 @@ export default function AdminArchitects() {
|
|||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-8">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
|
||||
Architects
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
{profiles?.length || 0} registered architects
|
||||
{filteredProfiles.length} of {profiles?.length || 0} architects
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search architects..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="bg-card border border-white/10 pl-10 pr-4 py-2 text-sm text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none w-64"
|
||||
data-testid="input-search"
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<select
|
||||
value={roleFilter}
|
||||
onChange={(e) => setRoleFilter(e.target.value)}
|
||||
className="bg-card border border-white/10 px-3 py-2 text-sm text-white focus:border-primary/50 focus:outline-none"
|
||||
data-testid="filter-role"
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="oversee">Overseer</option>
|
||||
<option value="employee">Employee</option>
|
||||
<option value="member">Member</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={verifiedFilter}
|
||||
onChange={(e) => setVerifiedFilter(e.target.value)}
|
||||
className="bg-card border border-white/10 px-3 py-2 text-sm text-white focus:border-primary/50 focus:outline-none"
|
||||
data-testid="filter-verified"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="verified">Verified</option>
|
||||
<option value="unverified">Unverified</option>
|
||||
</select>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search architects..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="bg-card border border-white/10 pl-10 pr-4 py-2 text-sm text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none w-64"
|
||||
data-testid="input-search"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="flex items-center gap-4 mb-4 p-3 bg-primary/10 border border-primary/30 rounded">
|
||||
<span className="text-sm text-white">{selectedIds.size} selected</span>
|
||||
<button
|
||||
onClick={() => bulkVerify(true)}
|
||||
className="px-3 py-1 bg-green-500/20 text-green-400 text-sm rounded hover:bg-green-500/30 transition-colors"
|
||||
data-testid="bulk-verify"
|
||||
>
|
||||
Verify Selected
|
||||
</button>
|
||||
<button
|
||||
onClick={() => bulkVerify(false)}
|
||||
className="px-3 py-1 bg-red-500/20 text-red-400 text-sm rounded hover:bg-red-500/30 transition-colors"
|
||||
data-testid="bulk-unverify"
|
||||
>
|
||||
Revoke Selected
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedIds(new Set())}
|
||||
className="px-3 py-1 text-muted-foreground text-sm hover:text-white transition-colors"
|
||||
>
|
||||
Clear Selection
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={exportToCSV}
|
||||
className="flex items-center gap-2 px-3 py-1 bg-white/10 text-white text-sm rounded hover:bg-white/20 transition-colors"
|
||||
data-testid="export-csv"
|
||||
>
|
||||
<Download className="w-4 h-4" /> Export CSV
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedIds.size === 0 && (
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
onClick={exportToCSV}
|
||||
className="flex items-center gap-2 px-3 py-1 bg-white/10 text-white text-sm rounded hover:bg-white/20 transition-colors"
|
||||
data-testid="export-csv-all"
|
||||
>
|
||||
<Download className="w-4 h-4" /> Export All to CSV
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-card/50 border border-white/10 overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10 text-left">
|
||||
<th className="p-4 w-10">
|
||||
<button onClick={toggleSelectAll} className="text-muted-foreground hover:text-white transition-colors">
|
||||
{selectedIds.size === filteredProfiles.length && filteredProfiles.length > 0 ? (
|
||||
<CheckSquare className="w-5 h-5 text-primary" />
|
||||
) : (
|
||||
<Square className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">User</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Role</th>
|
||||
<th className="p-4 text-xs text-muted-foreground uppercase tracking-wider font-bold">Level</th>
|
||||
|
|
@ -141,19 +285,28 @@ export default function AdminArchitects() {
|
|||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-8 text-center text-muted-foreground">
|
||||
<td colSpan={8} className="p-8 text-center text-muted-foreground">
|
||||
Loading...
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredProfiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-8 text-center text-muted-foreground">
|
||||
<td colSpan={8} className="p-8 text-center text-muted-foreground">
|
||||
No architects found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredProfiles.map((profile: any) => (
|
||||
<tr key={profile.id} className="border-b border-white/5 hover:bg-white/5 transition-colors">
|
||||
<tr key={profile.id} className={`border-b border-white/5 hover:bg-white/5 transition-colors ${selectedIds.has(profile.id) ? 'bg-primary/5' : ''}`}>
|
||||
<td className="p-4">
|
||||
<button onClick={() => toggleSelect(profile.id)} className="text-muted-foreground hover:text-white transition-colors">
|
||||
{selectedIds.has(profile.id) ? (
|
||||
<CheckSquare className="w-5 h-5 text-primary" />
|
||||
) : (
|
||||
<Square className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
|
|
|
|||
|
|
@ -1187,6 +1187,47 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
const saved = localStorage.getItem('aethex-widget-positions');
|
||||
return saved ? JSON.parse(saved) : getDefaultWidgetPositions();
|
||||
});
|
||||
const [positionResetKey, setPositionResetKey] = useState(0);
|
||||
const [widgetVisibility, setWidgetVisibility] = useState<Record<string, boolean>>(() => {
|
||||
const saved = localStorage.getItem('aethex-widget-visibility');
|
||||
return saved ? JSON.parse(saved) : { clock: true, weather: true, status: true, notifications: true, leaderboard: true, pipeline: true, kpi: true, heartbeat: true };
|
||||
});
|
||||
const [showWidgetSettings, setShowWidgetSettings] = useState(false);
|
||||
const [mobileWidgetsOpen, setMobileWidgetsOpen] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const toggleWidgetVisibility = (id: string) => {
|
||||
setWidgetVisibility(prev => {
|
||||
const updated = { ...prev, [id]: !prev[id] };
|
||||
localStorage.setItem('aethex-widget-visibility', JSON.stringify(updated));
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const resetWidgetPositions = () => {
|
||||
const defaults = getDefaultWidgetPositions();
|
||||
setWidgetPositions(defaults);
|
||||
setPositionResetKey(k => k + 1);
|
||||
localStorage.setItem('aethex-widget-positions', JSON.stringify(defaults));
|
||||
};
|
||||
|
||||
const widgetOptions = [
|
||||
{ id: 'clock', label: 'Clock' },
|
||||
{ id: 'weather', label: 'Weather' },
|
||||
{ id: 'status', label: 'System Status' },
|
||||
{ id: 'notifications', label: 'Notifications' },
|
||||
{ id: 'leaderboard', label: 'Leaderboard' },
|
||||
{ id: 'pipeline', label: 'Pipeline' },
|
||||
{ id: 'kpi', label: 'KPI Dashboard' },
|
||||
{ id: 'heartbeat', label: 'Network Heartbeat' },
|
||||
];
|
||||
|
||||
const { data: metrics } = useQuery({
|
||||
queryKey: ['os-metrics'],
|
||||
|
|
@ -1234,21 +1275,190 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
return { color: 'text-cyan-400', icon: <Users className="w-3 h-3" /> };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<DraggableWidget id="clock" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
|
||||
<div className="p-3">
|
||||
<div className="text-2xl font-mono text-white font-bold">
|
||||
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
<div className="text-xs text-white/50 font-mono">
|
||||
{time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })}
|
||||
</div>
|
||||
</div>
|
||||
</DraggableWidget>
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setMobileWidgetsOpen(!mobileWidgetsOpen)}
|
||||
className="fixed top-4 right-4 z-50 w-10 h-10 bg-slate-900/90 backdrop-blur-xl border border-white/20 rounded-lg flex items-center justify-center text-white/70 hover:text-white transition-colors pointer-events-auto"
|
||||
data-testid="mobile-widgets-toggle"
|
||||
>
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{mobileWidgetsOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 300 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 300 }}
|
||||
className="fixed top-0 right-0 bottom-12 w-72 bg-slate-900/95 backdrop-blur-xl border-l border-white/10 overflow-y-auto z-40 pointer-events-auto"
|
||||
>
|
||||
<div className="p-4 border-b border-white/10 flex items-center justify-between sticky top-0 bg-slate-900/95">
|
||||
<span className="text-sm text-white/70 uppercase tracking-wider">Widgets</span>
|
||||
<button onClick={() => setMobileWidgetsOpen(false)} className="text-white/50 hover:text-white">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
{widgetVisibility.clock !== false && (
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<div className="text-2xl font-mono text-white font-bold">
|
||||
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
<div className="text-xs text-white/50 font-mono">
|
||||
{time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{widgetVisibility.weather !== false && weather?.current_weather && (
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">Weather</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{getWeatherIcon(weather.current_weather.weathercode)}</span>
|
||||
<div>
|
||||
<div className="text-xl font-mono text-white">{Math.round(weather.current_weather.temperature)}°F</div>
|
||||
<div className="text-xs text-white/50">Wind: {weather.current_weather.windspeed} mph</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{widgetVisibility.status !== false && metrics && (
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">System Status</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs font-mono">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Architects</span>
|
||||
<span className="text-cyan-400">{metrics.totalProfiles || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Projects</span>
|
||||
<span className="text-purple-400">{metrics.totalProjects || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Verified</span>
|
||||
<span className="text-yellow-400">{metrics.verifiedUsers || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Online</span>
|
||||
<span className="text-green-400">{metrics.onlineUsers || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{widgetVisibility.notifications !== false && notifications && notifications.length > 0 && (
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">Notifications</div>
|
||||
<div className="space-y-1.5 text-xs">
|
||||
{notifications.slice(0, 4).map((n, i) => {
|
||||
const cat = getNotificationCategory(n);
|
||||
return (
|
||||
<div key={i} className={`flex items-center gap-2 ${cat.color}`}>
|
||||
{cat.icon}
|
||||
<span className="truncate text-white/70">{n}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{widgetVisibility.leaderboard !== false && leaderboard && leaderboard.length > 0 && (
|
||||
<div className="bg-white/5 rounded-lg p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||
<Award className="w-3 h-3 text-yellow-400" />
|
||||
Top Architects
|
||||
</div>
|
||||
<div className="space-y-1.5 text-xs font-mono">
|
||||
{leaderboard.map((arch: any, i: number) => (
|
||||
<div key={arch.id} className="flex items-center gap-2">
|
||||
<span className={`w-4 text-center ${i === 0 ? 'text-yellow-400' : i === 1 ? 'text-gray-300' : i === 2 ? 'text-amber-600' : 'text-white/40'}`}>
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="flex-1 truncate text-white/80">{arch.username || arch.display_name}</span>
|
||||
<span className="text-cyan-400">Lv{arch.level || 1}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
{weather?.current_weather && (
|
||||
<DraggableWidget id="weather" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 hidden md:block">
|
||||
<button
|
||||
onClick={() => setShowWidgetSettings(true)}
|
||||
className="fixed top-4 left-4 z-50 w-8 h-8 bg-slate-900/80 backdrop-blur-xl border border-white/20 rounded-lg flex items-center justify-center text-white/50 hover:text-white transition-colors pointer-events-auto"
|
||||
data-testid="widget-settings-btn"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{showWidgetSettings && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm pointer-events-auto"
|
||||
onClick={() => setShowWidgetSettings(false)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="bg-slate-900/95 backdrop-blur-xl border border-white/20 rounded-xl p-6 w-80"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-white font-display uppercase tracking-wider">Widget Settings</h3>
|
||||
<button onClick={() => setShowWidgetSettings(false)} className="text-white/50 hover:text-white">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2 mb-4">
|
||||
{widgetOptions.map(opt => (
|
||||
<label key={opt.id} className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={widgetVisibility[opt.id] !== false}
|
||||
onChange={() => toggleWidgetVisibility(opt.id)}
|
||||
className="w-4 h-4 rounded border-white/30 bg-white/10 text-cyan-500 focus:ring-cyan-500"
|
||||
/>
|
||||
<span className="text-white/80 text-sm">{opt.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={resetWidgetPositions}
|
||||
className="w-full py-2 bg-white/10 hover:bg-white/20 text-white/80 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
Reset Positions
|
||||
</button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{widgetVisibility.clock !== false && (
|
||||
<DraggableWidget key={`clock-${positionResetKey}`} id="clock" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
|
||||
<div className="p-3">
|
||||
<div className="text-2xl font-mono text-white font-bold">
|
||||
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
<div className="text-xs text-white/50 font-mono">
|
||||
{time.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' })}
|
||||
</div>
|
||||
</div>
|
||||
</DraggableWidget>
|
||||
)}
|
||||
|
||||
{widgetVisibility.weather !== false && weather?.current_weather && (
|
||||
<DraggableWidget key={`weather-${positionResetKey}`} id="weather" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">Weather</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -1262,8 +1472,8 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
</DraggableWidget>
|
||||
)}
|
||||
|
||||
{metrics && (
|
||||
<DraggableWidget id="status" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
|
||||
{widgetVisibility.status !== false && metrics && (
|
||||
<DraggableWidget key={`status-${positionResetKey}`} id="status" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">System Status</div>
|
||||
<div className="space-y-1.5 text-xs font-mono">
|
||||
|
|
@ -1294,8 +1504,8 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
</DraggableWidget>
|
||||
)}
|
||||
|
||||
{notifications && notifications.length > 0 && (
|
||||
<DraggableWidget id="notifications" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
|
||||
{widgetVisibility.notifications !== false && notifications && notifications.length > 0 && (
|
||||
<DraggableWidget key={`notifications-${positionResetKey}`} id="notifications" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2">Notifications</div>
|
||||
<div className="space-y-1.5 text-xs max-h-24 overflow-y-auto">
|
||||
|
|
@ -1313,8 +1523,8 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
</DraggableWidget>
|
||||
)}
|
||||
|
||||
{leaderboard && leaderboard.length > 0 && (
|
||||
<DraggableWidget id="leaderboard" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
|
||||
{widgetVisibility.leaderboard !== false && leaderboard && leaderboard.length > 0 && (
|
||||
<DraggableWidget key={`leaderboard-${positionResetKey}`} id="leaderboard" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||
<Award className="w-3 h-3 text-yellow-400" />
|
||||
|
|
@ -1335,8 +1545,8 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
</DraggableWidget>
|
||||
)}
|
||||
|
||||
{metrics && (
|
||||
<DraggableWidget id="pipeline" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
|
||||
{widgetVisibility.pipeline !== false && metrics && (
|
||||
<DraggableWidget key={`pipeline-${positionResetKey}`} id="pipeline" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||
<Layers className="w-3 h-3 text-purple-400" />
|
||||
|
|
@ -1375,8 +1585,8 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
</DraggableWidget>
|
||||
)}
|
||||
|
||||
{metrics && (
|
||||
<DraggableWidget id="kpi" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
|
||||
{widgetVisibility.kpi !== false && metrics && (
|
||||
<DraggableWidget key={`kpi-${positionResetKey}`} id="kpi" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-52">
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||
<BarChart3 className="w-3 h-3 text-cyan-400" />
|
||||
|
|
@ -1404,30 +1614,32 @@ function DesktopWidgets({ time, weather, notifications }: {
|
|||
</DraggableWidget>
|
||||
)}
|
||||
|
||||
<DraggableWidget id="heartbeat" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||
<Activity className="w-3 h-3 text-red-400" />
|
||||
Network Pulse
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }}
|
||||
className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center"
|
||||
>
|
||||
{widgetVisibility.heartbeat !== false && (
|
||||
<DraggableWidget key={`heartbeat-${positionResetKey}`} id="heartbeat" positions={widgetPositions} onPositionChange={handlePositionChange} className="w-48">
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-white/50 uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||
<Activity className="w-3 h-3 text-red-400" />
|
||||
Network Pulse
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.1, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", delay: 0.1 }}
|
||||
className="w-4 h-4 rounded-full bg-red-500"
|
||||
/>
|
||||
</motion.div>
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }}
|
||||
className="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.1, 1] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", delay: 0.1 }}
|
||||
className="w-4 h-4 rounded-full bg-red-500"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="text-center text-xs text-white/60 font-mono">
|
||||
<span className="text-green-400">●</span> All Systems Operational
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center text-xs text-white/60 font-mono">
|
||||
<span className="text-green-400">●</span> All Systems Operational
|
||||
</div>
|
||||
</div>
|
||||
</DraggableWidget>
|
||||
</DraggableWidget>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue