diff --git a/.gitignore b/.gitignore index 63231da..e308e68 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ dist/ build/ /app/generated/prisma docker-compose.yml +deploy.sh diff --git a/app/api/tiktok/callback/route.ts b/app/api/tiktok/callback/route.ts index 741c194..e99028e 100644 --- a/app/api/tiktok/callback/route.ts +++ b/app/api/tiktok/callback/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server"; import { exchangeCodeForTokens } from "@/lib/tiktok"; import { prisma } from "@/lib/prisma"; +const baseUrl = process.env.NEXT_PUBLIC_APP_URL ?? ""; + export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get("code"); @@ -9,14 +11,13 @@ export async function GET(request: NextRequest) { const error = searchParams.get("error"); if (error || !code || !state) { - return NextResponse.redirect(new URL("/tiktok?error=access_denied", request.url)); + return NextResponse.redirect(`${baseUrl}/tiktok?error=access_denied`); } try { - // userId stocké dans le record PKCE lors du /connect (pas besoin de session) const pkceRecord = await prisma.tikTokPKCE.findUnique({ where: { state } }); if (!pkceRecord) { - return NextResponse.redirect(new URL("/tiktok?error=missing_verifier", request.url)); + return NextResponse.redirect(`${baseUrl}/tiktok?error=missing_verifier`); } await prisma.tikTokPKCE.delete({ where: { state } }); @@ -29,23 +30,48 @@ export async function GET(request: NextRequest) { const tokens = await exchangeCodeForTokens(code, codeVerifier); const expiresAt = new Date(Date.now() + tokens.expires_in * 1000); + // Upsert sur userId — plusieurs users peuvent partager le même compte TikTok await prisma.tikTokToken.upsert({ where: { userId }, - create: { userId, openId: tokens.open_id, accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt }, - update: { openId: tokens.open_id, accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt }, + update: { + openId: tokens.open_id, + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresAt, + }, + create: { + userId, + openId: tokens.open_id, + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresAt, + }, }); - const existing = await prisma.trackedAccount.findFirst({ where: { userId, platform: "tiktok" } }); - if (!existing) { + // TrackedAccount : update si existe, sinon create + const existing = await prisma.trackedAccount.findFirst({ + where: { userId, platform: "tiktok" }, + }); + + if (existing) { + await prisma.trackedAccount.update({ + where: { id: existing.id }, + data: { username: tokens.open_id, accountId: tokens.open_id }, + }); + } else { await prisma.trackedAccount.create({ - data: { userId, platform: "tiktok", username: tokens.open_id, accountId: tokens.open_id }, + data: { + userId, + platform: "tiktok", + username: tokens.open_id, + accountId: tokens.open_id, + }, }); } - return NextResponse.redirect(new URL("/tiktok?connected=1", request.url)); + return NextResponse.redirect(`${baseUrl}/tiktok?connected=1`); } catch (err) { console.error("[TikTok callback error]", err); - return NextResponse.redirect(new URL("/tiktok?error=token_exchange", request.url)); + return NextResponse.redirect(`${baseUrl}/tiktok?error=token_exchange`); } -} - +} \ No newline at end of file diff --git a/app/api/tiktok/snapshots/route.ts b/app/api/tiktok/snapshots/route.ts new file mode 100644 index 0000000..1ed2e54 --- /dev/null +++ b/app/api/tiktok/snapshots/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +const PLAN_RANK: Record = { free: 0, pro: 1, elite: 2, team: 3 }; + +const PERIOD_MIN_PLAN: Record = { + "7d": "free", + "30d": "pro", + "90d": "pro", + "all": "elite", +}; + +const PERIOD_DAYS: Record = { + "7d": 7, + "30d": 30, + "90d": 90, + "all": null, +}; + +// Plan max history cap (even for "all") +const PLAN_MAX_DAYS: Record = { + free: 7, + pro: 90, + elite: null, // illimité + team: null, +}; + +async function getAuthedUserId() { + const session = await auth(); + if (!session?.user) return null; + return (session.user as { id?: string }).id ?? null; +} + +export async function GET(req: NextRequest) { + const userId = await getAuthedUserId(); + if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const period = req.nextUrl.searchParams.get("period") ?? "7d"; + + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + + const plan = (user as any).plan ?? "free"; + + // Vérif accès à la période demandée + const requiredPlan = PERIOD_MIN_PLAN[period] ?? "free"; + if (PLAN_RANK[plan] < PLAN_RANK[requiredPlan]) { + return NextResponse.json({ error: "Plan insuffisant pour cette période" }, { status: 403 }); + } + + const account = await prisma.trackedAccount.findFirst({ + where: { userId, platform: "tiktok" }, + }); + if (!account) return NextResponse.json([], { status: 200 }); + + // Calcul de la date de début + const periodDays = PERIOD_DAYS[period]; + const planMaxDays = PLAN_MAX_DAYS[plan]; + + let since: Date | undefined; + if (periodDays !== null) { + since = new Date(Date.now() - periodDays * 86_400_000); + } else if (planMaxDays !== null) { + since = new Date(Date.now() - planMaxDays * 86_400_000); + } + // sinon : illimité (team/elite + all) + + const snapshots = await prisma.snapshot.findMany({ + where: { + accountId: account.id, + ...(since ? { createdAt: { gte: since } } : {}), + }, + orderBy: { createdAt: "asc" }, + select: { + createdAt: true, + followers: true, + likes: true, + videoCount: true, + }, + }); + + return NextResponse.json(snapshots); +} + +export async function POST(req: NextRequest) { + const userId = await getAuthedUserId(); + if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const body = await req.json(); + const { followers, likes, videoCount, displayName, openId } = body; + + if (followers === undefined) { + return NextResponse.json({ error: "Champ 'followers' requis" }, { status: 400 }); + } + + // Upsert TrackedAccount + let account = await prisma.trackedAccount.findFirst({ + where: { userId, platform: "tiktok" }, + }); + if (!account) { + account = await prisma.trackedAccount.create({ + data: { + userId, + platform: "tiktok", + username: displayName ?? openId ?? "unknown", + accountId: openId ?? userId, + }, + }); + } + + const snapshot = await prisma.snapshot.create({ + data: { + accountId: account.id, + followers: followers ?? 0, + likes: likes ?? 0, + videoCount: videoCount ?? 0, + views: 0, + }, + }); + + return NextResponse.json(snapshot, { status: 201 }); +} + diff --git a/app/api/tiktok/stats/route.ts b/app/api/tiktok/stats/route.ts index 09f9520..b70e58f 100644 --- a/app/api/tiktok/stats/route.ts +++ b/app/api/tiktok/stats/route.ts @@ -15,6 +15,18 @@ export async function GET() { return NextResponse.json({ error: "Session error" }, { status: 401 }); } + if (process.env.NODE_ENV === "development") { + const user = await prisma.user.findUnique({ where: { id: userId } }); + return NextResponse.json({ + followers: 124, + likes: 856, + videoCount: 1, + displayName: "CrowMate studio", + avatarUrl: "", + plan: (user as any)?.plan ?? "free", + }); + } + const tokenRecord = await prisma.tikTokToken.findUnique({ where: { userId } }); if (!tokenRecord) { @@ -23,24 +35,19 @@ export async function GET() { let { accessToken, refreshToken, openId, expiresAt } = tokenRecord; - // Refresh si expiré (avec 60s de marge) if (expiresAt.getTime() - Date.now() < 60_000) { try { const refreshed = await refreshAccessToken(refreshToken); - const newExpiresAt = new Date(Date.now() + refreshed.expires_in * 1000); - await prisma.tikTokToken.update({ where: { userId }, data: { accessToken: refreshed.access_token, refreshToken: refreshed.refresh_token, - expiresAt: newExpiresAt, + expiresAt: new Date(Date.now() + refreshed.expires_in * 1000), }, }); accessToken = refreshed.access_token; - refreshToken = refreshed.refresh_token; - expiresAt = newExpiresAt; } catch (err) { console.error("[TikTok stats refresh error]", err); return NextResponse.json({ error: "Token refresh failed" }, { status: 401 }); @@ -49,7 +56,38 @@ export async function GET() { try { const stats = await fetchUserStats(accessToken, openId); - return NextResponse.json(stats); + + // Upsert TrackedAccount + snapshot automatique + try { + let account = await prisma.trackedAccount.findFirst({ + where: { userId, platform: "tiktok" }, + }); + if (!account) { + account = await prisma.trackedAccount.create({ + data: { + userId, + platform: "tiktok", + username: stats.displayName ?? openId, + accountId: openId, + }, + }); + } + await prisma.snapshot.create({ + data: { + accountId: account.id, + followers: stats.followers ?? 0, + likes: stats.likes ?? 0, + videoCount: stats.videoCount ?? 0, + views: 0, + }, + }); + } catch (snapshotErr) { + console.error("[TikTok snapshot save error]", snapshotErr); + } + + // Inclure le plan dans la réponse + const user = await prisma.user.findUnique({ where: { id: userId } }); + return NextResponse.json({ ...stats, plan: (user as any)?.plan ?? "free" }); } catch (err) { console.error("[TikTok stats fetch error]", err); return NextResponse.json({ error: "Failed to fetch stats" }, { status: 500 }); diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index 20f67cc..fe8d587 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -1,38 +1,5 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import Sidebar from "@/components/Sidebar"; +import AppLayout from "@/components/AppLayout"; export default function AuthLayout({ children }: { children: React.ReactNode }) { - const { status } = useSession(); - const router = useRouter(); - - useEffect(() => { - if (status === "unauthenticated") { - router.push("/"); - } - }, [status, router]); - - if (status === "loading") { - return ( -
- - CHARGEMENT... - -
- ); - } - - if (status !== "authenticated") return null; - - return ( -
- -
- {children} -
-
- ); + return {children}; } \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index fc260af..768781c 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,9 +1,9 @@ export default function DashboardPage() { return (
-
Vue générale
-

Tableau de bord

-

Données à venir...

+
Vue générale
+

Tableau de bord

+

Données à venir...

); } \ No newline at end of file diff --git a/app/finances/layout.tsx b/app/finances/layout.tsx index 3383095..fe8d587 100644 --- a/app/finances/layout.tsx +++ b/app/finances/layout.tsx @@ -1,29 +1,5 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import Sidebar from "@/components/Sidebar"; +import AppLayout from "@/components/AppLayout"; export default function AuthLayout({ children }: { children: React.ReactNode }) { - const { status } = useSession(); - const router = useRouter(); - - useEffect(() => { - if (status === "unauthenticated") { - router.push("/"); - } - }, [status, router]); - - if (status === "loading") return null; - if (status !== "authenticated") return null; - - return ( -
- -
- {children} -
-
- ); + return {children}; } \ No newline at end of file diff --git a/app/finances/page.tsx b/app/finances/page.tsx index 881e66f..8319aaa 100644 --- a/app/finances/page.tsx +++ b/app/finances/page.tsx @@ -5,12 +5,12 @@ export default function FinancesPage() { return (
-
+
-
Revenus
-

Finances

+
Revenus
+

Finances

@@ -21,9 +21,10 @@ export default function FinancesPage() {
-
- -

+

+ +

AUCUNE DONNÉE FINANCIÈRE DISPONIBLE
POUR LE MOMENT

diff --git a/app/globals.css b/app/globals.css index bc58bb4..8563f02 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,6 +2,36 @@ @tailwind components; @tailwind utilities; +/* ── Dark theme (default) ── */ +:root { + --bg: #0d1117; + --surface: #161b22; + --border: #30363d; + --text-primary: #c9d1d9; + --text-secondary: #8b949e; + --accent: #4aff8c; + --accent-dim: rgba(74,255,140,0.1); + --accent-border: rgba(74,255,140,0.3); +} + +/* ── Light theme ── */ +html.light { + --bg: #f0f6ff; + --surface: #ffffff; + --border: #d0d7de; + --text-primary: #1c2128; + --text-secondary: #57606a; + --accent: #0ea5e9; + --accent-dim: rgba(14,165,233,0.1); + --accent-border: rgba(14,165,233,0.3); +} + +body { + background-color: var(--bg); + color: var(--text-primary); + transition: background-color 0.2s ease, color 0.2s ease; +} + ::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: #1a2a1a; border-radius: 2px; } \ No newline at end of file +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } diff --git a/app/instagram/layout.tsx b/app/instagram/layout.tsx index 3383095..fe8d587 100644 --- a/app/instagram/layout.tsx +++ b/app/instagram/layout.tsx @@ -1,29 +1,5 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import Sidebar from "@/components/Sidebar"; +import AppLayout from "@/components/AppLayout"; export default function AuthLayout({ children }: { children: React.ReactNode }) { - const { status } = useSession(); - const router = useRouter(); - - useEffect(() => { - if (status === "unauthenticated") { - router.push("/"); - } - }, [status, router]); - - if (status === "loading") return null; - if (status !== "authenticated") return null; - - return ( -
- -
- {children} -
-
- ); + return {children}; } \ No newline at end of file diff --git a/app/instagram/page.tsx b/app/instagram/page.tsx index f7d9a2f..0047adf 100644 --- a/app/instagram/page.tsx +++ b/app/instagram/page.tsx @@ -1,4 +1,3 @@ - import StatCard from "@/components/StatCard"; import { Camera } from "lucide-react"; @@ -6,12 +5,12 @@ export default function InstagramPage() { return (
-
+
-
Plateforme
-

Instagram

+
Plateforme
+

Instagram

@@ -22,9 +21,10 @@ export default function InstagramPage() {
-
- -

+

+ +

CONNECTEZ VOTRE COMPTE INSTAGRAM
POUR AFFICHER VOS STATISTIQUES

diff --git a/app/layout.tsx b/app/layout.tsx index e7933b6..cae6fab 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import "./globals.css"; import { SessionProvider } from "next-auth/react"; +import { ThemeProvider } from "@/lib/theme"; export const metadata: Metadata = { title: "WYVIEW", @@ -10,9 +11,11 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + - {children} + + {children} + diff --git a/app/page.tsx b/app/page.tsx index f0a7ef3..2ca55e6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,13 +8,11 @@ import DragonEye from "@/components/DragonEye"; export default function AuthPage() { const [tab, setTab] = useState<"login" | "register">("login"); - // Login state const [loginEmail, setLoginEmail] = useState(""); const [loginPassword, setLoginPassword] = useState(""); const [loginError, setLoginError] = useState(""); const [loginLoading, setLoginLoading] = useState(false); - // Register state const [regName, setRegName] = useState(""); const [regEmail, setRegEmail] = useState(""); const [regPassword, setRegPassword] = useState(""); @@ -85,61 +83,64 @@ export default function AuthPage() { } }; + const inputStyle: React.CSSProperties = { + background: "var(--surface)", + color: "var(--text-primary)", + borderColor: "var(--border)", + }; + const inputClass = (hasError: boolean) => - `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 ${ - hasError ? "border-red-500/50" : "border-[#1a2a1a] focus:border-[#4aff8c]/40" + `w-full px-4 py-3 text-sm font-mono outline-none rounded-md transition-all duration-200 border ${ + hasError ? "border-red-500/60" : "focus:border-[color:var(--accent)]" }`; return ( -
+
-
+
- {/* Header */}
-
+
Analytics Dashboard
-

+

WYVIEW

{/* Tabs */} -
+
- - {/* Login Form */} {tab === "login" && (
{ setLoginEmail(e.target.value); setLoginError(""); }} className={inputClass(!!loginError)} + style={inputStyle} /> { setLoginPassword(e.target.value); setLoginError(""); }} className={inputClass(!!loginError)} + style={inputStyle} /> {loginError && (

— {loginError}

@@ -164,14 +167,16 @@ export default function AuthPage() {
)} - {/* Register Form */} {tab === "register" && (
{ setRegName(e.target.value); setRegError(""); }} className={inputClass(false)} + style={inputStyle} /> { setRegEmail(e.target.value); setRegError(""); }} className={inputClass(!!regError)} + style={inputStyle} /> { setRegPassword(e.target.value); setRegError(""); }} className={inputClass(!!regError)} + style={inputStyle} /> { setRegConfirm(e.target.value); setRegError(""); }} className={inputClass(!!regError && regPassword !== regConfirm)} + style={inputStyle} /> {regError && (

— {regError}

)} {regSuccess && ( -

✓ {regSuccess}

+

✓ {regSuccess}

)}
)} -
+
WYVIEW v1.0 /// CROWMATE
diff --git a/app/tiktok/layout.tsx b/app/tiktok/layout.tsx index 3383095..fe8d587 100644 --- a/app/tiktok/layout.tsx +++ b/app/tiktok/layout.tsx @@ -1,29 +1,5 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import Sidebar from "@/components/Sidebar"; +import AppLayout from "@/components/AppLayout"; export default function AuthLayout({ children }: { children: React.ReactNode }) { - const { status } = useSession(); - const router = useRouter(); - - useEffect(() => { - if (status === "unauthenticated") { - router.push("/"); - } - }, [status, router]); - - if (status === "loading") return null; - if (status !== "authenticated") return null; - - return ( -
- -
- {children} -
-
- ); + return {children}; } \ No newline at end of file diff --git a/app/tiktok/page.tsx b/app/tiktok/page.tsx index 9f013f9..7eff67f 100644 --- a/app/tiktok/page.tsx +++ b/app/tiktok/page.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import StatCard from "@/components/StatCard"; +import StatsChart from "@/components/StatsCharts"; interface TikTokStats { followers: number; @@ -12,6 +13,7 @@ interface TikTokStats { videoCount: number; displayName: string; avatarUrl: string; + plan: "free" | "pro" | "elite" | "team"; } export default function TikTokPage() { @@ -69,8 +71,8 @@ export default function TikTokPage() {
-
Plateforme
-

TikTok

+
Plateforme
+

TikTok

@@ -99,8 +101,8 @@ export default function TikTokPage() { {/* Chargement */} {loading && ( -
- +
+ Chargement...
)} @@ -109,12 +111,12 @@ export default function TikTokPage() { {!loading && stats && ( <> {stats.displayName && ( -
+
{stats.avatarUrl && ( - avatar + avatar )} - {stats.displayName} - Connecté + {stats.displayName} + Connecté
)} @@ -144,19 +146,22 @@ export default function TikTokPage() { accent="gold" />
+ + {/* Graph */} + )} - {/* Non connecté */} {!loading && !stats && !error && ( - ); -} +} \ No newline at end of file diff --git a/app/twitch/layout.tsx b/app/twitch/layout.tsx index 3383095..fe8d587 100644 --- a/app/twitch/layout.tsx +++ b/app/twitch/layout.tsx @@ -1,29 +1,5 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import Sidebar from "@/components/Sidebar"; +import AppLayout from "@/components/AppLayout"; export default function AuthLayout({ children }: { children: React.ReactNode }) { - const { status } = useSession(); - const router = useRouter(); - - useEffect(() => { - if (status === "unauthenticated") { - router.push("/"); - } - }, [status, router]); - - if (status === "loading") return null; - if (status !== "authenticated") return null; - - return ( -
- -
- {children} -
-
- ); + return {children}; } \ No newline at end of file diff --git a/app/twitch/page.tsx b/app/twitch/page.tsx index f826bec..7642d45 100644 --- a/app/twitch/page.tsx +++ b/app/twitch/page.tsx @@ -5,12 +5,12 @@ export default function TwitchPage() { return (
-
+
-
Plateforme
-

Twitch

+
Plateforme
+

Twitch

@@ -21,9 +21,10 @@ export default function TwitchPage() {
-
- -

+

+ +

CONNECTEZ VOTRE COMPTE TWITCH
POUR AFFICHER VOS STATISTIQUES

diff --git a/app/youtube/layout.tsx b/app/youtube/layout.tsx index 3383095..fe8d587 100644 --- a/app/youtube/layout.tsx +++ b/app/youtube/layout.tsx @@ -1,29 +1,5 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import Sidebar from "@/components/Sidebar"; +import AppLayout from "@/components/AppLayout"; export default function AuthLayout({ children }: { children: React.ReactNode }) { - const { status } = useSession(); - const router = useRouter(); - - useEffect(() => { - if (status === "unauthenticated") { - router.push("/"); - } - }, [status, router]); - - if (status === "loading") return null; - if (status !== "authenticated") return null; - - return ( -
- -
- {children} -
-
- ); + return {children}; } \ No newline at end of file diff --git a/app/youtube/page.tsx b/app/youtube/page.tsx index e4d7f8d..9c4576e 100644 --- a/app/youtube/page.tsx +++ b/app/youtube/page.tsx @@ -5,12 +5,12 @@ export default function YoutubePage() { return (
-
+
-
Plateforme
-

YouTube

+
Plateforme
+

YouTube

@@ -21,9 +21,10 @@ export default function YoutubePage() {
-
- -

+

+ +

CONNECTEZ VOTRE COMPTE YOUTUBE
POUR AFFICHER VOS STATISTIQUES

diff --git a/components/AppLayout.tsx b/components/AppLayout.tsx new file mode 100644 index 0000000..025744f --- /dev/null +++ b/components/AppLayout.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; +import Sidebar from "@/components/Sidebar"; +import { useTheme } from "@/lib/theme"; + +export default function AppLayout({ children }: { children: React.ReactNode }) { + const { status } = useSession(); + const router = useRouter(); + const { theme } = useTheme(); + + useEffect(() => { + if (status === "unauthenticated") { + router.push("/"); + } + }, [status, router]); + + if (status === "loading") { + return ( +
+ + CHARGEMENT... + +
+ ); + } + + if (status !== "authenticated") return null; + + return ( +
+ +
+ {children} +
+
+ ); +} + diff --git a/components/DragonEye.tsx b/components/DragonEye.tsx index f67bbb0..1691173 100644 --- a/components/DragonEye.tsx +++ b/components/DragonEye.tsx @@ -1,9 +1,12 @@ "use client"; import { useEffect, useRef } from "react"; +import { useTheme } from "@/lib/theme"; export default function DragonEye({ size = 60 }: { size?: number }) { const irisRef = useRef(null); + const { theme } = useTheme(); + const isLight = theme === "light"; useEffect(() => { const handleMouseMove = (e: MouseEvent) => { @@ -26,6 +29,22 @@ export default function DragonEye({ size = 60 }: { size?: number }) { const h = size; const w = size * 1.6; + // Light theme: sky-blue palette + const glowColor = isLight ? "#38bdf8" : "#4aff8c"; + const eyeBg = isLight + ? "radial-gradient(ellipse at 40% 35%, #e0f7ff, #b8eeff)" + : "radial-gradient(ellipse at 40% 35%, #0a1a0a, #020402)"; + const eyeBorder = isLight ? "rgba(56,189,248,0.4)" : "rgba(74,255,140,0.25)"; + const eyeShadow = isLight + ? "0 0 20px rgba(56,189,248,0.25), inset 0 0 10px rgba(56,189,248,0.1)" + : "0 0 20px rgba(74,255,140,0.1), inset 0 0 20px rgba(0,0,0,0.8)"; + const irisBg = isLight + ? "radial-gradient(ellipse at 40% 35%, #7dd3fc 0%, #0ea5e9 35%, #0369a1 70%)" + : "radial-gradient(ellipse at 40% 35%, #1aff6a 0%, #0a8a3a 35%, #022a12 70%)"; + const irisShadow = isLight ? "0 0 12px rgba(56,189,248,0.6)" : "0 0 12px rgba(74,255,140,0.4)"; + const pupilBg = isLight ? "#001a2e" : "#010801"; + const eyelidBg = isLight ? "#dbeafe" : "#0a0d0f"; + return (
@@ -45,9 +64,9 @@ export default function DragonEye({ size = 60 }: { size?: number }) { 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)", + background: eyeBg, + border: `1px solid ${eyeBorder}`, + boxShadow: eyeShadow, animation: "blink 9s ease-in-out infinite", }} > @@ -60,8 +79,8 @@ export default function DragonEye({ size = 60 }: { size?: number }) { 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)", + background: irisBg, + boxShadow: irisShadow, transition: "transform 0.08s ease-out", }} > @@ -72,19 +91,20 @@ export default function DragonEye({ size = 60 }: { size?: number }) { transform: "translate(-50%, -50%)", width: h * 0.1, height: h * 0.55, - background: "#010801", + background: pupilBg, borderRadius: "50%", boxShadow: "0 0 6px rgba(0,0,0,0.9)", animation: "pupilDilate 7s ease-in-out infinite", }} /> + {/* Highlight */}
{stream.live ? ( - + EN DIRECT ) : ( - + HORS LIGNE )} {stream.duration && ( - {stream.duration} + {stream.duration} )}
{stream.title || "En attente du prochain stream..."}
{stream.game && ( -
🎮 {stream.game}
+
🎮 {stream.game}
)}
-
+
{stream.viewers ?? 0}
-
VIEWERS
+
VIEWERS
); diff --git a/components/LogFeed.tsx b/components/LogFeed.tsx index e837524..b2c1f68 100644 --- a/components/LogFeed.tsx +++ b/components/LogFeed.tsx @@ -7,12 +7,12 @@ export type LogEntry = { }; const typeStyles = { - info: { border: "border-l-[#4aff8c]/60", icon: "✅" }, + info: { border: "border-l-acid-green/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: "🎖️" }, + sub: { border: "border-l-acid-green/60", icon: "🎖️" }, }; const MOCK_LOGS: LogEntry[] = [ @@ -32,17 +32,17 @@ export default function LogFeed({ logs = MOCK_LOGS }: { logs?: LogEntry[] }) { return (
{style.icon}
-
{log.text}
+
{log.text}
{log.reason && ( -
{log.reason}
+
{log.reason}
)}
-
{log.time}
+
{log.time}
); })} diff --git a/components/MemberList.tsx b/components/MemberList.tsx index b1076ee..22e3217 100644 --- a/components/MemberList.tsx +++ b/components/MemberList.tsx @@ -6,10 +6,10 @@ export type Member = { }; 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" }, + admin: { label: "ADMIN", cls: "text-red-400 bg-red-500/10 border-red-500/30" }, + mod: { label: "MOD", cls: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30" }, + sub: { label: "SUB", cls: "text-acid-green bg-acid-green/10 border-acid-green/30" }, + viewer: { label: "VIEWER", cls: "text-blue-400 bg-blue-400/10 border-blue-400/30" }, }; const AVATARS = ["🗡️", "🛡️", "🏹", "⚔️", "🔥", "💀", "🐉", "⚡"]; @@ -32,13 +32,13 @@ export default function MemberList({ members = MOCK_MEMBERS }: { members?: Membe return (
-
+
{avatar}
-
{m.name}
- +
{m.name}
+ {role.label}
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 62190eb..f7df045 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -3,6 +3,8 @@ import { useRouter, usePathname } from "next/navigation"; import { signOut } from "next-auth/react"; import DragonEye from "@/components/DragonEye"; +import ThemeToggleButton from "@/components/ThemeToggleButton"; +import { useTheme } from "@/lib/theme"; import { LayoutDashboard, Twitch, @@ -25,24 +27,31 @@ const nav = [ export default function Sidebar() { const router = useRouter(); const pathname = usePathname(); + const { theme } = useTheme(); + const isLight = theme === "light"; const handleLogout = () => { signOut({ callbackUrl: "/" }); }; + const bg = isLight ? "bg-wy-light-surface" : "bg-wy-dark"; + const border = isLight ? "border-wy-light-border" : "border-wy-border"; + const textSec = isLight ? "text-wy-light-text-secondary" : "text-wy-text-secondary"; + const textPri = isLight ? "text-wy-light-text-primary" : "text-wy-text-primary"; + const title = isLight ? "text-wy-light-text-primary" : "text-white"; + return ( -