PrestaGhis commited on
Commit
0e9ad51
·
verified ·
1 Parent(s): 6f8ff3c

Update app/admin/client/[id]/page.tsx

Browse files
Files changed (1) hide show
  1. app/admin/client/[id]/page.tsx +355 -121
app/admin/client/[id]/page.tsx CHANGED
@@ -1,130 +1,364 @@
1
- 'use client'
2
-
3
- import { useState, useEffect } from 'react'
4
- import { ArrowLeft, Clock, Activity, Zap, Sparkles, SearchCode, Globe, Layers, FileText, ExternalLink } from 'lucide-react'
5
- import Link from 'next/link'
6
- import { useParams, useRouter } from 'next/navigation'
7
- import { useAuth } from '@/lib/auth'
8
-
9
- export default function ClientDetailPage() {
10
- const params = useParams()
11
- const id = params?.id
12
- const router = useRouter()
13
- const { profile } = useAuth()
14
- const [data, setData] = useState<any>(null)
15
- const [loading, setLoading] = useState(true)
16
- const [activeTab, setActiveTab] = useState<'content' | 'veille' | 'sources'>('content')
17
-
18
- useEffect(() => {
19
- if (!profile) return
20
- if (profile.role !== 'admin') { router.push('/'); return; }
21
- const fetchData = async () => {
22
- try {
23
- const res = await fetch(`/api/admin/client/${id}`)
24
- const json = await res.json()
25
- setData(json)
26
- } catch (err) { console.error(err) } finally { setLoading(false) }
27
- }
28
- if (id) fetchData()
29
- }, [id, profile, router])
30
-
31
- if (loading) return <div className="min-h-screen bg-black flex items-center justify-center font-black italic">CHARGEMENT...</div>
32
- const avgSeo = data?.jobs?.length > 0 ? Math.round(data.jobs.reduce((a:number,j:any)=>a+(j.seo_score||0),0)/data.jobs.length) : 0
33
-
34
- return (
35
- <div className="min-h-screen bg-[#050505] text-white p-6 font-sans">
36
- <div className="max-w-[1400px] mx-auto">
37
- <Link href="/admin/dashboard" className="flex items-center gap-2 text-white/20 hover:text-white mb-6 transition-colors uppercase font-black text-[9px] tracking-[0.2em]">
38
- <ArrowLeft size={14} /> Retour Dashboard
39
- </Link>
40
-
41
- <div className="flex items-center justify-between mb-8">
42
- <h1 className="text-3xl font-black uppercase italic tracking-tighter text-white/90">{data.client.name}</h1>
43
- <div className="px-3 py-1 bg-white/5 border border-white/10 rounded-lg text-[9px] font-black uppercase tracking-widest text-white/30 truncate max-w-[200px]">{id}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </div>
45
-
46
- <div className="grid grid-cols-4 gap-4 mb-8">
47
- <StatC title="Optimisations" value={data.jobs.filter((j:any)=>j.mode==='OPTIMIZATION').length} color="cyan" />
48
- <StatC title="Briefs Stratégiques" value={data.jobs.filter((j:any)=>j.is_brief).length} color="amber" />
49
- <StatC title="Sources Veille" value={data.sites.length} color="indigo" />
50
- <StatC title="Moyenne SEO" value={`${avgSeo}%`} color="emerald" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  </div>
 
 
52
 
53
- <div className="flex gap-6 border-b border-white/5 mb-6 font-black uppercase text-[9px] tracking-[0.2em]">
54
- <button onClick={()=>setActiveTab('content')} className={`pb-3 px-1 transition-all ${activeTab==='content'?'text-indigo-400 border-b-2 border-indigo-400':'opacity-30 hover:opacity-100'}`}>Travaux de Contenu</button>
55
- <button onClick={()=>setActiveTab('veille')} className={`pb-3 px-1 transition-all ${activeTab==='veille'?'text-rose-400 border-b-2 border-rose-400':'opacity-30 hover:opacity-100'}`}>Événements de Veille</button>
56
- <button onClick={()=>setActiveTab('sources')} className={`pb-3 px-1 transition-all ${activeTab==='sources'?'text-amber-400 border-b-2 border-amber-400':'opacity-30 hover:opacity-100'}`}>Sources & Flux RSS</button>
 
 
 
 
57
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- {activeTab === 'sources' ? (
60
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
61
- {data.sites.length > 0 ? data.sites.map((site:any) => (
62
- <div key={site.id} className="p-4 bg-white/[0.02] border border-white/5 rounded-2xl flex items-center justify-between group">
63
- <div className="flex items-center gap-3">
64
- <div className="w-8 h-8 rounded-lg bg-amber-500/10 text-amber-500 flex items-center justify-center"><Globe size={16}/></div>
65
- <div>
66
- <div className="text-[10px] font-black uppercase tracking-tight text-white/90">{site.name}</div>
67
- <div className="text-[8px] text-white/20 font-bold truncate max-w-[200px]">{site.url}</div>
68
- </div>
 
 
 
 
 
 
 
69
  </div>
70
- <Link href={site.url} target="_blank" className="p-2 opacity-0 group-hover:opacity-100 transition-opacity text-white/20 hover:text-white"><ExternalLink size={14}/></Link>
71
- </div>
72
- )) : <div className="col-span-full py-20 text-center text-white/10 font-black uppercase text-xs">Aucune source configurée</div>}
73
- </div>
74
- ) : (
75
- <div className="bg-[#0a0a0a] border border-white/5 rounded-[1.5rem] overflow-hidden">
76
- <table className="w-full text-left">
77
- <thead>
78
- <tr className="bg-white/5 text-[9px] uppercase font-black text-white/30 tracking-[0.15em]">
79
- <th className="px-6 py-3">Date / Heure</th>
80
- <th className="px-6 py-3">Type / Libellé</th>
81
- <th className="px-6 py-3 text-center">État</th>
82
- </tr>
83
- </thead>
84
- <tbody className="divide-y divide-white/5">
85
- {activeTab === 'content' ? data.jobs.map((j:any)=>(
86
- <tr key={j.id} className="hover:bg-white/[0.01] transition-colors">
87
- <td className="px-6 py-3 text-[10px] font-black text-white/40">{new Date(j.created_at).toLocaleString('fr-FR', { hour12: false })}</td>
88
- <td className="px-6 py-3 text-xs font-black uppercase tracking-tight flex items-center gap-3">
89
- {j.target_keyword}
90
- {j.is_brief && <span className="px-2 py-0.5 rounded bg-amber-500/10 text-amber-500 text-[8px] border border-amber-500/20">BRIEF</span>}
91
- </td>
92
- <td className="px-6 py-3 text-center">
93
- <span className={`text-[9px] font-black uppercase px-2 py-0.5 rounded ${j.mode === 'CREATION' ? 'bg-purple-500/10 text-purple-400' : 'bg-cyan-500/10 text-cyan-400'}`}>{j.mode}</span>
94
- </td>
95
- </tr>
96
- )) : data.scans.map((s:any)=>(
97
- <tr key={s.id} className="hover:bg-white/[0.01] transition-colors">
98
- <td className="px-6 py-3 text-[10px] font-black text-white/40">{new Date(s.created_at).toLocaleString('fr-FR', { hour12: false })}</td>
99
- <td className="px-6 py-3 text-xs font-black uppercase text-white/70 italic flex items-center gap-2">
100
- <Activity size={10} className="text-white/20" />
101
- {s.type_label}
102
- </td>
103
- <td className="px-6 py-3 text-center">
104
- <span className={`text-[9px] font-black uppercase ${s.status === 'done' || s.status === 'completed' ? 'text-emerald-500' : 'text-amber-500'}`}>{s.status}</span>
105
- </td>
106
- </tr>
107
- ))}
108
- </tbody>
109
- </table>
110
- </div>
111
- )}
112
- </div>
113
- </div>
114
- )
115
  }
116
 
117
- function StatC({title, value, color}:any) {
118
- const colors:any = {
119
- cyan: 'text-cyan-400 bg-cyan-400/5',
120
- amber: 'text-amber-500 bg-amber-500/5',
121
- indigo: 'text-indigo-400 bg-indigo-400/5',
122
- emerald: 'text-emerald-400 bg-emerald-400/5'
123
- }
124
- return (
125
- <div className={`border border-white/5 p-4 rounded-2xl hover:bg-white/[0.01] transition-all ${colors[color]}`}>
126
- <div className="text-[8px] uppercase font-black opacity-40 mb-1 tracking-widest">{title}</div>
127
- <div className="text-xl font-black italic">{value}</div>
128
- </div>
129
- )
 
 
130
  }
 
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useParams, useRouter } from 'next/navigation';
5
+ import {
6
+ ArrowLeft,
7
+ Globe,
8
+ Calendar,
9
+ BarChart3,
10
+ FileText,
11
+ Search,
12
+ Zap,
13
+ Activity,
14
+ ExternalLink,
15
+ Clock,
16
+ CheckCircle2,
17
+ XCircle,
18
+ Loader2,
19
+ LayoutDashboard,
20
+ Database,
21
+ Cpu,
22
+ ShieldCheck,
23
+ Key,
24
+ TrendingUp,
25
+ Briefcase
26
+ } from 'lucide-react';
27
+
28
+ interface ClientDetail {
29
+ id: string;
30
+ name: string;
31
+ description?: string;
32
+ plan_type: string;
33
+ siteCount: number;
34
+ sources: any[];
35
+ jobs: any[];
36
+ scans: any[];
37
+ quotas: {
38
+ today: QuotaStats;
39
+ week: QuotaStats;
40
+ month: QuotaStats;
41
+ year: QuotaStats;
42
+ currentKey: string;
43
+ };
44
+ }
45
+
46
+ interface QuotaStats {
47
+ openai: number;
48
+ serper: number;
49
+ firecrawl: number;
50
+ cost: number;
51
+ }
52
+
53
+ export default function ClientPage() {
54
+ const params = useParams();
55
+ const router = useRouter();
56
+ const [client, setClient] = useState<ClientDetail | null>(null);
57
+ const [loading, setLoading] = useState(true);
58
+ const [activeTab, setActiveTab] = useState('content');
59
+
60
+ useEffect(() => {
61
+ fetchClientDetail();
62
+ }, [params.id]);
63
+
64
+ const fetchClientDetail = async () => {
65
+ try {
66
+ const res = await fetch(`/api/admin/client/${params.id}`);
67
+ const data = await res.json();
68
+ if (data.client) {
69
+ setClient(data.client);
70
+ }
71
+ } catch (err) {
72
+ console.error("Erreur fetch client:", err);
73
+ } finally {
74
+ setLoading(false);
75
+ }
76
+ };
77
+
78
+ if (loading) return (
79
+ <div className="flex-1 flex items-center justify-center min-h-screen bg-[var(--bg-main)]">
80
+ <Loader2 className="animate-spin theme-accent" size={32} />
81
  </div>
82
+ );
83
+
84
+ if (!client) return <div>Client non trouvé</div>;
85
+
86
+ return (
87
+ <div className="flex-1 flex flex-col min-h-screen bg-[var(--bg-main)] font-sans">
88
+ {/* Header / Navigation */}
89
+ <div className="w-full border-b border-[var(--border-subtle)] bg-[var(--bg-card)]/30 backdrop-blur-md sticky top-0 z-10">
90
+ <div className="max-w-[1400px] mx-auto px-6 py-4 flex items-center justify-between">
91
+ <div className="flex items-center gap-6">
92
+ <button
93
+ onClick={() => router.push('/admin/dashboard')}
94
+ className="p-2 hover:bg-white/5 rounded-xl transition-all border border-transparent hover:border-white/10 group"
95
+ >
96
+ <ArrowLeft size={20} className="theme-metadata group-hover:theme-title" />
97
+ </button>
98
+ <div>
99
+ <h1 className="text-2xl font-black theme-title tracking-tighter italic uppercase">{client.name}</h1>
100
+ <div className="flex items-center gap-2 mt-1">
101
+ <span className="text-[10px] theme-metadata font-bold opacity-60 uppercase tracking-widest leading-none">{client.id}</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <div className="w-full max-w-[1400px] mx-auto p-6 space-y-8">
109
+ {/* Stats Summary Cards */}
110
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
111
+ <SummaryCard title="Optimisations" value={client.jobs.filter(j => j.mode === 'OPTIMIZATION').length} icon={<Activity size={18} />} color="text-sky-400" />
112
+ <SummaryCard title="Briefs Stratégiques" value={client.jobs.filter(j => j.isBrief).length} icon={<BarChart3 size={18} />} color="text-amber-400" />
113
+ <SummaryCard title="Sources Veille" value={client.siteCount} icon={<Globe size={18} />} color="text-indigo-400" />
114
+ <SummaryCard title="Moyenne SEO" value="54%" icon={<TrendingUp size={18} />} color="text-emerald-400" />
115
+ </div>
116
+
117
+ {/* Main Content Area */}
118
+ <div className="theme-card border border-[var(--border-subtle)] rounded-3xl overflow-hidden shadow-2xl">
119
+ {/* Tabs */}
120
+ <div className="flex border-b border-[var(--border-subtle)] bg-[var(--bg-card)]/50 p-2 gap-2">
121
+ <TabButton id="content" label="Travaux de Contenu" icon={<FileText size={16} />} active={activeTab} onClick={setActiveTab} />
122
+ <TabButton id="scans" label="Événements de Veille" icon={<Clock size={16} />} active={activeTab} onClick={setActiveTab} />
123
+ <TabButton id="sources" label="Sources & Flux RSS" icon={<Database size={16} />} active={activeTab} onClick={setActiveTab} />
124
+ <TabButton id="quotas" label="Quotas API" icon={<Zap size={16} />} active={activeTab} onClick={setActiveTab} />
125
+ </div>
126
+
127
+ <div className="p-6">
128
+ {activeTab === 'content' && (
129
+ <table className="w-full">
130
+ <thead>
131
+ <tr className="border-b border-[var(--border-subtle)]">
132
+ <th className="text-left py-4 px-4 text-[10px] font-black theme-metadata uppercase tracking-widest">Date / Heure</th>
133
+ <th className="text-left py-4 px-4 text-[10px] font-black theme-metadata uppercase tracking-widest">Type / Libellé</th>
134
+ <th className="text-right py-4 px-4 text-[10px] font-black theme-metadata uppercase tracking-widest">État</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody className="divide-y divide-[var(--border-subtle)]/30">
138
+ {client.jobs.map((job) => (
139
+ <tr key={job.id} className="hover:bg-white/[0.02] transition-colors group">
140
+ <td className="py-4 px-4 text-xs font-mono theme-metadata">
141
+ {new Date(job.created_at).toLocaleString('fr-FR')}
142
+ </td>
143
+ <td className="py-4 px-4">
144
+ <div className="flex items-center gap-3">
145
+ <span className="font-bold theme-title uppercase tracking-tighter text-xs group-hover:theme-accent transition-colors">
146
+ {job.target_keyword}
147
+ </span>
148
+ {job.isBrief && (
149
+ <span className="px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-500 border border-amber-500/20 text-[8px] font-black uppercase">Brief</span>
150
+ )}
151
+ </div>
152
+ </td>
153
+ <td className="py-4 px-4 text-right">
154
+ <span className={`px-2 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest border ${
155
+ job.mode === 'CREATION' ? 'bg-purple-500/10 text-purple-400 border-purple-500/20' : 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'
156
+ }`}>
157
+ {job.mode}
158
+ </span>
159
+ </td>
160
+ </tr>
161
+ ))}
162
+ </tbody>
163
+ </table>
164
+ )}
165
+
166
+ {activeTab === 'scans' && (
167
+ <table className="w-full">
168
+ <thead>
169
+ <tr className="border-b border-[var(--border-subtle)]">
170
+ <th className="text-left py-4 px-4 text-[10px] font-black theme-metadata uppercase tracking-widest">Horodateur</th>
171
+ <th className="text-left py-4 px-4 text-[10px] font-black theme-metadata uppercase tracking-widest">Type de Scan</th>
172
+ <th className="text-right py-4 px-4 text-[10px] font-black theme-metadata uppercase tracking-widest">Statut</th>
173
+ </tr>
174
+ </thead>
175
+ <tbody className="divide-y divide-[var(--border-subtle)]/30">
176
+ {client.scans.map((scan) => (
177
+ <tr key={scan.id} className="hover:bg-white/[0.02] transition-colors group">
178
+ <td className="py-4 px-4 text-xs font-mono theme-metadata">
179
+ {new Date(scan.created_at).toLocaleString('fr-FR')}
180
+ </td>
181
+ <td className="py-4 px-4">
182
+ <span className="font-bold theme-title uppercase tracking-tighter text-xs">
183
+ {scan.label}
184
+ </span>
185
+ </td>
186
+ <td className="py-4 px-4 text-right">
187
+ <div className="flex items-center justify-end gap-2">
188
+ <span className={`text-[9px] font-black uppercase tracking-widest ${
189
+ scan.status === 'completed' ? 'text-emerald-400' : scan.status === 'error' ? 'text-red-400' : 'text-amber-400'
190
+ }`}>
191
+ {scan.status}
192
+ </span>
193
+ {scan.status === 'completed' ? <CheckCircle2 size={12} className="text-emerald-500" /> : <Clock size={12} className="text-amber-500" />}
194
+ </div>
195
+ </td>
196
+ </tr>
197
+ ))}
198
+ </tbody>
199
+ </table>
200
+ )}
201
+
202
+ {activeTab === 'sources' && (
203
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
204
+ {client.sources.map((source) => (
205
+ <div key={source.id} className="p-5 theme-card border border-[var(--border-subtle)] rounded-2xl bg-white/[0.02] hover:bg-white/[0.04] transition-all group">
206
+ <div className="flex items-start justify-between">
207
+ <div className="flex gap-4">
208
+ <div className="p-3 rounded-xl bg-indigo-500/10 border border-indigo-500/20 group-hover:scale-110 transition-transform">
209
+ <Globe size={20} className="text-indigo-400" />
210
+ </div>
211
+ <div>
212
+ <h4 className="font-bold theme-title uppercase tracking-tighter italic">{source.name}</h4>
213
+ <p className="text-[10px] theme-metadata font-mono opacity-60 truncate max-w-[200px] mt-1">{source.url || 'Aucune URL'}</p>
214
+ <div className="flex items-center gap-2 mt-3">
215
+ <span className="px-2 py-0.5 rounded-full bg-white/5 border border-white/10 text-[8px] font-black theme-metadata uppercase tracking-widest">
216
+ {source.pole_semantic || 'Non classé'}
217
+ </span>
218
+ <span className="px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-[8px] font-black uppercase tracking-widest">Actif</span>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ {source.url && (
223
+ <a href={source.url} target="_blank" rel="noopener noreferrer" className="p-2 text-white/20 hover:text-white transition-colors">
224
+ <ExternalLink size={14} />
225
+ </a>
226
+ )}
227
+ </div>
228
+ </div>
229
+ ))}
230
+ </div>
231
+ )}
232
+
233
+ {activeTab === 'quotas' && (
234
+ <div className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-500">
235
+ {/* Key Source Info */}
236
+ <div className="flex items-center justify-between p-6 rounded-3xl theme-card-sec border border-indigo-500/20 bg-indigo-500/[0.02]">
237
+ <div className="flex items-center gap-4">
238
+ <div className="p-3 rounded-2xl bg-indigo-500/20 text-indigo-400">
239
+ <Key size={24} />
240
+ </div>
241
+ <div>
242
+ <p className="text-[10px] font-black theme-metadata uppercase tracking-widest">Source des clés API</p>
243
+ <h3 className="text-xl font-black theme-title italic tracking-tighter uppercase">{client.quotas.currentKey}</h3>
244
+ </div>
245
+ </div>
246
+ <div className="text-right">
247
+ <div className="px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-[10px] font-black text-emerald-400 uppercase tracking-widest flex items-center gap-2">
248
+ <ShieldCheck size={14} /> Connexion Sécurisée
249
+ </div>
250
+ </div>
251
+ </div>
252
+
253
+ {/* Period Breakdown */}
254
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
255
+ <QuotaPeriodCard label="Aujourd'hui" stats={client.quotas.today} />
256
+ <QuotaPeriodCard label="Cette Semaine" stats={client.quotas.week} />
257
+ <QuotaPeriodCard label="Ce Mois" stats={client.quotas.month} icon={<Calendar size={18} />} highlight />
258
+ <QuotaPeriodCard label="Cette Année" stats={client.quotas.year} />
259
+ </div>
260
+
261
+ {/* Detailed Visualisation */}
262
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
263
+ <div className="lg:col-span-2 p-8 theme-card border border-[var(--border-subtle)] rounded-3xl">
264
+ <h4 className="text-[10px] font-black theme-metadata uppercase tracking-widest mb-8 flex items-center gap-2">
265
+ <Activity size={14} className="theme-accent" /> Détail Consommation Mensuelle
266
+ </h4>
267
+ <div className="space-y-8">
268
+ <ProgressBarItem label="OpenAI (Tokens)" value={client.quotas.month.openai} max={500000} color="bg-purple-500" unit="tokens" />
269
+ <ProgressBarItem label="Serper (Recherches)" value={client.quotas.month.serper} max={500} color="bg-sky-500" unit="requêtes" />
270
+ <ProgressBarItem label="Firecrawl (Crédits)" value={client.quotas.month.firecrawl} max={200} color="bg-emerald-500" unit="crédits" />
271
+ </div>
272
+ </div>
273
+
274
+ <div className="flex flex-col gap-6">
275
+ <div className="p-6 theme-card border border-[var(--border-subtle)] rounded-3xl flex-1 flex flex-col justify-center">
276
+ <div className="text-center">
277
+ <p className="text-[10px] font-black theme-metadata uppercase tracking-widest mb-2">Coût Infrastructure Est. (Mois)</p>
278
+ <div className="text-5xl font-black theme-title italic tracking-tighter">
279
+ ${client.quotas.month.cost.toFixed(2)}
280
+ </div>
281
+ <p className="text-[9px] font-bold text-emerald-400/60 uppercase tracking-widest mt-4">Paiement stable</p>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ )}
288
+ </div>
289
+ </div>
290
+ </div>
291
  </div>
292
+ );
293
+ }
294
 
295
+ function SummaryCard({ title, value, icon, color }: { title: string, value: any, icon: any, color: string }) {
296
+ return (
297
+ <div className="p-6 theme-card border border-[var(--border-subtle)] rounded-3xl hover:border-white/20 transition-all group">
298
+ <div className={`p-2 w-fit rounded-lg bg-white/5 border border-white/10 mb-4 ${color} group-hover:scale-110 transition-transform`}>
299
+ {icon}
300
+ </div>
301
+ <h3 className="text-[10px] font-black theme-metadata uppercase tracking-widest mb-1">{title}</h3>
302
+ <div className="text-4xl font-black theme-title italic tracking-tighter leading-none">{value}</div>
303
  </div>
304
+ );
305
+ }
306
+
307
+ function TabButton({ id, label, icon, active, onClick }: { id: string, label: string, icon: any, active: string, onClick: any }) {
308
+ const isActive = active === id;
309
+ return (
310
+ <button
311
+ onClick={() => onClick(id)}
312
+ className={`flex items-center gap-2 px-6 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all ${
313
+ isActive ? 'bg-[var(--accent)] text-[var(--accent-foreground)] shadow-lg shadow-[var(--accent-glow)]/20' : 'theme-metadata hover:bg-white/5 hover:theme-title'
314
+ }`}
315
+ >
316
+ {icon}
317
+ {label}
318
+ </button>
319
+ );
320
+ }
321
 
322
+ function QuotaPeriodCard({ label, stats, icon = <Clock size={18} />, highlight = false }: { label: string, stats: QuotaStats, icon?: any, highlight?: boolean }) {
323
+ return (
324
+ <div className={`p-5 rounded-3xl border transition-all ${
325
+ highlight ? 'bg-white/[0.04] border-white/20 scale-105 shadow-xl' : 'theme-card border-[var(--border-subtle)]'
326
+ }`}>
327
+ <div className="flex items-center justify-between mb-4">
328
+ <span className="text-[10px] font-black theme-metadata uppercase tracking-widest">{label}</span>
329
+ <div className="theme-metadata opacity-40">{icon}</div>
330
+ </div>
331
+ <div className="space-y-4">
332
+ <div>
333
+ <div className="text-[8px] font-black theme-metadata uppercase tracking-widest mb-1 opacity-60">OpenAI</div>
334
+ <div className="text-xl font-black theme-title tracking-tighter italic">{stats.openai.toLocaleString()}</div>
335
+ </div>
336
+ <div>
337
+ <div className="text-[8px] font-black theme-metadata uppercase tracking-widest mb-1 opacity-60">Scrap / Search</div>
338
+ <div className="text-xl font-black theme-title tracking-tighter italic">{stats.serper + stats.firecrawl}</div>
339
  </div>
340
+ <div className="pt-2 border-t border-white/5 flex items-center justify-between">
341
+ <span className="text-[9px] font-black theme-metadata uppercase">Dépense</span>
342
+ <span className="text-xs font-black theme-title">${stats.cost.toFixed(3)}</span>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  }
348
 
349
+ function ProgressBarItem({ label, value, max, color, unit }: { label: string, value: number, max: number, color: string, unit: string }) {
350
+ const pc = Math.min((value / max) * 100, 100);
351
+ return (
352
+ <div className="space-y-3">
353
+ <div className="flex justify-between items-end">
354
+ <span className="text-xs font-black theme-title uppercase tracking-wider">{label}</span>
355
+ <span className="text-[10px] font-black theme-metadata uppercase tracking-widest">
356
+ <span className="theme-title">{value.toLocaleString()}</span> / {max.toLocaleString()} {unit}
357
+ </span>
358
+ </div>
359
+ <div className="h-3 w-full bg-white/5 rounded-full overflow-hidden border border-white/5">
360
+ <div className={`h-full ${color} rounded-full transition-all duration-1000 ease-out shadow-lg`} style={{ width: `${pc}%` }} />
361
+ </div>
362
+ </div>
363
+ );
364
  }