diff --git a/shared/modules/dashboard/dashboard.types.ts b/shared/modules/dashboard/dashboard.types.ts index 5549d4f..3dc7107 100644 --- a/shared/modules/dashboard/dashboard.types.ts +++ b/shared/modules/dashboard/dashboard.types.ts @@ -37,8 +37,38 @@ export const DashboardStatsSchema = z.object({ totalWealth: z.string(), avgLevel: z.number(), topStreak: z.number(), + totalItems: z.number().optional(), }), recentEvents: z.array(RecentEventSchema), + activeLootdrops: z.array(z.object({ + rewardAmount: z.number(), + currency: z.string(), + createdAt: z.string(), + expiresAt: z.string().nullable(), + })).optional(), + lootdropState: z.object({ + monitoredChannels: z.number(), + hottestChannel: z.object({ + id: z.string(), + messages: z.number(), + progress: z.number(), + cooldown: z.boolean(), + }).nullable(), + config: z.object({ + requiredMessages: z.number(), + dropChance: z.number(), + }), + }).optional(), + leaderboards: z.object({ + topLevels: z.array(z.object({ + username: z.string(), + level: z.number(), + })), + topWealth: z.array(z.object({ + username: z.string(), + balance: z.string(), + })), + }).optional(), uptime: z.number(), lastCommandTimestamp: z.number().nullable(), maintenanceMode: z.boolean(), diff --git a/shared/modules/economy/lootdrop.service.ts b/shared/modules/economy/lootdrop.service.ts index 6584dbc..960fd37 100644 --- a/shared/modules/economy/lootdrop.service.ts +++ b/shared/modules/economy/lootdrop.service.ts @@ -163,6 +163,43 @@ class LootdropService { return { success: false, error: "An error occurred while processing the reward." }; } } + public getLootdropState() { + let hottestChannel: { id: string; messages: number; progress: number; cooldown: boolean; } | null = null; + let maxMessages = -1; + + const window = config.lootdrop.activityWindowMs; + const now = Date.now(); + const required = config.lootdrop.minMessages; + + for (const [channelId, timestamps] of this.channelActivity.entries()) { + // Filter valid just to be sure we are reporting accurate numbers + const validCount = timestamps.filter(t => now - t < window).length; + + // Check cooldown + const cooldownUntil = this.channelCooldowns.get(channelId); + const isOnCooldown = !!(cooldownUntil && now < cooldownUntil); + + if (validCount > maxMessages) { + maxMessages = validCount; + hottestChannel = { + id: channelId, + messages: validCount, + progress: Math.min(100, (validCount / required) * 100), + cooldown: isOnCooldown + }; + } + } + + return { + monitoredChannels: this.channelActivity.size, + hottestChannel, + config: { + requiredMessages: required, + dropChance: config.lootdrop.spawnChance + } + }; + } + public async clearCaches() { this.channelActivity.clear(); this.channelCooldowns.clear(); diff --git a/web/src/components/lootdrop-card.tsx b/web/src/components/lootdrop-card.tsx new file mode 100644 index 0000000..12977f6 --- /dev/null +++ b/web/src/components/lootdrop-card.tsx @@ -0,0 +1,128 @@ +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Progress } from "./ui/progress"; +import { Gift, Clock, Sparkles, Zap, Timer } from "lucide-react"; +import { cn } from "../lib/utils"; +import { Skeleton } from "./ui/skeleton"; + +export interface LootdropData { + rewardAmount: number; + currency: string; + createdAt: string; + expiresAt: string | null; +} + +export interface LootdropState { + monitoredChannels: number; + hottestChannel: { + id: string; + messages: number; + progress: number; + cooldown: boolean; + } | null; + config: { + requiredMessages: number; + dropChance: number; + }; +} + +interface LootdropCardProps { + drop?: LootdropData | null; + state?: LootdropState; + isLoading?: boolean; + className?: string; +} + +export function LootdropCard({ drop, state, isLoading, className }: LootdropCardProps) { + if (isLoading) { + return ( + + + Lootdrop Status + + + +
+ + +
+
+
+ ); + } + + const isActive = !!drop; + const progress = state?.hottestChannel?.progress || 0; + const isCooldown = state?.hottestChannel?.cooldown || false; + + return ( + + {/* Ambient Background Effect */} + {isActive && ( +
+ )} + + + + {isActive ? "Active Lootdrop" : "Lootdrop Potential"} + {isActive && ( + + + + + )} + + + + + {isActive ? ( +
+
+ + + {drop.rewardAmount.toLocaleString()} {drop.currency} + +
+
+ + Dropped {new Date(drop.createdAt).toLocaleTimeString()} +
+
+ ) : ( +
+ {isCooldown ? ( +
+ +

Cooling Down...

+

Channels are recovering.

+
+ ) : ( +
+
+
+ 80 ? "text-yellow-500" : "text-muted-foreground")} /> + Next Drop Chance +
+ {Math.round(progress)}% +
+ 80 ? "bg-yellow-500" : "bg-primary")} /> + {state?.hottestChannel ? ( +

+ {state.hottestChannel.messages} / {state.config.requiredMessages} msgs +

+ ) : ( +

+ No recent activity +

+ )} +
+ )} +
+ )} +
+ + ); +} diff --git a/web/src/components/recent-activity.tsx b/web/src/components/recent-activity.tsx new file mode 100644 index 0000000..2767147 --- /dev/null +++ b/web/src/components/recent-activity.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import { Card, CardHeader, CardTitle, CardContent } from "./ui/card"; +import { Badge } from "./ui/badge"; +import { type RecentEvent } from "@shared/modules/dashboard/dashboard.types"; +import { cn } from "../lib/utils"; +import { Skeleton } from "./ui/skeleton"; + +function timeAgo(dateInput: Date | string) { + const date = new Date(dateInput); + const now = new Date(); + const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + return date.toLocaleDateString(); +} + +interface RecentActivityProps { + events: RecentEvent[]; + isLoading?: boolean; + className?: string; +} + +export function RecentActivity({ events, isLoading, className }: RecentActivityProps) { + return ( + + + + + + Live Activity + + {!isLoading && events.length > 0 && ( + + {events.length} EVENTS + + )} + + + + {isLoading ? ( +
+ {[1, 2, 3].map((i) => ( +
+ +
+ + +
+
+ ))} +
+ ) : events.length === 0 ? ( +
+
😴
+

No recent activity

+
+ ) : ( +
+ {events.map((event, i) => ( +
+
+ {event.icon || "📝"} +
+
+

+ {event.message} +

+
+ + {event.type} + + + {timeAgo(event.timestamp)} + +
+
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/web/src/components/stat-card.tsx b/web/src/components/stat-card.tsx new file mode 100644 index 0000000..a28878d --- /dev/null +++ b/web/src/components/stat-card.tsx @@ -0,0 +1,53 @@ +import React, { type ReactNode } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Skeleton } from "./ui/skeleton"; +import { type LucideIcon } from "lucide-react"; +import { cn } from "../lib/utils"; + +interface StatCardProps { + title: string; + value: ReactNode; + subtitle?: ReactNode; + icon: LucideIcon; + isLoading?: boolean; + className?: string; + valueClassName?: string; + iconClassName?: string; +} + +export function StatCard({ + title, + value, + subtitle, + icon: Icon, + isLoading = false, + className, + valueClassName, + iconClassName, +}: StatCardProps) { + return ( + + + {title} + + + + {isLoading ? ( +
+ + +
+ ) : ( + <> +
{value}
+ {subtitle && ( +

+ {subtitle} +

+ )} + + )} +
+
+ ); +} diff --git a/web/src/components/ui/progress.tsx b/web/src/components/ui/progress.tsx new file mode 100644 index 0000000..5939061 --- /dev/null +++ b/web/src/components/ui/progress.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import { cn } from "../../lib/utils" + +const Progress = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { value?: number | null, indicatorClassName?: string } +>(({ className, value, indicatorClassName, ...props }, ref) => ( +
+
+
+)) +Progress.displayName = "Progress" + +export { Progress } diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index e330cac..f7dc453 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -2,9 +2,10 @@ import React from "react"; import { Link } from "react-router-dom"; import { useSocket } from "../hooks/use-socket"; import { Badge } from "../components/ui/badge"; -import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; -import { Skeleton } from "../components/ui/skeleton"; -import { Server, Users, Terminal, Activity } from "lucide-react"; +import { StatCard } from "../components/stat-card"; +import { RecentActivity } from "../components/recent-activity"; +import { LootdropCard } from "../components/lootdrop-card"; +import { Server, Users, Terminal, Activity, Coins, TrendingUp, Flame, Package } from "lucide-react"; import { cn } from "../lib/utils"; export function Dashboard() { @@ -60,102 +61,115 @@ export function Dashboard() { {/* Stats Grid */}
- - - Total Servers - - - - {stats ? ( - <> -
{stats.guilds.count.toLocaleString()}
-

- {stats.guilds.changeFromLastMonth - ? `${stats.guilds.changeFromLastMonth > 0 ? '+' : ''}${stats.guilds.changeFromLastMonth} from last month` - : "Active Guilds" - } -

- - ) : ( -
- - -
- )} -
-
+ 0 ? '+' : ''}${stats.guilds.changeFromLastMonth} from last month` + : "Active Guilds" + } + /> - - - Total Users - - - - {stats ? ( - <> -
{stats.users.total.toLocaleString()}
-

- {stats.users.active.toLocaleString()} active now -

- - ) : ( -
- - -
- )} -
-
+ - - - Commands - - - - {stats ? ( - <> -
{stats.commands.total.toLocaleString()}
-

- {stats.commands.active} active ¡ {stats.commands.disabled} disabled -

- - ) : ( -
- - -
- )} -
-
+ - - - System Ping - - - - {stats ? ( - <> -
- {Math.round(stats.ping.avg)}ms -
-

- Average latency -

- - ) : ( -
- - -
- )} -
-
+ +
+ +
+ {/* Economy Stats */} +
+

Economy Overview

+
+ + + + + + + +
+
+ + {/* Recent Activity */} + {/* Recent Activity & Lootdrops */} +
+ +

Live Feed

+ +
diff --git a/web/src/pages/DesignSystem.tsx b/web/src/pages/DesignSystem.tsx index 6dda9ce..cc68c52 100644 --- a/web/src/pages/DesignSystem.tsx +++ b/web/src/pages/DesignSystem.tsx @@ -8,6 +8,25 @@ 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 { StatCard } from "../components/stat-card"; +import { LootdropCard } from "../components/lootdrop-card"; +import { Activity, Coins, Flame } from "lucide-react"; + +import { RecentActivity } from "../components/recent-activity"; +import { type RecentEvent } from "@shared/modules/dashboard/dashboard.types"; + +const mockEvents: RecentEvent[] = [ + { type: 'success', message: 'User leveled up to 5', timestamp: new Date(Date.now() - 1000 * 60 * 5), icon: 'âŦ†ī¸' }, + { type: 'info', message: 'New user joined', timestamp: new Date(Date.now() - 1000 * 60 * 15), icon: '👋' }, + { type: 'warn', message: 'Failed login attempt', timestamp: new Date(Date.now() - 1000 * 60 * 60), icon: 'âš ī¸' } +]; + +const mockManyEvents: RecentEvent[] = Array.from({ length: 15 }).map((_, i) => ({ + type: i % 3 === 0 ? 'success' : i % 3 === 1 ? 'info' : 'error', // Use string literals matching the type definition + message: `Event #${i + 1} generated for testing scroll behavior`, + timestamp: new Date(Date.now() - 1000 * 60 * i * 10), + icon: i % 3 === 0 ? '✨' : i % 3 === 1 ? 'â„šī¸' : '🚨', +})); export function DesignSystem() { return ( @@ -249,6 +268,79 @@ export function DesignSystem() { />
+ {/* Stat Cards Demo */} +
+ + + +
+ + {/* Game Event Cards Demo */} +
+

Game Event Cards

+
+ + + + +
+
+ {/* Testimonial Cards Demo */}
+ + {/* Recent Activity Demo */} +
+

Recent Activity Feed

+
+ + + + +
+
diff --git a/web/src/server.ts b/web/src/server.ts index 7d726ae..90809aa 100644 --- a/web/src/server.ts +++ b/web/src/server.ts @@ -363,6 +363,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise