From 1b84dbd36d393f20754f76fc2677ec4bafa2fad4 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Fri, 9 Jan 2026 15:12:35 +0100 Subject: [PATCH] feat: (ui) new design --- web/build.ts | 2 +- web/src/App.tsx | 16 +- web/src/components/ActivityChart.tsx | 125 ------ web/src/components/AppSidebar.tsx | 97 ----- web/src/components/ControlPanel.tsx | 116 ------ web/src/components/feature-card.tsx | 48 +++ web/src/components/info-card.tsx | 30 ++ web/src/components/section-header.tsx | 39 ++ web/src/components/testimonial-card.tsx | 41 ++ web/src/components/ui/badge.tsx | 37 ++ web/src/components/ui/button.tsx | 2 + web/src/components/ui/card.tsx | 2 +- web/src/hooks/use-activity-stats.ts | 50 --- web/src/hooks/use-dashboard-stats.ts | 143 ------- web/src/hooks/use-mobile.ts | 19 - web/src/hooks/use-socket.ts | 61 +++ web/src/layouts/DashboardLayout.tsx | 35 -- web/src/pages/Activity.tsx | 113 ------ web/src/pages/Dashboard.tsx | 319 +++------------ web/src/pages/DesignSystem.tsx | 305 ++++++++++++++ web/src/pages/Home.tsx | 236 +++++++++++ web/src/pages/Settings.tsx | 509 ------------------------ web/styles/globals.css | 214 +++++++--- 23 files changed, 1023 insertions(+), 1536 deletions(-) delete mode 100644 web/src/components/ActivityChart.tsx delete mode 100644 web/src/components/AppSidebar.tsx delete mode 100644 web/src/components/ControlPanel.tsx create mode 100644 web/src/components/feature-card.tsx create mode 100644 web/src/components/info-card.tsx create mode 100644 web/src/components/section-header.tsx create mode 100644 web/src/components/testimonial-card.tsx create mode 100644 web/src/components/ui/badge.tsx delete mode 100644 web/src/hooks/use-activity-stats.ts delete mode 100644 web/src/hooks/use-dashboard-stats.ts delete mode 100644 web/src/hooks/use-mobile.ts create mode 100644 web/src/hooks/use-socket.ts delete mode 100644 web/src/layouts/DashboardLayout.tsx delete mode 100644 web/src/pages/Activity.tsx create mode 100644 web/src/pages/DesignSystem.tsx create mode 100644 web/src/pages/Home.tsx delete mode 100644 web/src/pages/Settings.tsx diff --git a/web/build.ts b/web/build.ts index d655e48..5c7850f 100644 --- a/web/build.ts +++ b/web/build.ts @@ -136,7 +136,7 @@ const build = async () => { target: "browser", sourcemap: "linked", define: { - "process.env.NODE_ENV": JSON.stringify("production"), + "process.env.NODE_ENV": JSON.stringify((cliConfig as any).watch ? "development" : "production"), }, ...cliConfig, }); diff --git a/web/src/App.tsx b/web/src/App.tsx index bbcab54..f6362d3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,10 +1,8 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; -import { DashboardLayout } from "./layouts/DashboardLayout"; -import { Dashboard } from "./pages/Dashboard"; -import { Activity } from "./pages/Activity"; -import { Settings } from "./pages/Settings"; import "./index.css"; - +import { Dashboard } from "./pages/Dashboard"; +import { DesignSystem } from "./pages/DesignSystem"; +import { Home } from "./pages/Home"; import { Toaster } from "sonner"; export function App() { @@ -12,11 +10,9 @@ export function App() { - }> - } /> - } /> - } /> - + } /> + } /> + } /> ); diff --git a/web/src/components/ActivityChart.tsx b/web/src/components/ActivityChart.tsx deleted file mode 100644 index bdf1aaf..0000000 --- a/web/src/components/ActivityChart.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from 'recharts'; -import type { ActivityData } from '../hooks/use-activity-stats'; - -interface ActivityChartProps { - data: ActivityData[]; - loading?: boolean; -} - -const CustomTooltip = ({ active, payload }: any) => { - if (active && payload && payload.length) { - const data = payload[0].payload; - - return ( -
-

- {data.displayTime} -

-
-

- Commands - {payload[0].value} -

-

- Transactions - {payload[1].value} -

-
-
- ); - } - return null; -}; - -export const ActivityChart: React.FC = ({ data, loading }) => { - if (loading) { - return ( -
-
-
-

Aggregating stats...

-
-
- ); - } - - // Format hour for XAxis (e.g., "HH:00") - const chartData = data.map(item => ({ - ...item, - displayTime: new Date(item.hour).getHours() + ':00' - })); - - return ( -
- - - - - - - - - - - - - - { - const date = new Date(str); - return date.getHours() % 4 === 0 ? `${date.getHours()}:00` : ''; - }} - axisLine={false} - tickLine={false} - stroke="var(--muted-foreground)" - minTickGap={30} - /> - - } /> - - - - -
- ); -}; diff --git a/web/src/components/AppSidebar.tsx b/web/src/components/AppSidebar.tsx deleted file mode 100644 index e3a0938..0000000 --- a/web/src/components/AppSidebar.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { LayoutDashboard, Settings, Activity } from "lucide-react"; -import { Link, useLocation } from "react-router-dom"; -import { - Sidebar, - SidebarContent, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarFooter, - SidebarRail, -} from "@/components/ui/sidebar"; -import { useDashboardStats } from "@/hooks/use-dashboard-stats"; - -// Menu items. -const items = [ - { - title: "Dashboard", - url: "/", - icon: LayoutDashboard, - }, - { - title: "Activity", - url: "/activity", - icon: Activity, - }, - { - title: "Settings", - url: "/settings", - icon: Settings, - }, -]; - -export function AppSidebar() { - const location = useLocation(); - const { stats } = useDashboardStats(); - - const botName = stats?.bot?.name || "Aurora"; - const botAvatar = stats?.bot?.avatarUrl; - - return ( - - - - - - -
- {botAvatar ? ( - {botName} - ) : ( -
A
- )} -
-
- {botName} - Admin Portal -
- -
-
-
-
- - - Main Navigation - - - {items.map((item) => ( - - - - - {item.title} - - - - ))} - - - - - - -
- ); -} diff --git a/web/src/components/ControlPanel.tsx b/web/src/components/ControlPanel.tsx deleted file mode 100644 index 356dd09..0000000 --- a/web/src/components/ControlPanel.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState } from "react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"; -import { Button } from "./ui/button"; -import { RefreshCw, Trash2, ShieldAlert, Loader2, Power } from "lucide-react"; - -/** - * Props for the ControlPanel component - */ -interface ControlPanelProps { - maintenanceMode: boolean; -} - -/** - * ControlPanel component provides quick administrative actions for the bot. - * Integrated with the premium glassmorphic theme. - */ -export function ControlPanel({ maintenanceMode }: ControlPanelProps) { - const [loading, setLoading] = useState(null); - - /** - * Handles triggering an administrative action via the API - */ - const handleAction = async (action: string, payload?: Record) => { - setLoading(action); - try { - const response = await fetch(`/api/actions/${action}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: payload ? JSON.stringify(payload) : undefined, - }); - if (!response.ok) throw new Error(`Action ${action} failed`); - } catch (error) { - console.error("Action Error:", error); - // Ideally we'd show a toast here - } finally { - setLoading(null); - } - }; - - return ( - - -
- -
- -
- System Controls - - Administrative bot operations - - -
- {/* Reload Commands Button */} - - - {/* Clear Cache Button */} - -
- - {/* Maintenance Mode Toggle Button */} - -
- - ); -} diff --git a/web/src/components/feature-card.tsx b/web/src/components/feature-card.tsx new file mode 100644 index 0000000..3f25cb1 --- /dev/null +++ b/web/src/components/feature-card.tsx @@ -0,0 +1,48 @@ +import React, { type ReactNode } from "react"; +import { cn } from "../lib/utils"; +import { Card, CardHeader, CardTitle, CardContent } from "./ui/card"; +import { Badge } from "./ui/badge"; + +interface FeatureCardProps { + title: string; + category: string; + description?: string; + icon?: ReactNode; + children?: ReactNode; + className?: string; + delay?: number; // Animation delay in ms or generic unit +} + +export function FeatureCard({ + title, + category, + description, + icon, + children, + className, +}: FeatureCardProps) { + return ( + + {icon && ( +
+ {icon} +
+ )} + + {category} + {title} + + + {description && ( +

+ {description} +

+ )} + {children} +
+
+ ); +} diff --git a/web/src/components/info-card.tsx b/web/src/components/info-card.tsx new file mode 100644 index 0000000..5ced589 --- /dev/null +++ b/web/src/components/info-card.tsx @@ -0,0 +1,30 @@ +import React, { type ReactNode } from "react"; +import { cn } from "../lib/utils"; + +interface InfoCardProps { + icon: ReactNode; + title: string; + description: string; + iconWrapperClassName?: string; + className?: string; +} + +export function InfoCard({ + icon, + title, + description, + iconWrapperClassName, + className, +}: InfoCardProps) { + return ( +
+
+ {icon} +
+

{title}

+

+ {description} +

+
+ ); +} diff --git a/web/src/components/section-header.tsx b/web/src/components/section-header.tsx new file mode 100644 index 0000000..4adc47a --- /dev/null +++ b/web/src/components/section-header.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { cn } from "../lib/utils"; +import { Badge } from "./ui/badge"; + +interface SectionHeaderProps { + badge: string; + title: string; + description?: string; + align?: "center" | "left" | "right"; + className?: string; +} + +export function SectionHeader({ + badge, + title, + description, + align = "center", + className, +}: SectionHeaderProps) { + const alignClasses = { + center: "text-center mx-auto", + left: "text-left mr-auto", // reset margin if needed + right: "text-right ml-auto", + }; + + return ( +
+ {badge} +

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ ); +} diff --git a/web/src/components/testimonial-card.tsx b/web/src/components/testimonial-card.tsx new file mode 100644 index 0000000..186482e --- /dev/null +++ b/web/src/components/testimonial-card.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { cn } from "../lib/utils"; +import { Card } from "./ui/card"; + +interface TestimonialCardProps { + quote: string; + author: string; + role: string; + avatarGradient: string; + className?: string; +} + +export function TestimonialCard({ + quote, + author, + role, + avatarGradient, + className, +}: TestimonialCardProps) { + return ( + +
+ {[1, 2, 3, 4, 5].map((_, i) => ( + + + + ))} +
+

+ "{quote}" +

+
+
+
+

{author}

+

{role}

+
+
+ + ); +} diff --git a/web/src/components/ui/badge.tsx b/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..6db0169 --- /dev/null +++ b/web/src/components/ui/badge.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:opacity-90", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:opacity-80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground border-border hover:bg-accent hover:text-accent-foreground", + aurora: "border-transparent bg-aurora text-primary-foreground shadow-sm", + glass: "glass-card border-border/50 text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps { } + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx index 37a7d4b..f6f2085 100644 --- a/web/src/components/ui/button.tsx +++ b/web/src/components/ui/button.tsx @@ -19,6 +19,8 @@ const buttonVariants = cva( ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", + aurora: "bg-aurora text-primary-foreground shadow-sm hover:opacity-90", + glass: "glass-card border-border/50 text-foreground hover:bg-accent/50", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx index 681ad98..e322855 100644 --- a/web/src/components/ui/card.tsx +++ b/web/src/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
Promise; -} - -/** - * Custom hook to fetch hourly activity data for charts. - * Data is cached on the server for 5 minutes. - */ -export function useActivityStats(): UseActivityStatsResult { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchActivity = async () => { - setLoading(true); - try { - const response = await fetch("/api/stats/activity"); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - const jsonData = await response.json(); - setData(jsonData); - setError(null); - } catch (err) { - console.error("Failed to fetch activity stats:", err); - setError(err instanceof Error ? err.message : "Failed to fetch activity"); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchActivity(); - - // Refresh every 5 minutes to match server cache - const interval = setInterval(fetchActivity, 5 * 60 * 1000); - return () => clearInterval(interval); - }, []); - - return { data, loading, error, refresh: fetchActivity }; -} diff --git a/web/src/hooks/use-dashboard-stats.ts b/web/src/hooks/use-dashboard-stats.ts deleted file mode 100644 index 73d8a4e..0000000 --- a/web/src/hooks/use-dashboard-stats.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { useState, useEffect } from "react"; - -interface DashboardStats { - bot: { - name: string; - avatarUrl: string | null; - }; - guilds: { - count: number; - }; - users: { - active: number; - total: number; - }; - commands: { - total: number; - }; - ping: { - avg: number; - }; - economy: { - totalWealth: string; - avgLevel: number; - topStreak: number; - totalItems: number; - }; - activeLootdrops: Array<{ - rewardAmount: number; - currency: string; - createdAt: string; - expiresAt: string | null; - }>; - leaderboards: { - topLevels: Array<{ username: string; level: number | null }>; - topWealth: Array<{ username: string; balance: string }>; - }; - recentEvents: Array<{ - type: 'success' | 'error' | 'info' | 'warn'; - message: string; - timestamp: string; - icon?: string; - }>; - uptime: number; - lastCommandTimestamp: number | null; - maintenanceMode: boolean; -} - -interface UseDashboardStatsResult { - stats: DashboardStats | null; - loading: boolean; - error: string | null; -} - -/** - * Custom hook to fetch and auto-refresh dashboard statistics using WebSockets with HTTP fallback - */ -export function useDashboardStats(): UseDashboardStatsResult { - const [stats, setStats] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchStats = async () => { - try { - const response = await fetch("/api/stats"); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - const data = await response.json(); - setStats(data); - setError(null); - } catch (err) { - console.error("Failed to fetch dashboard stats:", err); - setError(err instanceof Error ? err.message : "Failed to fetch stats"); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - // Initial fetch - fetchStats(); - - // WebSocket setup - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/ws`; - let socket: WebSocket | null = null; - let reconnectTimeout: Timer | null = null; - - const connect = () => { - socket = new WebSocket(wsUrl); - - socket.onopen = () => { - console.log("🟢 [WS] Connected to dashboard live stream"); - setError(null); - if (reconnectTimeout) { - clearTimeout(reconnectTimeout); - reconnectTimeout = null; - } - }; - - socket.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - - if (message.type === "STATS_UPDATE") { - setStats(message.data); - } else if (message.type === "NEW_EVENT") { - setStats(prev => { - if (!prev) return prev; - return { - ...prev, - recentEvents: [message.data, ...prev.recentEvents].slice(0, 10) - }; - }); - } - } catch (e) { - console.error("Error parsing WS message:", e); - } - }; - - socket.onclose = () => { - console.log("🟠 [WS] Connection lost. Attempting reconnect in 5s..."); - reconnectTimeout = setTimeout(connect, 5000); - }; - - socket.onerror = (err) => { - console.error("🔴 [WS] Socket error:", err); - socket?.close(); - }; - }; - - connect(); - - // Cleanup on unmount - return () => { - if (socket) { - socket.onclose = null; // Prevent reconnect on intentional close - socket.close(); - } - if (reconnectTimeout) clearTimeout(reconnectTimeout); - }; - }, []); - - return { stats, loading, error }; -} diff --git a/web/src/hooks/use-mobile.ts b/web/src/hooks/use-mobile.ts deleted file mode 100644 index 2b0fe1d..0000000 --- a/web/src/hooks/use-mobile.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react" - -const MOBILE_BREAKPOINT = 768 - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) - - return !!isMobile -} diff --git a/web/src/hooks/use-socket.ts b/web/src/hooks/use-socket.ts new file mode 100644 index 0000000..2d66303 --- /dev/null +++ b/web/src/hooks/use-socket.ts @@ -0,0 +1,61 @@ +import { useEffect, useState, useRef } from "react"; +import type { DashboardStats } from "@shared/modules/dashboard/dashboard.types"; + +export function useSocket() { + const [isConnected, setIsConnected] = useState(false); + const [stats, setStats] = useState(null); + const socketRef = useRef(null); + + useEffect(() => { + // Determine WS protocol based on current page schema + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const host = window.location.host; + const wsUrl = `${protocol}//${host}/ws`; + + function connect() { + const ws = new WebSocket(wsUrl); + socketRef.current = ws; + + ws.onopen = () => { + console.log("Connected to dashboard websocket"); + setIsConnected(true); + }; + + ws.onmessage = (event) => { + try { + const payload = JSON.parse(event.data); + + if (payload.type === "STATS_UPDATE") { + setStats(payload.data); + } + } catch (err) { + console.error("Failed to parse WS message", err); + } + }; + + ws.onclose = () => { + console.log("Disconnected from dashboard websocket"); + setIsConnected(false); + // Simple reconnect logic + setTimeout(connect, 3000); + }; + + ws.onerror = (err) => { + console.error("WebSocket error:", err); + ws.close(); + }; + } + + connect(); + + return () => { + if (socketRef.current) { + // Prevent reconnect on unmount + socketRef.current.onclose = null; + socketRef.current.close(); + } + }; + }, []); + + return { isConnected, stats }; +} diff --git a/web/src/layouts/DashboardLayout.tsx b/web/src/layouts/DashboardLayout.tsx deleted file mode 100644 index 4715281..0000000 --- a/web/src/layouts/DashboardLayout.tsx +++ /dev/null @@ -1,35 +0,0 @@ - -import { Outlet } from "react-router-dom"; -import { AppSidebar } from "../components/AppSidebar"; -import { SidebarProvider, SidebarInset, SidebarTrigger } from "../components/ui/sidebar"; -import { Separator } from "../components/ui/separator"; - -export function DashboardLayout() { - return ( - -
-
-
-
- - - -
- - -
-

Dashboard

-
-
-
-
-
-
-
- -
-
-
- - ); -} diff --git a/web/src/pages/Activity.tsx b/web/src/pages/Activity.tsx deleted file mode 100644 index aba0222..0000000 --- a/web/src/pages/Activity.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { ActivityChart } from "@/components/ActivityChart"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { useActivityStats } from "@/hooks/use-activity-stats"; -import { useDashboardStats } from "@/hooks/use-dashboard-stats"; -import { Activity as ActivityIcon, RefreshCw, Terminal } from "lucide-react"; - -export function Activity() { - const { data: activityData, loading: activityLoading, refresh: refreshActivity } = useActivityStats(); - const { stats, loading: statsLoading } = useDashboardStats(); - - return ( -
-
-

- Activity Monitoring -

-

Real-time system logs and performance metrics.

-
- - {/* Activity Chart Section */} - - -
- -
- Command & Transaction Volume - - Hourly traffic analysis (Last 24 Hours) -
- - - - - - - - {/* Detailed Activity Logs */} - - -
-
- -
-
- System Logs - Recent operational events and alerts -
-
-
- - {statsLoading && !stats ? ( -
- -

Connecting to event stream...

-
- ) : ( -
- {!stats?.recentEvents || stats.recentEvents.length === 0 ? ( -
- -

No recent activity recorded

-
- ) : ( - stats.recentEvents.map((event, i) => ( -
-
-
-
{event.icon}
-
-
-
-
-
-

{event.message}

- - {new Date(event.timestamp).toLocaleString()} - -
- {event.type === 'error' && ( -

- Error: Check system console for stack trace -

- )} -
-
- )) - )} -
- )} - - -
- ); -} diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 90efaae..7f6612f 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -1,278 +1,67 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Activity, Server, Users, Zap, Package, Trophy, Coins } from "lucide-react"; -import { useDashboardStats } from "@/hooks/use-dashboard-stats"; -import { useActivityStats } from "@/hooks/use-activity-stats"; -import { ControlPanel } from "@/components/ControlPanel"; -import { ActivityChart } from "@/components/ActivityChart"; +import React from "react"; +import { Link } from "react-router-dom"; +import { useSocket } from "../hooks/use-socket"; +import { Badge } from "../components/ui/badge"; export function Dashboard() { - const { stats, loading, error } = useDashboardStats(); - const { data: activityData, loading: activityLoading } = useActivityStats(); - - if (loading && !stats) { - return ( -
-
-

Dashboard

-

Loading dashboard data...

-
-
- {[1, 2, 3, 4].map((i) => ( - - - Loading... - - -
- - - ))} -
-
- ); - } - - if (error) { - return ( -
-
-

Dashboard

-

Error loading dashboard: {error}

-
-
- ); - } - - if (!stats) { - return null; - } + const { isConnected, stats } = useSocket(); return ( -
-
-

- {stats.bot.name} Overview -

-

Monitoring real-time activity and core bot metrics.

-
- -
- {/* Metric Cards */} - {[ - { title: "Active Users", value: stats.users.active.toLocaleString(), label: `${stats.users.total.toLocaleString()} total registered`, icon: Users, color: "from-purple-500 to-pink-500" }, - { title: "Items in Circulation", value: stats.economy.totalItems.toLocaleString(), label: "Total items owned", icon: Package, color: "from-blue-500 to-cyan-500" }, - { title: "Commands registered", value: stats.commands.total, label: "Total system capabilities", icon: Zap, color: "from-yellow-500 to-orange-500" }, - { title: "Avg Latency", value: `${stats.ping.avg}ms`, label: "WebSocket heartbeat", icon: Activity, color: "from-emerald-500 to-teal-500" }, - ].map((metric, i) => ( - - - {metric.title} -
- -
-
- -
{metric.value}
-

{metric.label}

-
-
- ))} -
- - {/* Activity Chart Section */} - - - -
- Live Activity Analytics - - Hourly command and transaction volume across the network (last 24h) - - - - - - -
- - -
- -
- Economy Overview - - Global wealth and progression statistics -
-
-
- - Uptime: {Math.floor(stats.uptime / 3600)}h {Math.floor((stats.uptime % 3600) / 60)}m - -
- - -
-
-
-
-

Total Distributed Wealth

-

- {BigInt(stats.economy.totalWealth).toLocaleString()} AU -

-
-
-
-
-

Avg Level

-

{stats.economy.avgLevel}

-
-
-

Peak Streak

-

{stats.economy.topStreak} days

-
-
-
-
- - -
- {/* Administrative Control Panel */} - - - {/* Active Lootdrop Alert */} - {stats.activeLootdrops && stats.activeLootdrops.length > 0 && ( - -
- -
- - - - - - - ACTIVE LOOTDROP - - - -
-

- {stats.activeLootdrops[0]?.rewardAmount} {stats.activeLootdrops[0]?.currency} -

-

- Expires {stats.activeLootdrops[0]?.expiresAt ? new Date(stats.activeLootdrops[0].expiresAt).toLocaleTimeString() : 'Never'} -

-
-
-
+
+ {/* Navigation */} + - - -
- -
-
- Richest Users - Highest net worth -
-
- -
- {stats.leaderboards?.topWealth.map((user, i) => ( -
-
-
- {i + 1} -
- {user.username} -
- {BigInt(user.balance).toLocaleString()} AU -
- )) ||

No data available

} -
-
-
-
+ {/* Content Placeholder */} +
+
+

Dashboard Overview

+

+ Real-time connection status: + {isConnected ? "Connected" : "Disconnected"} + +

+
+
); } diff --git a/web/src/pages/DesignSystem.tsx b/web/src/pages/DesignSystem.tsx new file mode 100644 index 0000000..6dda9ce --- /dev/null +++ b/web/src/pages/DesignSystem.tsx @@ -0,0 +1,305 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { Badge } from "../components/ui/badge"; +import { Card, CardHeader, CardTitle, CardContent } from "../components/ui/card"; +import { Button } from "../components/ui/button"; +import { Switch } from "../components/ui/switch"; +import { FeatureCard } from "../components/feature-card"; +import { InfoCard } from "../components/info-card"; +import { SectionHeader } from "../components/section-header"; +import { TestimonialCard } from "../components/testimonial-card"; + +export function DesignSystem() { + return ( +
+ {/* Navigation */} + + +
+ {/* Header Section */} +
+ v1.2.0-solar +

+ Aurora Design System +

+

+ Welcome to the Solaris Dark theme. A warm, celestial-inspired aesthetic designed for the Aurora astrology RPG. +

+
+ + {/* Color Palette */} +
+

+ + Color Palette +

+
+ + + + + + + +
+
+ + {/* Badges & Pills */} +
+

+ + Badges & Tags +

+
+ Primary + Secondary + Solaris + Celestial Glass + Outline + Destructive +
+
+ + {/* Animations & Interactions */} +
+

+ + Animations & Interactions +

+
+
+

Hover Lift

+

Smooth upward translation with enhanced depth.

+
+
+

Hover Glow

+

Subtle border and shadow illumination on hover.

+
+
+ +
+
+
+ + {/* Gradients & Special Effects */} +
+

+ + Gradients & Effects +

+
+
+

The Solaris Gradient (Background)

+
+ Celestial Void +
+
+
+

Glassmorphism

+
+
+ Frosted Celestial Glass +
+
+
+
+
+ + {/* Components Showcase */} +
+

+ + Component Showcase +

+
+ {/* Action Card with Tags */} + +
+ + Celestial Action + New + + +
+ Quest + Level 15 +
+

+ Experience the warmth of the sun in every interaction and claim your rewards. +

+
+ +
+
+ + + {/* Profile/Entity Card with Tags */} + + +
+
+ Online +
+ Stellar Navigator +

Level 42 Mage

+ + +
+ Astronomy + Pyromancy + Leadership +
+
+
+
+ + + + {/* Interactive Card with Tags */} + + +
+ Beta +
+ System Settings +
+ +
+
+
Starry Background
+
Enable animated SVG stars
+
+ +
+
+
+
+ Solar Flare Glow + Pro +
+
Add bloom to primary elements
+
+ +
+
+
+
+
+ + {/* Refactored Application Components */} +
+

+ + Application Components +

+ +
+ {/* Section Header Demo */} +
+ +
+ + {/* Feature Cards Demo */} +
+ } + /> + +
+ Custom Child Content +
+
+
+ + {/* Info Cards Demo */} +
+ } + title="Info Card" + description="Compact card for highlighting features or perks with an icon." + iconWrapperClassName="bg-primary/20 text-primary" + /> +
+ + {/* Testimonial Cards Demo */} +
+ +
+
+
+ + {/* Typography */} +
+

Fluid Typography

+
+ + + + + + + + +
+

+ Try resizing your browser window to see the text scale smoothly. +

+
+
+
+ ); +} + +function TypographyRow({ step, className, label }: { step: string, className: string, label: string }) { + return ( +
+ Step {step} +

{label}

+
+ ); +} + +function ColorSwatch({ label, color, text = "text-foreground", border = false }: { label: string, color: string, text?: string, border?: boolean }) { + return ( +
+
+ {label} +
+
+ ); +} + +export default DesignSystem; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx new file mode 100644 index 0000000..fef426b --- /dev/null +++ b/web/src/pages/Home.tsx @@ -0,0 +1,236 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { Badge } from "../components/ui/badge"; +import { Button } from "../components/ui/button"; +import { FeatureCard } from "../components/feature-card"; +import { InfoCard } from "../components/info-card"; +import { SectionHeader } from "../components/section-header"; +import { TestimonialCard } from "../components/testimonial-card"; +import { + GraduationCap, + Coins, + Package, + ShieldCheck, + Zap, + Trophy +} from "lucide-react"; + +export function Home() { + return ( +
+ {/* Navigation (Simple) */} + + {/* Hero Section */} +
+ + The Ultimate Academic Strategy RPG + + +

+ + Rise to the Top + + + of the Elite Academy + +

+ +

+ Aurora is a competitive academic RPG bot where students are assigned to Classes A through D, vying for supremacy in a high-stakes elite school setting. +

+ +
+ + +
+
+ + {/* Features Section (Bento Grid) */} +
+
+ + {/* Class System */} + } + > +
+ Constellation Units + Special Exams +
+
+ + {/* Economy */} + } + /> + + {/* Inventory */} + } + /> + + {/* Exams */} + +
+
+
+
+
+ Island Exam + Active +
+
+ + + {/* Trading & Social */} + } + > +
+ Constellation Units + Special Exams +
+
+ + {/* Tech Stack */} + +
+ BUN 1.0+ + DISCORD.JS + DRIZZLE + POSTGRES +
+
+
+
+ + {/* Unique Features Section */} +
+
+ + +
+ } + title="Merit-Based Society" + description="Your class standing determines your privileges. Earn points to rise, or lose them and face the consequences of falling behind." + iconWrapperClassName="bg-primary/20 text-primary" + /> + } + title="Psychological Warfare" + description="Form alliances, uncover spies, and execute strategies during Special Exams where trust is the most valuable currency." + iconWrapperClassName="bg-secondary/20 text-secondary" + /> + } + title="Dynamic World" + description="The school rules change based on the actions of the student body. Your decisions shape the future of the academy." + iconWrapperClassName="bg-primary/20 text-primary" + /> +
+
+
+ + {/* Testimonials Section */} +
+ + +
+ + + +
+
+ + {/* Footer */} + +
+ ); +} + +export default Home; diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx deleted file mode 100644 index 3aea38c..0000000 --- a/web/src/pages/Settings.tsx +++ /dev/null @@ -1,509 +0,0 @@ -import { useState, useEffect } from "react"; -import { toast } from "sonner"; -import { Loader2, Save, RefreshCw, Smartphone, Coins, Trophy, Shield, Users, Terminal, MessageSquare } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Card, CardContent } from "@/components/ui/card"; -import { Switch } from "@/components/ui/switch"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -// Types matching the backend response -interface RoleOption { id: string; name: string; color: string; } -interface ChannelOption { id: string; name: string; type: number; } - -interface SettingsMeta { - roles: RoleOption[]; - channels: ChannelOption[]; - commands: { name: string; category: string }[]; -} - -import { type GameConfigType } from "@shared/lib/config"; - -// Recursive partial type for nested updates -type DeepPartial = { - [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; -}; - -export function Settings() { - // We use a DeepPartial type for the local form state to allow for safe updates - const [config, setConfig] = useState | null>(null); - const [meta, setMeta] = useState({ roles: [], channels: [], commands: [] }); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [activeTab, setActiveTab] = useState("general"); - - useEffect(() => { - fetchData(); - }, []); - - const fetchData = async () => { - setLoading(true); - try { - const [configRes, metaRes] = await Promise.all([ - fetch("/api/settings"), - fetch("/api/settings/meta") - ]); - - if (configRes.ok && metaRes.ok) { - const configData = await configRes.json(); - const metaData = await metaRes.json(); - setConfig(configData); - setMeta(metaData); - } - } catch (error) { - console.error("Failed to fetch settings:", error); - } finally { - setLoading(false); - } - }; - - const handleSave = async () => { - setSaving(true); - const toastId = toast.loading("Saving configuration..."); - try { - const response = await fetch("/api/settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(config) - }); - - if (!response.ok) throw new Error("Failed to save"); - // Reload to satisfy any server-side transformations - await fetchData(); - toast.success("Settings saved successfully", { id: toastId }); - } catch (error) { - console.error("Failed to save settings:", error); - toast.error("Failed to save settings", { - id: toastId, - description: "Please check your input and try again." - }); - } finally { - setSaving(false); - } - }; - - // Helper to update nested state - const updateConfig = (path: string, value: any) => { - if (!path) return; - setConfig((prev: any) => { - const newConfig = { ...prev }; - const parts = path.split('.'); - let current = newConfig; - - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i]; - if (part === undefined) return prev; - // Clone nested objects to ensure immutability - if (!current[part]) current[part] = {}; - current[part] = { ...current[part] }; - current = current[part]; - } - - const lastPart = parts[parts.length - 1]; - if (lastPart !== undefined) { - current[lastPart] = value; - } - - return newConfig; - }); - }; - - if (loading || !config) { - return ( -
- -
- ); - } - - const tabs = [ - { id: "general", label: "General", icon: Smartphone }, - { id: "economy", label: "Economy", icon: Coins }, - { id: "leveling", label: "Leveling", icon: Trophy }, - { id: "moderation", label: "Moderation", icon: Shield }, - { id: "roles", label: "Roles", icon: Users }, - { id: "system", label: "System", icon: Terminal }, - ]; - - return ( -
-
-
-

Settings

-

Manage global bot configuration

-
-
- - -
-
- - {/* Tabs Navigation */} -
- {tabs.map((tab) => ( - - ))} -
- - - - {activeTab === "general" && ( -
- -
- updateConfig("welcomeChannelId", v)} - /> - updateConfig("feedbackChannelId", v)} - /> -
- - -
- updateConfig("terminal.channelId", v)} - /> - updateConfig("terminal.messageId", v)} - placeholder="ID of the pinned message" - /> -
-
- )} - - {activeTab === "economy" && ( -
- -
- updateConfig("economy.daily.amount", v)} - /> - updateConfig("economy.daily.streakBonus", v)} - /> - updateConfig("economy.daily.weeklyBonus", v)} - /> -
- - -
- updateConfig("economy.transfers.minAmount", v)} - /> -
- Allow Self Transfer - updateConfig("economy.transfers.allowSelfTransfer", checked)} - /> -
-
- - -
- updateConfig("lootdrop.spawnChance", Number(v))} - /> - updateConfig("lootdrop.cooldownMs", Number(v))} - /> - updateConfig("lootdrop.minMessages", Number(v))} - /> - updateConfig("lootdrop.reward.min", Number(v))} - /> - updateConfig("lootdrop.reward.max", Number(v))} - /> -
-
- )} - - {activeTab === "leveling" && ( -
- -
- updateConfig("leveling.base", Number(v))} - /> - updateConfig("leveling.exponent", Number(v))} - /> -
- - -
- updateConfig("leveling.chat.minXp", Number(v))} - /> - updateConfig("leveling.chat.maxXp", Number(v))} - /> - updateConfig("leveling.chat.cooldownMs", Number(v))} - /> -
-
- )} - - {activeTab === "roles" && ( -
- -
- updateConfig("studentRole", v)} - /> - updateConfig("visitorRole", v)} - /> -
- - - {/* Multi-select for color roles is complex, simpler impl for now */} -
-

Available Color Roles

-
- {(config?.colorRoles || []).map((roleId: string | undefined) => { - if (!roleId) return null; - const role = meta.roles.find(r => r.id === roleId); - return ( - - - {role?.name || roleId} - - - ); - })} -
-
- -
-
-
- )} - {activeTab === "moderation" && ( -
- -
- updateConfig("moderation.prune.maxAmount", Number(v))} - /> - updateConfig("moderation.prune.confirmThreshold", Number(v))} - /> -
-
- )} - - {activeTab === "system" && ( -
- - - {meta.commands.length === 0 ? ( -
- No commands found in metadata. -
- ) : ( - Object.entries( - meta.commands.reduce((acc, cmd) => { - const cat = cmd.category || 'Uncategorized'; - if (!acc[cat]) acc[cat] = []; - acc[cat].push(cmd); - return acc; - }, {} as Record) - ).sort(([a], [b]) => a.localeCompare(b)).map(([category, commands]) => ( -
-

{category}

-
- {commands.map(cmd => ( -
-
- /{cmd.name} - - {config?.commands?.[cmd.name] === false ? "Disabled" : "Enabled"} - -
- updateConfig(`commands.${cmd.name}`, checked)} - /> -
- ))} -
-
- )) - )} -
- )} -
-
-
- ); -} - -// Sub-components for cleaner code - -function SectionTitle({ icon: Icon, title, description }: { icon: any, title: string, description: string }) { - return ( -
-
- -
-
-

{title}

-

{description}

-
-
- ); -} - -function InputField({ label, value, onChange, type = "text", placeholder }: { label: string, value: any, onChange: (val: string) => void, type?: string, placeholder?: string }) { - return ( -
- - onChange(e.target.value)} - placeholder={placeholder} - className="bg-black/20 border-white/10 text-white focus:border-primary/50" - /> -
- ); -} - -function SelectField({ label, value, options, onChange }: { label: string, value: string | undefined, options: any[], onChange: (val: string) => void }) { - return ( -
- - -
- ); -} diff --git a/web/styles/globals.css b/web/styles/globals.css index ba9da65..572524b 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -39,68 +39,178 @@ --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); + + --text-step--2: var(--step--2); + --text-step--1: var(--step--1); + --text-step-0: var(--step-0); + --text-step-1: var(--step-1); + --text-step-2: var(--step-2); + --text-step-3: var(--step-3); + --text-step-4: var(--step-4); + --text-step-5: var(--step-5); } :root { - --radius: 1rem; - --background: oklch(0.12 0.02 260); - --foreground: oklch(0.98 0.01 260); - --card: oklch(0.16 0.03 260 / 0.5); - --card-foreground: oklch(0.98 0.01 260); - --popover: oklch(0.14 0.02 260 / 0.8); - --popover-foreground: oklch(0.98 0.01 260); - --primary: oklch(0.65 0.18 250); - --primary-foreground: oklch(1 0 0); - --secondary: oklch(0.25 0.04 260); - --secondary-foreground: oklch(0.98 0.01 260); - --muted: oklch(0.2 0.03 260 / 0.6); - --muted-foreground: oklch(0.7 0.02 260); - --accent: oklch(0.3 0.05 250 / 0.4); - --accent-foreground: oklch(0.98 0.01 260); - --destructive: oklch(0.6 0.18 25); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 5%); - --ring: oklch(0.65 0.18 250 / 50%); - --chart-1: oklch(0.6 0.18 250); - --chart-2: oklch(0.7 0.15 160); - --chart-3: oklch(0.8 0.12 80); - --chart-4: oklch(0.6 0.2 300); - --chart-5: oklch(0.6 0.25 20); - --sidebar: oklch(0.14 0.02 260 / 0.6); - --sidebar-foreground: oklch(0.98 0.01 260); - --sidebar-primary: oklch(0.65 0.18 250); - --sidebar-primary-foreground: oklch(1 0 0); - --sidebar-accent: oklch(1 0 0 / 5%); - --sidebar-accent-foreground: oklch(0.98 0.01 260); - --sidebar-border: oklch(1 0 0 / 8%); - --sidebar-ring: oklch(0.65 0.18 250 / 50%); -} + --step--2: clamp(0.5002rem, 0.449rem + 0.2273vw, 0.6252rem); + --step--1: clamp(0.7072rem, 0.6349rem + 0.3215vw, 0.884rem); + --step-0: clamp(1rem, 0.8977rem + 0.4545vw, 1.25rem); + --step-1: clamp(1.414rem, 1.2694rem + 0.6427vw, 1.7675rem); + --step-2: clamp(1.9994rem, 1.7949rem + 0.9088vw, 2.4992rem); + --step-3: clamp(2.8271rem, 2.538rem + 1.2851vw, 3.5339rem); + --step-4: clamp(3.9976rem, 3.5887rem + 1.8171vw, 4.997rem); + --step-5: clamp(5.6526rem, 5.0745rem + 2.5694vw, 7.0657rem); -@layer base { - * { - @apply border-border outline-ring/50; - } - - body { - @apply bg-background text-foreground selection:bg-primary/30; - font-family: 'Outfit', 'Inter', system-ui, sans-serif; - background-image: - radial-gradient(at 0% 0%, oklch(0.25 0.1 260 / 0.15) 0px, transparent 50%), - radial-gradient(at 100% 0%, oklch(0.35 0.12 300 / 0.1) 0px, transparent 50%); - background-attachment: fixed; - } + --radius: 0.5rem; + --background: oklch(0.12 0.015 40); + --foreground: oklch(0.98 0.01 60); + --card: oklch(0.16 0.03 40 / 0.6); + --card-foreground: oklch(0.98 0.01 60); + --popover: oklch(0.14 0.02 40 / 0.85); + --popover-foreground: oklch(0.98 0.01 60); + --primary: oklch(0.82 0.18 85); + --primary-foreground: oklch(0.12 0.015 40); + --secondary: oklch(0.65 0.2 55); + --secondary-foreground: oklch(0.98 0.01 60); + --muted: oklch(0.22 0.02 40 / 0.6); + --muted-foreground: oklch(0.7 0.08 40); + --accent: oklch(0.75 0.15 70 / 0.15); + --accent-foreground: oklch(0.98 0.01 60); + --destructive: oklch(0.55 0.18 25); + --border: oklch(1 0 0 / 12%); + --input: oklch(1 0 0 / 8%); + --ring: oklch(0.82 0.18 85 / 40%); + --chart-1: oklch(0.82 0.18 85); + --chart-2: oklch(0.65 0.2 55); + --chart-3: oklch(0.75 0.15 70); + --chart-4: oklch(0.55 0.18 25); + --chart-5: oklch(0.9 0.1 95); + --sidebar: oklch(0.14 0.02 40 / 0.7); + --sidebar-foreground: oklch(0.98 0.01 60); + --sidebar-primary: oklch(0.82 0.18 85); + --sidebar-primary-foreground: oklch(0.12 0.015 40); + --sidebar-accent: oklch(1 0 0 / 8%); + --sidebar-accent-foreground: oklch(0.98 0.01 60); + --sidebar-border: oklch(1 0 0 / 12%); + --sidebar-ring: oklch(0.82 0.18 85 / 40%); } @layer utilities { - .glass { - @apply bg-card backdrop-blur-xl border border-white/10 shadow-2xl; + .bg-aurora-page { + background: radial-gradient(circle at 50% -20%, oklch(0.25 0.1 50) 0%, var(--background) 70%); + background-attachment: fixed; } - .glass-sidebar { - @apply bg-sidebar backdrop-blur-2xl border-r border-white/5; + .bg-aurora { + background-image: linear-gradient(135deg, oklch(0.82 0.18 85) 0%, oklch(0.65 0.2 55) 100%); } - .text-glow { - text-shadow: 0 0 10px oklch(var(--primary) / 0.5); + .glass-card { + background: var(--card); + backdrop-filter: blur(12px); + border: 1px solid var(--border); + } + + .sun-flare { + box-shadow: 0 0 40px oklch(0.82 0.18 85 / 0.12); + } + + /* Entrance Animations */ + .animate-in { + animation-duration: 0.6s; + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); + animation-fill-mode: forwards; + } + + .fade-in { + opacity: 0; + animation-name: fade-in; + } + + .slide-up { + opacity: 0; + transform: translateY(20px); + animation-name: slide-up; + } + + .zoom-in { + opacity: 0; + transform: scale(0.95); + animation-name: zoom-in; + } + + @keyframes fade-in { + to { + opacity: 1; + } + } + + @keyframes slide-up { + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes zoom-in { + to { + opacity: 1; + transform: scale(1); + } + } + + /* Interaction Utilities */ + .hover-lift { + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease; + } + + .hover-lift:hover { + transform: translateY(-4px); + box-shadow: 0 10px 25px -5px oklch(0 0 0 / 0.3), 0 0 20px oklch(0.82 0.18 85 / 0.1); + } + + .hover-glow { + transition: box-shadow 0.3s ease, border-color 0.3s ease; + } + + .hover-glow:hover { + border-color: oklch(0.82 0.18 85 / 0.4); + box-shadow: 0 0 20px oklch(0.82 0.18 85 / 0.15); + } + + .hover-scale { + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + .hover-scale:hover { + transform: scale(1.02); + } + + .active-press { + transition: transform 0.1s ease; + } + + .active-press:active { + transform: scale(0.97); + } + + /* Staggered Delay Utilities */ + .delay-100 { + animation-delay: 100ms; + } + + .delay-200 { + animation-delay: 200ms; + } + + .delay-300 { + animation-delay: 300ms; + } + + .delay-400 { + animation-delay: 400ms; + } + + .delay-500 { + animation-delay: 500ms; } } \ No newline at end of file