init: init of the repository with basic connexion page and dashboard

This commit is contained in:
Pierre Ryssen
2026-02-27 11:47:51 +01:00
commit 7905adb55d
28 changed files with 2754 additions and 0 deletions

129
components/DragonEye.tsx Normal file
View File

@@ -0,0 +1,129 @@
"use client";
import { useEffect, useRef } from "react";
export default function DragonEye({ size = 60 }: { size?: number }) {
const irisRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!irisRef.current) return;
const rect = irisRef.current.parentElement!.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = e.clientX - cx;
const dy = e.clientY - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
const max = 5;
const mx = (dx / Math.max(dist, 1)) * Math.min(dist * 0.1, max);
const my = (dy / Math.max(dist, 1)) * Math.min(dist * 0.1, max);
irisRef.current.style.transform = `translate(calc(-50% + ${mx}px), calc(-50% + ${my}px))`;
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
const h = size;
const w = size * 1.6;
return (
<div
className="relative flex-shrink-0"
style={{ width: w, height: h }}
>
{/* Outer glow */}
<div
className="absolute inset-0 rounded-full blur-md opacity-30"
style={{
background: "radial-gradient(ellipse, #4aff8c 0%, transparent 70%)",
borderRadius: "50%",
}}
/>
{/* Eye shape */}
<div
className="absolute inset-0 overflow-hidden"
style={{
borderRadius: "50%",
background: "radial-gradient(ellipse at 40% 35%, #0a1a0a, #020402)",
border: "1px solid rgba(74,255,140,0.25)",
boxShadow: "0 0 20px rgba(74,255,140,0.1), inset 0 0 20px rgba(0,0,0,0.8)",
animation: "blink 9s ease-in-out infinite",
}}
>
{/* Iris */}
<div
ref={irisRef}
className="absolute top-1/2 left-1/2"
style={{
transform: "translate(-50%, -50%)",
width: h * 0.65,
height: h * 0.65,
borderRadius: "50%",
background: "radial-gradient(ellipse at 40% 35%, #1aff6a 0%, #0a8a3a 35%, #022a12 70%)",
boxShadow: "0 0 12px rgba(74,255,140,0.4)",
transition: "transform 0.08s ease-out",
}}
>
{/* Pupil */}
<div
className="absolute top-1/2 left-1/2"
style={{
transform: "translate(-50%, -50%)",
width: h * 0.1,
height: h * 0.55,
background: "#010801",
borderRadius: "50%",
boxShadow: "0 0 6px rgba(0,0,0,0.9)",
animation: "pupilDilate 7s ease-in-out infinite",
}}
/>
{/* Highlight */}
<div
className="absolute"
style={{
top: "18%", left: "22%",
width: h * 0.12, height: h * 0.08,
background: "rgba(200,255,220,0.4)",
borderRadius: "50%",
transform: "rotate(-20deg)",
filter: "blur(1px)",
}}
/>
</div>
{/* Eyelid top */}
<div
className="absolute top-0 left-0 right-0"
style={{
height: "30%",
background: "linear-gradient(to bottom, #0a0d0f, rgba(10,13,15,0.5))",
borderRadius: "50% 50% 0 0 / 80% 80% 0 0",
zIndex: 2,
}}
/>
{/* Eyelid bottom */}
<div
className="absolute bottom-0 left-0 right-0"
style={{
height: "22%",
background: "linear-gradient(to top, #0a0d0f, rgba(10,13,15,0.5))",
borderRadius: "0 0 50% 50% / 0 0 80% 80%",
zIndex: 2,
}}
/>
</div>
<style>{`
@keyframes blink {
0%, 88%, 100% { transform: scaleY(1); }
93% { transform: scaleY(0.06); }
}
@keyframes pupilDilate {
0%, 100% { width: ${h * 0.1}px; height: ${h * 0.55}px; }
50% { width: ${h * 0.07}px; height: ${h * 0.62}px; }
}
`}</style>
</div>
);
}

48
components/LiveBanner.tsx Normal file
View File

@@ -0,0 +1,48 @@
export type StreamInfo = {
live: boolean;
title?: string;
game?: string;
viewers?: number;
duration?: string;
};
export default function LiveBanner({ stream }: { stream: StreamInfo }) {
return (
<div className={`border rounded-sm p-5 flex items-center gap-5 transition-colors duration-300 ${
stream.live
? "bg-[#4aff8c]/3 border-[#4aff8c]/20"
: "bg-white/[0.02] border-[#1a2a1a]"
}`}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
{stream.live ? (
<span className="flex items-center gap-1.5 text-[9px] font-mono tracking-[0.2em] text-red-400 bg-red-500/10 border border-red-500/20 px-2 py-1 rounded-sm">
<span className="w-1.5 h-1.5 rounded-full bg-red-400 animate-pulse" />
EN DIRECT
</span>
) : (
<span className="text-[9px] font-mono tracking-[0.2em] text-[#3a5a3a] bg-white/[0.02] border border-[#1a2a1a] px-2 py-1 rounded-sm">
HORS LIGNE
</span>
)}
{stream.duration && (
<span className="text-[10px] font-mono text-[#3a5a3a]">{stream.duration}</span>
)}
</div>
<div className="text-white font-bold truncate">
{stream.title || "En attente du prochain stream..."}
</div>
{stream.game && (
<div className="text-[12px] text-[#3a5a3a] font-mono mt-1">🎮 {stream.game}</div>
)}
</div>
<div className="text-right flex-shrink-0">
<div className={`text-3xl font-black leading-none ${stream.live ? "text-[#4aff8c]" : "text-[#2a3a2a]"}`}>
{stream.viewers ?? 0}
</div>
<div className="text-[9px] font-mono tracking-widest text-[#3a5a3a] mt-0.5">VIEWERS</div>
</div>
</div>
);
}

51
components/LogFeed.tsx Normal file
View File

@@ -0,0 +1,51 @@
export type LogEntry = {
id: string;
type: "info" | "warn" | "ban" | "twitch" | "sub" | "unban";
text: string;
reason?: string;
time: string;
};
const typeStyles = {
info: { border: "border-l-[#4aff8c]/60", icon: "✅" },
warn: { border: "border-l-yellow-400/60", icon: "⚠️" },
ban: { border: "border-l-red-500/60", icon: "🔨" },
unban: { border: "border-l-blue-400/60", icon: "🔓" },
twitch: { border: "border-l-purple-400/60", icon: "📡" },
sub: { border: "border-l-[#4aff8c]/60", icon: "🎖️" },
};
const MOCK_LOGS: LogEntry[] = [
{ id: "1", type: "ban", text: "DragonSlayer99 banni par Marouette", reason: "Spam répété", time: "il y a 12 min" },
{ id: "2", type: "warn", text: "ChaosGremlin — avertissement", reason: "Langage inapproprié", time: "il y a 28 min" },
{ id: "3", type: "sub", text: "Rôle Sub attribué à HunterX42", reason: "Abonnement Twitch détecté", time: "il y a 1h" },
{ id: "4", type: "info", text: "Rôle Viewer donné à NoviceHunter", time: "il y a 1h" },
{ id: "5", type: "twitch", text: "EventSub enregistré : stream.online", time: "il y a 2h" },
{ id: "6", type: "info", text: "RATHIAN éveillé — connexion établie", time: "il y a 2h" },
];
export default function LogFeed({ logs = MOCK_LOGS }: { logs?: LogEntry[] }) {
return (
<div className="flex flex-col gap-2 max-h-[320px] overflow-y-auto pr-1">
{logs.map((log, i) => {
const style = typeStyles[log.type];
return (
<div
key={log.id}
className={`flex items-start gap-3 px-3 py-2.5 bg-white/[0.02] border border-[#1a2a1a] border-l-2 ${style.border} rounded-sm text-sm`}
style={{ animation: `fadeIn 0.3s ${i * 0.05}s both` }}
>
<span className="text-base flex-shrink-0 mt-0.5">{style.icon}</span>
<div className="flex-1 min-w-0">
<div className="text-[#b0c4b0] leading-snug">{log.text}</div>
{log.reason && (
<div className="text-[11px] text-[#3a5a3a] font-mono mt-0.5 italic">{log.reason}</div>
)}
</div>
<div className="text-[10px] font-mono text-[#2a4a2a] whitespace-nowrap mt-0.5">{log.time}</div>
</div>
);
})}
</div>
);
}

49
components/MemberList.tsx Normal file
View File

@@ -0,0 +1,49 @@
export type Member = {
id: string;
name: string;
role: "admin" | "mod" | "sub" | "viewer";
avatar?: string;
};
const roleStyles = {
admin: { label: "ADMIN", cls: "text-red-400 bg-red-500/8 border-red-500/20" },
mod: { label: "MOD", cls: "text-yellow-400 bg-yellow-400/8 border-yellow-400/20" },
sub: { label: "SUB", cls: "text-[#4aff8c] bg-[#4aff8c]/8 border-[#4aff8c]/20" },
viewer: { label: "VIEWER", cls: "text-blue-400 bg-blue-400/8 border-blue-400/20" },
};
const AVATARS = ["🗡️", "🛡️", "🏹", "⚔️", "🔥", "💀", "🐉", "⚡"];
const MOCK_MEMBERS: Member[] = [
{ id: "1", name: "HunterX42", role: "sub" },
{ id: "2", name: "IronWarden", role: "mod" },
{ id: "3", name: "NoviceHunter", role: "viewer" },
{ id: "4", name: "BladeRunner7", role: "sub" },
{ id: "5", name: "EmberWolf", role: "viewer" },
{ id: "6", name: "ShadowCrawler", role: "viewer" },
];
export default function MemberList({ members = MOCK_MEMBERS }: { members?: Member[] }) {
return (
<div className="flex flex-col gap-2 max-h-[320px] overflow-y-auto pr-1">
{members.map((m, i) => {
const role = roleStyles[m.role];
const avatar = AVATARS[i % AVATARS.length];
return (
<div
key={m.id}
className="flex items-center gap-3 px-3 py-2 bg-white/[0.02] border border-[#1a2a1a] rounded-sm hover:bg-[#4aff8c]/3 transition-colors duration-150"
>
<div className="w-7 h-7 rounded-full bg-[#0a1a0a] border border-[#1a2a1a] flex items-center justify-center text-xs flex-shrink-0">
{avatar}
</div>
<div className="flex-1 text-sm text-[#b0c4b0] font-mono">{m.name}</div>
<span className={`text-[9px] font-mono tracking-wider px-2 py-0.5 border rounded-sm ${role.cls}`}>
{role.label}
</span>
</div>
);
})}
</div>
);
}

80
components/Sidebar.tsx Normal file
View File

@@ -0,0 +1,80 @@
"use client";
import { useRouter, usePathname } from "next/navigation";
import DragonEye from "@/components/DragonEye";
import {
LayoutDashboard,
Twitch,
Youtube,
Instagram,
Music2,
DollarSign,
LogOut,
} from "lucide-react";
const nav = [
{ label: "Vue générale", href: "/dashboard", icon: LayoutDashboard },
{ label: "Twitch", href: "/twitch", icon: Twitch },
{ label: "YouTube", href: "/youtube", icon: Youtube },
{ label: "Instagram", href: "/instagram", icon: Instagram },
{ label: "TikTok", href: "/tiktok", icon: Music2 },
{ label: "Finances", href: "/finances", icon: DollarSign },
];
export default function Sidebar() {
const router = useRouter();
const pathname = usePathname();
const handleLogout = () => {
sessionStorage.clear();
router.push("/");
};
return (
<aside className="fixed left-0 top-0 h-screen w-56 bg-[#0a0d0f] border-r border-[#1a2a1a] flex flex-col z-50">
{/* Logo */}
<div className="flex items-center gap-3 px-5 py-6 border-b border-[#1a2a1a]">
<DragonEye size={32} />
<div>
<div className="text-[8px] font-mono tracking-[0.3em] text-[#4aff8c]/40 uppercase">Analytics</div>
<div className="text-base font-black tracking-widest text-white">WYVIEW</div>
</div>
</div>
{/* Nav */}
<nav className="flex-1 px-3 py-4 flex flex-col gap-1">
{nav.map(({ label, href, icon: Icon }) => {
const active = pathname === href;
return (
<button
key={href}
onClick={() => router.push(href)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-sm text-left transition-all duration-150 group ${
active
? "bg-[#4aff8c]/8 border border-[#4aff8c]/20 text-[#4aff8c]"
: "border border-transparent text-[#4a6a4a] hover:text-[#a0c4a0] hover:bg-white/[0.03]"
}`}
>
<Icon size={14} className="flex-shrink-0" />
<span className="text-[11px] font-mono tracking-wider">{label}</span>
{active && <span className="ml-auto w-1 h-1 rounded-full bg-[#4aff8c]" />}
</button>
);
})}
</nav>
{/* Footer */}
<div className="px-3 py-4 border-t border-[#1a2a1a]">
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-sm text-[#3a5a3a] hover:text-red-400 hover:bg-red-500/5 border border-transparent hover:border-red-500/20 transition-all duration-150"
>
<LogOut size={14} />
<span className="text-[11px] font-mono tracking-wider">DÉCONNEXION</span>
</button>
</div>
</aside>
);
}

42
components/StatCard.tsx Normal file
View File

@@ -0,0 +1,42 @@
interface StatCardProps {
label: string;
value: string | number;
sub?: string;
accent?: "green" | "red" | "blue" | "purple" | "gold";
delta?: string;
deltaUp?: boolean;
}
const accentColors = {
green: { val: "text-[#4aff8c]", border: "border-t-[#4aff8c]/40" },
red: { val: "text-red-400", border: "border-t-red-500/40" },
blue: { val: "text-blue-400", border: "border-t-blue-400/40" },
purple: { val: "text-purple-400", border: "border-t-purple-400/40" },
gold: { val: "text-yellow-400", border: "border-t-yellow-400/40" },
};
export default function StatCard({ label, value, sub, accent = "green", delta, deltaUp }: StatCardProps) {
const colors = accentColors[accent];
return (
<div className={`bg-[#0d1210] border border-[#1a2a1a] border-t-2 ${colors.border} rounded-sm p-5 relative group hover:-translate-y-0.5 transition-transform duration-200`}>
<div className="text-[9px] font-mono tracking-[0.25em] text-[#3a5a3a] uppercase mb-3">
{label}
</div>
<div className={`text-3xl font-black tracking-tight ${colors.val} leading-none mb-1`}>
{value === "—" || value === 0 ? <span className="text-[#2a3a2a]"></span> : value}
</div>
{sub && (
<div className="text-[11px] text-[#3a4a3a] font-mono mt-1">{sub}</div>
)}
{delta && (
<div className={`absolute top-4 right-4 text-[9px] font-mono tracking-wider px-2 py-1 rounded-sm border ${
deltaUp
? "text-[#4aff8c] bg-[#4aff8c]/5 border-[#4aff8c]/20"
: "text-red-400 bg-red-500/5 border-red-500/20"
}`}>
{deltaUp ? "+" : ""}{delta}
</div>
)}
</div>
);
}