init: init of the repository with basic connexion page and dashboard
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
npm-debug.log*
|
||||
.idea/
|
||||
.vscode/
|
||||
dist/
|
||||
build/
|
||||
24
app/dashboard/layout.tsx
Normal file
24
app/dashboard/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem("wyview_auth") !== "true") {
|
||||
router.push("/");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<main className="ml-56 flex-1 p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
app/dashboard/page.tsx
Normal file
9
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[9px] font-mono tracking-[0.3em] text-[#4aff8c]/50 uppercase mb-1">Vue générale</div>
|
||||
<h1 className="text-2xl font-black tracking-widest text-white uppercase mb-8">Tableau de bord</h1>
|
||||
<p className="text-[#3a5a3a] font-mono text-sm">Données à venir...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
app/finances/layout.tsx
Normal file
24
app/finances/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem("wyview_auth") !== "true") {
|
||||
router.push("/");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<main className="ml-56 flex-1 p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
app/finances/page.tsx
Normal file
0
app/finances/page.tsx
Normal file
7
app/globals.css
Normal file
7
app/globals.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #1a2a1a; border-radius: 2px; }
|
||||
24
app/instagram/layout.tsx
Normal file
24
app/instagram/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem("wyview_auth") !== "true") {
|
||||
router.push("/");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<main className="ml-56 flex-1 p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
app/instagram/page.tsx
Normal file
0
app/instagram/page.tsx
Normal file
17
app/layout.tsx
Normal file
17
app/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "WYVIEW",
|
||||
description: "Dashboard analytics",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="fr">
|
||||
<body className="bg-[#0a0d0f] text-white antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
78
app/page.tsx
Normal file
78
app/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import DragonEye from "@/components/DragonEye";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
if (password === process.env.NEXT_PUBLIC_PASSWORD) {
|
||||
sessionStorage.setItem("wyview_auth", "true");
|
||||
router.push("/dashboard");
|
||||
} else {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a0d0f] flex flex-col items-center justify-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-20 pointer-events-none" style={{
|
||||
backgroundImage: `linear-gradient(rgba(74,255,140,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(74,255,140,0.05) 1px, transparent 1px)`,
|
||||
backgroundSize: "40px 40px",
|
||||
}} />
|
||||
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] rounded-full bg-[#4aff8c]/[0.02] blur-3xl pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-8 w-full max-w-sm px-6">
|
||||
<div className="flex flex-col items-center gap-5">
|
||||
<DragonEye size={80} />
|
||||
<div className="text-center">
|
||||
<div className="text-[9px] tracking-[0.45em] text-[#4aff8c]/50 uppercase mb-2 font-mono">
|
||||
Analytics Dashboard
|
||||
</div>
|
||||
<h1 className="text-4xl font-black tracking-[0.15em] uppercase text-white">
|
||||
WYVIEW
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="w-full flex flex-col gap-3">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Mot de passe"
|
||||
value={password}
|
||||
onChange={(e) => { setPassword(e.target.value); setError(false); }}
|
||||
className={`w-full bg-[#0d1210] border px-4 py-3 text-sm font-mono text-white placeholder-[#2a3a2a] outline-none rounded-sm transition-all duration-200 ${
|
||||
error ? "border-red-500/50" : "border-[#1a2a1a] focus:border-[#4aff8c]/40"
|
||||
}`}
|
||||
/>
|
||||
{error && (
|
||||
<p className="text-red-400/80 text-[11px] font-mono tracking-widest">— ACCÈS REFUSÉ</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !password}
|
||||
className="w-full py-3 text-[11px] font-mono tracking-[0.3em] uppercase font-bold border border-[#4aff8c]/30 text-[#4aff8c] hover:bg-[#4aff8c]/8 hover:border-[#4aff8c]/60 transition-all duration-200 rounded-sm disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "VÉRIFICATION..." : "ACCÉDER"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-[9px] font-mono text-[#1a3a1a] tracking-[0.3em]">
|
||||
WYVIEW v1.0 /// CROWMATE
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
24
app/tiktok/layout.tsx
Normal file
24
app/tiktok/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem("wyview_auth") !== "true") {
|
||||
router.push("/");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<main className="ml-56 flex-1 p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
app/tiktok/page.tsx
Normal file
0
app/tiktok/page.tsx
Normal file
24
app/twitch/layout.tsx
Normal file
24
app/twitch/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem("wyview_auth") !== "true") {
|
||||
router.push("/");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<main className="ml-56 flex-1 p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
app/twitch/page.tsx
Normal file
0
app/twitch/page.tsx
Normal file
24
app/youtube/layout.tsx
Normal file
24
app/youtube/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem("wyview_auth") !== "true") {
|
||||
router.push("/");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<main className="ml-56 flex-1 p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
app/youtube/page.tsx
Normal file
0
app/youtube/page.tsx
Normal file
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>
|
||||
);
|
||||
}
|
||||
6
next-env.d.ts
vendored
Normal file
6
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
2010
package-lock.json
generated
Normal file
2010
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "wyview",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3001"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/CrowMate/Wyview.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/CrowMate/Wyview/issues"
|
||||
},
|
||||
"homepage": "https://github.com/CrowMate/Wyview#readme",
|
||||
"dependencies": {
|
||||
"@types/node": "^25.3.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"lucide-react": "^0.575.0",
|
||||
"next": "^15.5.12",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./*"] }
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user