init: init of the repository with basic connexion page and dashboard
This commit is contained in:
129
components/DragonEye.tsx
Normal file
129
components/DragonEye.tsx
Normal 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
48
components/LiveBanner.tsx
Normal 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
51
components/LogFeed.tsx
Normal 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
49
components/MemberList.tsx
Normal 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
80
components/Sidebar.tsx
Normal 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
42
components/StatCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user