@hed-hog/developer-mode 0.0.194 → 0.0.197

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1251 @@
1
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
2
+ import { Badge } from '@/components/ui/badge';
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from '@/components/ui/card';
11
+ import { Checkbox } from '@/components/ui/checkbox';
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogHeader,
17
+ DialogTitle,
18
+ } from '@/components/ui/dialog';
19
+ import {
20
+ DropdownMenu,
21
+ DropdownMenuContent,
22
+ DropdownMenuItem,
23
+ DropdownMenuLabel,
24
+ DropdownMenuSeparator,
25
+ DropdownMenuTrigger,
26
+ } from '@/components/ui/dropdown-menu';
27
+ import { Input } from '@/components/ui/input';
28
+ import { Label } from '@/components/ui/label';
29
+ import { Progress } from '@/components/ui/progress';
30
+ import { ScrollArea } from '@/components/ui/scroll-area';
31
+ import { cn } from '@/lib/utils';
32
+ import { useApp } from '@hed-hog/next-app-provider';
33
+ import {
34
+ AlertCircle,
35
+ ArrowLeft,
36
+ ArrowRight,
37
+ ArrowUp,
38
+ Check,
39
+ CheckCircle2,
40
+ Clock,
41
+ Database,
42
+ Download,
43
+ ExternalLink,
44
+ FolderGit2,
45
+ GitBranch,
46
+ GitCommit,
47
+ GitPullRequest,
48
+ HardDrive,
49
+ Loader2,
50
+ MoreVertical,
51
+ Package,
52
+ Play,
53
+ Plus,
54
+ Table2,
55
+ Terminal,
56
+ Zap,
57
+ } from 'lucide-react';
58
+ import { useTranslations } from 'next-intl';
59
+ import * as React from 'react';
60
+ import {
61
+ frameworkIcons,
62
+ getFrameworkLabel,
63
+ getQuickActions,
64
+ getStatusLabel,
65
+ mockStats,
66
+ statusColors,
67
+ variantStyles,
68
+ } from './data';
69
+ import { GitIcon, HedHogIcon, NodeJSIcon, PostgreSQLIcon } from './icons';
70
+ import {
71
+ AppInfo,
72
+ CommitInfo,
73
+ LibraryInfo,
74
+ QuickAction,
75
+ WizardConfig,
76
+ } from './types';
77
+
78
+ // ─── Stat Item ──────────────────────────────────────────────────────────────
79
+
80
+ function StatItem({
81
+ title,
82
+ value,
83
+ icon,
84
+ }: {
85
+ title: string;
86
+ value: string | number;
87
+ icon: React.ReactNode;
88
+ }) {
89
+ return (
90
+ <div className="flex items-center gap-3 p-3 rounded-lg bg-secondary/30 border border-border">
91
+ <div className="text-muted-foreground">{icon}</div>
92
+ <div className="min-w-0">
93
+ <p className="text-xs text-muted-foreground truncate">{title}</p>
94
+ <p className="text-sm font-medium text-foreground truncate">{value}</p>
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ // ─── Stats Cards ────────────────────────────────────────────────────────────
101
+
102
+ export function StatsCards({ stats }: { stats: typeof mockStats }) {
103
+ const t = useTranslations('developer-mode.Sections');
104
+
105
+ return (
106
+ <Card className="bg-card border-border">
107
+ <CardContent className="p-4">
108
+ <div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-2">
109
+ <StatItem
110
+ title={t('stats.node')}
111
+ value={stats.nodeVersion}
112
+ icon={<NodeJSIcon className="h-4 w-4" />}
113
+ />
114
+ <StatItem
115
+ title={t('stats.disk')}
116
+ value={stats.diskUsage}
117
+ icon={<HardDrive className="h-4 w-4" />}
118
+ />
119
+ <StatItem
120
+ title={t('stats.apps')}
121
+ value={stats.totalApps}
122
+ icon={<Package className="h-4 w-4" />}
123
+ />
124
+ <StatItem
125
+ title={t('stats.libraries')}
126
+ value={stats.totalLibraries}
127
+ icon={<FolderGit2 className="h-4 w-4" />}
128
+ />
129
+ <StatItem
130
+ title={t('stats.dbConnections')}
131
+ value={stats.dbConnections}
132
+ icon={<Database className="h-4 w-4" />}
133
+ />
134
+ <StatItem
135
+ title={t('stats.branch')}
136
+ value={stats.gitBranch}
137
+ icon={<GitBranch className="h-4 w-4" />}
138
+ />
139
+ <StatItem
140
+ title={t('stats.commit')}
141
+ value={stats.lastCommit}
142
+ icon={<GitCommit className="h-4 w-4" />}
143
+ />
144
+ <StatItem
145
+ title={t('stats.uptime')}
146
+ value={stats.uptime}
147
+ icon={<Clock className="h-4 w-4" />}
148
+ />
149
+ </div>
150
+ </CardContent>
151
+ </Card>
152
+ );
153
+ }
154
+
155
+ // ─── Quick Actions Bar ──────────────────────────────────────────────────────
156
+
157
+ export function QuickActionsBar({
158
+ onAction,
159
+ }: {
160
+ onAction: (action: QuickAction) => void;
161
+ }) {
162
+ const t = useTranslations('developer-mode.Sections');
163
+ const quickActions = React.useMemo(
164
+ () => getQuickActions((key, values) => t(key, values)),
165
+ [t]
166
+ );
167
+
168
+ return (
169
+ <Card className="bg-card border-border">
170
+ <CardHeader className="pb-3">
171
+ <div className="flex items-center justify-between">
172
+ <div className="flex items-center gap-2">
173
+ <Zap className="h-4 w-4 text-accent" />
174
+ <CardTitle className="text-sm text-foreground">
175
+ {t('quickActions.title')}
176
+ </CardTitle>
177
+ </div>
178
+ </div>
179
+ </CardHeader>
180
+ <CardContent className="pt-0">
181
+ <div className="flex flex-wrap gap-2">
182
+ {quickActions.map((action) => (
183
+ <Button
184
+ key={action.id}
185
+ variant="outline"
186
+ size="sm"
187
+ className={cn(
188
+ 'gap-2 text-xs bg-transparent border-border transition-all',
189
+ variantStyles[action.variant]
190
+ )}
191
+ onClick={() => onAction(action)}
192
+ title={action.description}
193
+ >
194
+ {action.icon}
195
+ {action.label}
196
+ </Button>
197
+ ))}
198
+ </div>
199
+ </CardContent>
200
+ </Card>
201
+ );
202
+ }
203
+
204
+ // ─── Apps Grid ──────────────────────────────────────────────────────────────
205
+
206
+ export function AppsGrid({
207
+ apps,
208
+ onRunScript,
209
+ }: {
210
+ apps: AppInfo[];
211
+ onRunScript: (app: AppInfo, script: string) => void;
212
+ }) {
213
+ const t = useTranslations('developer-mode.Sections');
214
+
215
+ return (
216
+ <Card className="bg-card border-border">
217
+ <CardHeader>
218
+ <div className="flex items-center justify-between">
219
+ <div>
220
+ <CardTitle className="text-foreground">{t('apps.title')}</CardTitle>
221
+ <CardDescription className="text-muted-foreground">
222
+ {t('apps.description')}
223
+ </CardDescription>
224
+ </div>
225
+ <Badge variant="outline" className="font-mono">
226
+ {t('apps.total', { count: apps.length })}
227
+ </Badge>
228
+ </div>
229
+ </CardHeader>
230
+ <CardContent>
231
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
232
+ {apps.map((app) => (
233
+ <div
234
+ key={app.name}
235
+ className="flex items-start gap-4 p-4 rounded-lg border border-border bg-secondary/30 hover:bg-secondary/50 transition-colors"
236
+ >
237
+ <div className="shrink-0 p-2 rounded-lg bg-background">
238
+ {frameworkIcons[app.framework]}
239
+ </div>
240
+ <div className="flex-1 min-w-0">
241
+ <div className="flex items-center gap-2 mb-1">
242
+ <h3 className="font-semibold text-foreground truncate">
243
+ {app.name}
244
+ </h3>
245
+ <Badge
246
+ variant="outline"
247
+ className={cn('text-xs', statusColors[app.status])}
248
+ >
249
+ {getStatusLabel(app.status, (key, values) =>
250
+ t(key, values)
251
+ )}
252
+ </Badge>
253
+ </div>
254
+ <p className="text-sm text-muted-foreground mb-2">
255
+ {getFrameworkLabel(app.framework, (key, values) =>
256
+ t(key, values)
257
+ )}
258
+ {app.database && (
259
+ <span className="inline-flex items-center gap-1 ml-2">
260
+ <PostgreSQLIcon className="h-3 w-3" />
261
+ {app.database.toUpperCase()}
262
+ </span>
263
+ )}
264
+ </p>
265
+ <p className="text-xs text-muted-foreground mb-3 line-clamp-2">
266
+ {app.description}
267
+ </p>
268
+ <div className="flex flex-wrap items-center gap-2">
269
+ {app.port && (
270
+ <Button
271
+ variant="outline"
272
+ size="sm"
273
+ className="h-7 text-xs bg-transparent"
274
+ asChild
275
+ >
276
+ <a
277
+ href={`http://localhost:${app.port}`}
278
+ target="_blank"
279
+ rel="noreferrer"
280
+ >
281
+ <ExternalLink className="h-3 w-3 mr-1" />:{app.port}
282
+ </a>
283
+ </Button>
284
+ )}
285
+ <Button
286
+ variant="outline"
287
+ size="sm"
288
+ className="h-7 text-xs bg-transparent"
289
+ onClick={() => onRunScript(app, 'dev')}
290
+ >
291
+ <Play className="h-3 w-3 mr-1" />
292
+ {t('apps.actions.dev')}
293
+ </Button>
294
+ <Button
295
+ variant="outline"
296
+ size="sm"
297
+ className="h-7 text-xs bg-transparent"
298
+ onClick={() => onRunScript(app, 'build')}
299
+ >
300
+ <Terminal className="h-3 w-3 mr-1" />
301
+ {t('apps.actions.build')}
302
+ </Button>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ ))}
307
+ </div>
308
+ </CardContent>
309
+ </Card>
310
+ );
311
+ }
312
+
313
+ // ─── Libraries List ─────────────────────────────────────────────────────────
314
+
315
+ export function LibrariesList({
316
+ libraries,
317
+ onRunScript,
318
+ onInstall,
319
+ onUpdate,
320
+ onCreateLibrary,
321
+ }: {
322
+ libraries: LibraryInfo[];
323
+ onRunScript: (library: LibraryInfo, script: string) => void;
324
+ onInstall: (library: LibraryInfo) => void;
325
+ onUpdate: (library: LibraryInfo) => void;
326
+ onCreateLibrary: () => void;
327
+ }) {
328
+ const t = useTranslations('developer-mode.Sections');
329
+ const installedCount = libraries.filter((l) => l.installed).length;
330
+ const updatesAvailable = libraries.filter(
331
+ (l) => l.installed && l.latestVersion && l.version !== l.latestVersion
332
+ ).length;
333
+
334
+ return (
335
+ <Card className="bg-card border-border h-full flex flex-col">
336
+ <CardHeader className="pb-4">
337
+ <div className="flex items-center justify-between">
338
+ <div className="flex items-center gap-3">
339
+ <div className="p-2.5 rounded-xl bg-accent/10 border border-accent/20">
340
+ <HedHogIcon className="h-6 w-6 text-accent" />
341
+ </div>
342
+ <div>
343
+ <CardTitle className="text-lg text-foreground">
344
+ {t('libraries.title')}
345
+ </CardTitle>
346
+ <CardDescription className="text-muted-foreground">
347
+ {t('libraries.installedCount', {
348
+ installed: installedCount,
349
+ total: libraries.length,
350
+ })}
351
+ {updatesAvailable > 0 && (
352
+ <span className="text-warning ml-2">
353
+ {t('libraries.updates', { count: updatesAvailable })}
354
+ </span>
355
+ )}
356
+ </CardDescription>
357
+ </div>
358
+ </div>
359
+ <Button
360
+ variant="outline"
361
+ size="sm"
362
+ className="gap-2 bg-transparent hover:bg-accent/10 hover:text-accent hover:border-accent/50"
363
+ onClick={onCreateLibrary}
364
+ >
365
+ <Plus className="h-4 w-4" />
366
+ {t('libraries.actions.newLibrary')}
367
+ </Button>
368
+ </div>
369
+ </CardHeader>
370
+ <CardContent className="flex-1 p-0">
371
+ <ScrollArea className="h-[500px]">
372
+ <div className="px-6 pb-6 space-y-3">
373
+ {libraries.map((lib) => {
374
+ const hasUpdate =
375
+ lib.installed &&
376
+ lib.latestVersion &&
377
+ lib.version !== lib.latestVersion;
378
+ return (
379
+ <div
380
+ key={lib.name}
381
+ className={cn(
382
+ 'rounded-xl border overflow-hidden transition-all duration-200',
383
+ lib.installed
384
+ ? 'border-border bg-secondary/20 hover:bg-secondary/40'
385
+ : 'border-dashed border-border/50 bg-background/30 opacity-70 hover:opacity-100'
386
+ )}
387
+ >
388
+ <div className="p-4">
389
+ <div className="flex items-start gap-4">
390
+ <div
391
+ className={cn(
392
+ 'p-2.5 rounded-lg shrink-0',
393
+ lib.installed ? 'bg-accent/10' : 'bg-muted/50'
394
+ )}
395
+ >
396
+ <Package
397
+ className={cn(
398
+ 'h-5 w-5',
399
+ lib.installed
400
+ ? 'text-accent'
401
+ : 'text-muted-foreground'
402
+ )}
403
+ />
404
+ </div>
405
+ <div className="flex-1 min-w-0">
406
+ <div className="flex items-center gap-2 flex-wrap">
407
+ <span className="font-mono text-sm font-semibold text-foreground">
408
+ @hedhog/{lib.name}
409
+ </span>
410
+ {lib.installed && (
411
+ <Badge
412
+ variant="outline"
413
+ className="text-xs font-mono bg-secondary/50"
414
+ >
415
+ v{lib.version}
416
+ </Badge>
417
+ )}
418
+ {hasUpdate && (
419
+ <Badge className="text-xs bg-warning/20 text-warning border-warning/30 gap-1">
420
+ <ArrowUp className="h-3 w-3" />v
421
+ {lib.latestVersion}
422
+ </Badge>
423
+ )}
424
+ {lib.installed && (
425
+ <Check className="h-4 w-4 text-success ml-auto shrink-0" />
426
+ )}
427
+ </div>
428
+ <p className="text-xs text-muted-foreground mt-1 line-clamp-2">
429
+ {lib.description}
430
+ </p>
431
+ <div className="flex items-center gap-2 mt-3">
432
+ {lib.installed ? (
433
+ <>
434
+ {lib.scripts.length > 0 && (
435
+ <DropdownMenu>
436
+ <DropdownMenuTrigger asChild>
437
+ <Button
438
+ variant="outline"
439
+ size="sm"
440
+ className="h-7 text-xs gap-1.5 bg-transparent hover:bg-accent/10 hover:text-accent hover:border-accent/50"
441
+ >
442
+ <Terminal className="h-3 w-3" />
443
+ {t('libraries.actions.scripts')}
444
+ <Badge
445
+ variant="secondary"
446
+ className="ml-1 h-4 px-1 text-[10px]"
447
+ >
448
+ {lib.scripts.length}
449
+ </Badge>
450
+ </Button>
451
+ </DropdownMenuTrigger>
452
+ <DropdownMenuContent
453
+ align="start"
454
+ className="w-56"
455
+ >
456
+ <DropdownMenuLabel className="text-xs">
457
+ {t('libraries.availableScripts')}
458
+ </DropdownMenuLabel>
459
+ <DropdownMenuSeparator />
460
+ {lib.scripts.map((script) => (
461
+ <DropdownMenuItem
462
+ key={script.name}
463
+ className="gap-2 cursor-pointer font-mono text-xs"
464
+ onClick={() =>
465
+ onRunScript(lib, script.name)
466
+ }
467
+ >
468
+ <Play className="h-3 w-3 text-accent" />
469
+ npm run {script.name}
470
+ </DropdownMenuItem>
471
+ ))}
472
+ </DropdownMenuContent>
473
+ </DropdownMenu>
474
+ )}
475
+ {hasUpdate && (
476
+ <Button
477
+ variant="outline"
478
+ size="sm"
479
+ className="h-7 text-xs gap-1.5 bg-transparent hover:bg-warning/10 hover:text-warning hover:border-warning/50"
480
+ onClick={() => onUpdate(lib)}
481
+ >
482
+ <ArrowUp className="h-3 w-3" />
483
+ {t('libraries.actions.update')}
484
+ </Button>
485
+ )}
486
+ <DropdownMenu>
487
+ <DropdownMenuTrigger asChild>
488
+ <Button
489
+ variant="ghost"
490
+ size="sm"
491
+ className="h-7 w-7 p-0 ml-auto"
492
+ >
493
+ <MoreVertical className="h-4 w-4" />
494
+ </Button>
495
+ </DropdownMenuTrigger>
496
+ <DropdownMenuContent align="end">
497
+ <DropdownMenuItem className="gap-2 cursor-pointer text-xs">
498
+ <ExternalLink className="h-3 w-3" />
499
+ {t('libraries.menu.viewNpm')}
500
+ </DropdownMenuItem>
501
+ <DropdownMenuItem className="gap-2 cursor-pointer text-xs">
502
+ <Package className="h-3 w-3" />
503
+ {t('libraries.menu.configure')}
504
+ </DropdownMenuItem>
505
+ <DropdownMenuSeparator />
506
+ <DropdownMenuItem className="gap-2 cursor-pointer text-xs text-destructive">
507
+ <Package className="h-3 w-3" />
508
+ {t('libraries.menu.uninstall')}
509
+ </DropdownMenuItem>
510
+ </DropdownMenuContent>
511
+ </DropdownMenu>
512
+ </>
513
+ ) : (
514
+ <Button
515
+ variant="outline"
516
+ size="sm"
517
+ className="h-7 text-xs gap-1.5 bg-transparent hover:bg-accent/10 hover:text-accent hover:border-accent/50"
518
+ onClick={() => onInstall(lib)}
519
+ >
520
+ <Download className="h-3 w-3" />
521
+ {t('libraries.actions.install')}
522
+ </Button>
523
+ )}
524
+ </div>
525
+ </div>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ );
530
+ })}
531
+ </div>
532
+ </ScrollArea>
533
+ </CardContent>
534
+ </Card>
535
+ );
536
+ }
537
+
538
+ // ─── Git Info ───────────────────────────────────────────────────────────────
539
+
540
+ export function GitInfo({
541
+ repoName,
542
+ currentBranch,
543
+ totalBranches,
544
+ openPRs,
545
+ recentCommits,
546
+ }: {
547
+ repoName: string;
548
+ currentBranch: string;
549
+ totalBranches: number;
550
+ openPRs: number;
551
+ recentCommits: CommitInfo[];
552
+ }) {
553
+ const t = useTranslations('developer-mode.Sections');
554
+
555
+ return (
556
+ <Card className="bg-card border-border h-full">
557
+ <CardHeader>
558
+ <div className="flex items-center gap-3">
559
+ <div className="p-2 rounded-lg bg-[#F05032]/10">
560
+ <GitIcon className="h-5 w-5" />
561
+ </div>
562
+ <div>
563
+ <CardTitle className="text-foreground">{repoName}</CardTitle>
564
+ <CardDescription className="text-muted-foreground">
565
+ {t('git.repository')}
566
+ </CardDescription>
567
+ </div>
568
+ </div>
569
+ </CardHeader>
570
+ <CardContent className="space-y-4">
571
+ <div className="flex items-center gap-3">
572
+ <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary/50 border border-border">
573
+ <GitBranch className="h-4 w-4 text-muted-foreground" />
574
+ <span className="font-mono text-sm text-foreground">
575
+ {currentBranch}
576
+ </span>
577
+ </div>
578
+ <Badge variant="outline" className="font-mono text-xs">
579
+ {t('git.branches', { count: totalBranches })}
580
+ </Badge>
581
+ {openPRs > 0 && (
582
+ <Badge className="bg-info/20 text-info border-info/30">
583
+ <GitPullRequest className="h-3 w-3 mr-1" />
584
+ {t('git.openPrs', { count: openPRs })}
585
+ </Badge>
586
+ )}
587
+ </div>
588
+ <div>
589
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
590
+ {t('git.recentCommits')}
591
+ </h4>
592
+ <ScrollArea className="h-[180px]">
593
+ <div className="space-y-3 pr-4">
594
+ {recentCommits.map((commit) => (
595
+ <div
596
+ key={commit.hash}
597
+ className="flex items-start gap-3 p-2 rounded-lg hover:bg-secondary/30 transition-colors"
598
+ >
599
+ <Avatar className="h-8 w-8">
600
+ <AvatarImage src={commit.authorAvatar} />
601
+ <AvatarFallback className="bg-secondary text-xs">
602
+ {commit.author
603
+ .split(' ')
604
+ .map((n) => n[0])
605
+ .join('')
606
+ .toUpperCase()}
607
+ </AvatarFallback>
608
+ </Avatar>
609
+ <div className="flex-1 min-w-0">
610
+ <p className="text-sm text-foreground line-clamp-1">
611
+ {commit.message}
612
+ </p>
613
+ <div className="flex items-center gap-2 mt-1">
614
+ <span className="font-mono text-xs text-accent">
615
+ {commit.hash.substring(0, 7)}
616
+ </span>
617
+ <span className="text-xs text-muted-foreground">
618
+ {commit.author}
619
+ </span>
620
+ <span className="text-xs text-muted-foreground">
621
+ {commit.date}
622
+ </span>
623
+ </div>
624
+ </div>
625
+ </div>
626
+ ))}
627
+ </div>
628
+ </ScrollArea>
629
+ </div>
630
+ </CardContent>
631
+ </Card>
632
+ );
633
+ }
634
+
635
+ // ─── Database Info Card ─────────────────────────────────────────────────────
636
+
637
+ export function DatabaseInfoCard({
638
+ name,
639
+ host,
640
+ tables,
641
+ size,
642
+ connections,
643
+ lastMigration,
644
+ }: {
645
+ name: string;
646
+ host: string;
647
+ tables: number;
648
+ size: string;
649
+ connections: { active: number; max: number };
650
+ lastMigration?: {
651
+ name: string;
652
+ date: string;
653
+ status: 'success' | 'pending' | 'failed';
654
+ };
655
+ }) {
656
+ const t = useTranslations('developer-mode.Sections');
657
+ const connectionPercentage = (connections.active / connections.max) * 100;
658
+ const migrationStatusColors: Record<string, string> = {
659
+ success: 'bg-success/20 text-success border-success/30',
660
+ pending: 'bg-warning/20 text-warning border-warning/30',
661
+ failed: 'bg-destructive/20 text-destructive border-destructive/30',
662
+ };
663
+
664
+ return (
665
+ <Card className="bg-card border-border">
666
+ <CardHeader>
667
+ <div className="flex items-center justify-between">
668
+ <div className="flex items-center gap-3">
669
+ <div className="p-2 rounded-lg bg-[#336791]/20">
670
+ <PostgreSQLIcon className="h-5 w-5" />
671
+ </div>
672
+ <div>
673
+ <CardTitle className="text-foreground">{name}</CardTitle>
674
+ <CardDescription className="text-muted-foreground font-mono text-xs">
675
+ {host}
676
+ </CardDescription>
677
+ </div>
678
+ </div>
679
+ <Badge variant="outline" className="font-mono text-xs uppercase">
680
+ {t('database.type')}
681
+ </Badge>
682
+ </div>
683
+ </CardHeader>
684
+ <CardContent className="space-y-4">
685
+ <div className="grid grid-cols-2 gap-4">
686
+ <div className="flex items-center gap-3 p-3 rounded-lg bg-secondary/30">
687
+ <Table2 className="h-4 w-4 text-muted-foreground" />
688
+ <div>
689
+ <p className="text-lg font-semibold text-foreground">{tables}</p>
690
+ <p className="text-xs text-muted-foreground">
691
+ {t('database.tables')}
692
+ </p>
693
+ </div>
694
+ </div>
695
+ <div className="flex items-center gap-3 p-3 rounded-lg bg-secondary/30">
696
+ <HardDrive className="h-4 w-4 text-muted-foreground" />
697
+ <div>
698
+ <p className="text-lg font-semibold text-foreground">{size}</p>
699
+ <p className="text-xs text-muted-foreground">
700
+ {t('database.size')}
701
+ </p>
702
+ </div>
703
+ </div>
704
+ </div>
705
+ <div className="space-y-2">
706
+ <div className="flex items-center justify-between text-sm">
707
+ <span className="text-muted-foreground">
708
+ {t('database.connections')}
709
+ </span>
710
+ <span className="font-mono text-foreground">
711
+ {connections.active}/{connections.max}
712
+ </span>
713
+ </div>
714
+ <Progress value={connectionPercentage} className="h-2" />
715
+ </div>
716
+ {lastMigration && (
717
+ <div className="pt-3 border-t border-border">
718
+ <div className="flex items-center justify-between">
719
+ <div className="flex items-center gap-2">
720
+ <Clock className="h-4 w-4 text-muted-foreground" />
721
+ <span className="text-sm text-muted-foreground">
722
+ {t('database.lastMigration')}
723
+ </span>
724
+ </div>
725
+ <Badge
726
+ variant="outline"
727
+ className={migrationStatusColors[lastMigration.status]}
728
+ >
729
+ {lastMigration.status}
730
+ </Badge>
731
+ </div>
732
+ <p className="text-sm text-foreground mt-2 font-mono">
733
+ {lastMigration.name}
734
+ </p>
735
+ <p className="text-xs text-muted-foreground mt-1">
736
+ {lastMigration.date}
737
+ </p>
738
+ </div>
739
+ )}
740
+ </CardContent>
741
+ </Card>
742
+ );
743
+ }
744
+
745
+ // ─── Wizard Dialog ──────────────────────────────────────────────────────────
746
+
747
+ export function WizardDialog({
748
+ open,
749
+ onOpenChange,
750
+ config,
751
+ }: {
752
+ open: boolean;
753
+ onOpenChange: (open: boolean) => void;
754
+ config: WizardConfig;
755
+ }) {
756
+ const t = useTranslations('developer-mode.Sections');
757
+ const { accessToken, currentLocaleCode } = useApp();
758
+ const [currentStep, setCurrentStep] = React.useState(0);
759
+ const [formData, setFormData] = React.useState<Record<string, string>>({});
760
+ const [selectedOptions, setSelectedOptions] = React.useState<string[]>([]);
761
+ const [isRunning, setIsRunning] = React.useState(false);
762
+ const [output, setOutput] = React.useState<string[]>([]);
763
+ const [completed, setCompleted] = React.useState(false);
764
+ const [hasError, setHasError] = React.useState(false);
765
+ const outputRef = React.useRef<HTMLDivElement>(null);
766
+ const streamAbortRef = React.useRef<AbortController | null>(null);
767
+ const executedProgressStepRef = React.useRef<string | null>(null);
768
+ const step = config.steps[currentStep] ?? config.steps[0]!;
769
+ const apiBaseUrl =
770
+ process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_API_URL;
771
+
772
+ React.useEffect(() => {
773
+ if (outputRef.current)
774
+ outputRef.current.scrollTop = outputRef.current.scrollHeight;
775
+ }, [output]);
776
+
777
+ const simulateOutput = React.useCallback(() => {
778
+ const lines = [
779
+ `$ hedhog ${config.id}`,
780
+ ``,
781
+ t('wizard.output.starting', {
782
+ title: config.title.toLowerCase(),
783
+ }),
784
+ t('wizard.output.checkingPrerequisites'),
785
+ t('wizard.output.validatingConfiguration'),
786
+ ...(Object.keys(formData).length > 0
787
+ ? Object.entries(formData).map(([k, v]) =>
788
+ t('wizard.output.setting', { key: k, value: v })
789
+ )
790
+ : []),
791
+ ...(selectedOptions.length > 0
792
+ ? [
793
+ t('wizard.output.selectedModules', {
794
+ modules: selectedOptions.join(', '),
795
+ }),
796
+ ]
797
+ : []),
798
+ t('wizard.output.processing'),
799
+ t('wizard.output.creatingDirectories'),
800
+ t('wizard.output.generatingFiles'),
801
+ t('wizard.output.installingDependencies'),
802
+ t('wizard.output.runningPostInstallHooks'),
803
+ ``,
804
+ t('wizard.output.completedSuccessfully'),
805
+ ];
806
+ setHasError(false);
807
+ setCompleted(false);
808
+ setIsRunning(true);
809
+ setOutput([]);
810
+ lines.forEach((line, i) => {
811
+ setTimeout(
812
+ () => {
813
+ setOutput((prev) => [...prev, line]);
814
+ if (i === lines.length - 1) {
815
+ setIsRunning(false);
816
+ setCompleted(true);
817
+ }
818
+ },
819
+ (i + 1) * 300
820
+ );
821
+ });
822
+ }, [config.id, config.title, formData, selectedOptions, t]);
823
+
824
+ const runScriptOutputStream = React.useCallback(async () => {
825
+ if (!config.scriptExecution || !apiBaseUrl) {
826
+ simulateOutput();
827
+ return;
828
+ }
829
+
830
+ const abortController = new AbortController();
831
+ streamAbortRef.current = abortController;
832
+
833
+ setHasError(false);
834
+ setCompleted(false);
835
+ setIsRunning(true);
836
+ setOutput([]);
837
+
838
+ const pushOutput = (line: string) => {
839
+ setOutput((prev) => [...prev, line]);
840
+ };
841
+
842
+ pushOutput(t('wizard.output.connectingStream'));
843
+
844
+ let streamHasError = false;
845
+
846
+ try {
847
+ const response = await fetch(
848
+ `${apiBaseUrl}/developer-mode/scripts/stream`,
849
+ {
850
+ method: 'POST',
851
+ headers: {
852
+ 'Content-Type': 'application/json',
853
+ Accept: 'text/event-stream',
854
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
855
+ ...(currentLocaleCode
856
+ ? { 'Accept-Language': currentLocaleCode }
857
+ : {}),
858
+ },
859
+ body: JSON.stringify(config.scriptExecution),
860
+ signal: abortController.signal,
861
+ }
862
+ );
863
+
864
+ if (!response.ok || !response.body) {
865
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
866
+ }
867
+
868
+ pushOutput(t('wizard.output.streamConnected'));
869
+
870
+ const reader = response.body.getReader();
871
+ const decoder = new TextDecoder();
872
+ let buffer = '';
873
+
874
+ const processChunk = (rawChunk: string) => {
875
+ if (!rawChunk.trim()) return;
876
+
877
+ let event = 'message';
878
+ let data = '';
879
+
880
+ for (const line of rawChunk.split(/\r?\n/)) {
881
+ if (line.startsWith('event:')) {
882
+ event = line.replace('event:', '').trim();
883
+ }
884
+ if (line.startsWith('data:')) {
885
+ data += `${line.replace('data:', '').trim()}\n`;
886
+ }
887
+ }
888
+
889
+ const payloadText = data.trim();
890
+ if (!payloadText) return;
891
+
892
+ let payloadMessage = payloadText;
893
+ try {
894
+ const parsed = JSON.parse(payloadText) as { message?: string };
895
+ payloadMessage = parsed.message ?? payloadText;
896
+ } catch {
897
+ payloadMessage = payloadText;
898
+ }
899
+
900
+ if (event === 'error') {
901
+ streamHasError = true;
902
+ setHasError(true);
903
+ }
904
+
905
+ if (payloadMessage) {
906
+ pushOutput(payloadMessage);
907
+ }
908
+ };
909
+
910
+ while (true) {
911
+ const { done, value } = await reader.read();
912
+ if (done) {
913
+ processChunk(buffer);
914
+ break;
915
+ }
916
+
917
+ buffer += decoder.decode(value, { stream: true });
918
+ const chunks = buffer.split(/\r?\n\r?\n/);
919
+ buffer = chunks.pop() ?? '';
920
+
921
+ for (const chunk of chunks) {
922
+ processChunk(chunk);
923
+ }
924
+ }
925
+
926
+ setCompleted(!streamHasError);
927
+ } catch (error) {
928
+ if (!abortController.signal.aborted) {
929
+ const message =
930
+ error instanceof Error
931
+ ? error.message
932
+ : t('wizard.output.streamFailed');
933
+ setHasError(true);
934
+ pushOutput(message);
935
+ }
936
+ } finally {
937
+ if (streamAbortRef.current === abortController) {
938
+ streamAbortRef.current = null;
939
+ }
940
+ setIsRunning(false);
941
+ }
942
+ }, [
943
+ accessToken,
944
+ apiBaseUrl,
945
+ config.scriptExecution,
946
+ currentLocaleCode,
947
+ simulateOutput,
948
+ t,
949
+ ]);
950
+
951
+ React.useEffect(() => {
952
+ if (!open) return;
953
+ if (step.type !== 'progress') return;
954
+ if (executedProgressStepRef.current === step.id) return;
955
+
956
+ executedProgressStepRef.current = step.id;
957
+ runScriptOutputStream();
958
+ }, [open, runScriptOutputStream, step.id, step.type]);
959
+
960
+ React.useEffect(
961
+ () => () => {
962
+ streamAbortRef.current?.abort();
963
+ },
964
+ []
965
+ );
966
+
967
+ const handleNext = () => {
968
+ if (currentStep < config.steps.length - 1) {
969
+ const next = currentStep + 1;
970
+ setCurrentStep(next);
971
+ }
972
+ };
973
+
974
+ const handleClose = () => {
975
+ streamAbortRef.current?.abort();
976
+ streamAbortRef.current = null;
977
+ executedProgressStepRef.current = null;
978
+ setCurrentStep(0);
979
+ setFormData({});
980
+ setSelectedOptions([]);
981
+ setOutput([]);
982
+ setIsRunning(false);
983
+ setCompleted(false);
984
+ setHasError(false);
985
+ onOpenChange(false);
986
+ };
987
+
988
+ const handleDialogOpenChange = (nextOpen: boolean) => {
989
+ if (!nextOpen) {
990
+ handleClose();
991
+ }
992
+ };
993
+
994
+ const toggleOption = (value: string) => {
995
+ if (config.selectMultiple)
996
+ setSelectedOptions((p) =>
997
+ p.includes(value) ? p.filter((v) => v !== value) : [...p, value]
998
+ );
999
+ else setSelectedOptions([value]);
1000
+ };
1001
+
1002
+ const canProceed = () => {
1003
+ if (step.type === 'form' && config.formFields)
1004
+ return config.formFields
1005
+ .filter((f) => f.required)
1006
+ .every((f) => formData[f.name]?.trim());
1007
+ if (step.type === 'select') return selectedOptions.length > 0;
1008
+ return true;
1009
+ };
1010
+
1011
+ const renderStep = () => {
1012
+ switch (step.type) {
1013
+ case 'info':
1014
+ return (
1015
+ <div className="space-y-4 py-4">
1016
+ <div className="rounded-lg border border-border bg-secondary/30 p-4">
1017
+ <p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-line">
1018
+ {step.description}
1019
+ </p>
1020
+ </div>
1021
+ </div>
1022
+ );
1023
+ case 'form':
1024
+ return (
1025
+ <div className="space-y-4 py-4">
1026
+ {config.formFields?.map((field) => (
1027
+ <div key={field.name} className="space-y-2">
1028
+ <Label htmlFor={field.name} className="text-foreground">
1029
+ {field.label}
1030
+ {field.required && (
1031
+ <span className="text-destructive ml-1">*</span>
1032
+ )}
1033
+ </Label>
1034
+ <Input
1035
+ id={field.name}
1036
+ type={field.type || 'text'}
1037
+ placeholder={field.placeholder}
1038
+ value={formData[field.name] || ''}
1039
+ onChange={(e) =>
1040
+ setFormData((p) => ({ ...p, [field.name]: e.target.value }))
1041
+ }
1042
+ className="bg-input border-border font-mono"
1043
+ />
1044
+ </div>
1045
+ ))}
1046
+ </div>
1047
+ );
1048
+ case 'select':
1049
+ return (
1050
+ <div className="space-y-3 py-4">
1051
+ <p className="text-sm text-muted-foreground mb-4">
1052
+ {step.description}
1053
+ </p>
1054
+ <ScrollArea className="h-[250px] pr-4">
1055
+ <div className="space-y-2">
1056
+ {(config.selectOptions ?? []).map((option) => (
1057
+ <div
1058
+ key={option.value}
1059
+ className={cn(
1060
+ 'flex items-start gap-3 rounded-lg border border-border p-3 cursor-pointer transition-colors',
1061
+ selectedOptions.includes(option.value)
1062
+ ? 'bg-accent/10 border-accent'
1063
+ : 'hover:bg-secondary/50'
1064
+ )}
1065
+ onClick={() => toggleOption(option.value)}
1066
+ >
1067
+ <Checkbox
1068
+ checked={selectedOptions.includes(option.value)}
1069
+ onCheckedChange={() => toggleOption(option.value)}
1070
+ className="mt-0.5 border-muted-foreground data-[state=checked]:bg-accent data-[state=checked]:border-accent"
1071
+ />
1072
+ <div>
1073
+ <span className="font-mono text-sm text-foreground">
1074
+ @hedhog/{option.label ?? option.value}
1075
+ </span>
1076
+ {option.description && (
1077
+ <p className="text-xs text-muted-foreground mt-1">
1078
+ {option.description}
1079
+ </p>
1080
+ )}
1081
+ </div>
1082
+ </div>
1083
+ ))}
1084
+ </div>
1085
+ </ScrollArea>
1086
+ </div>
1087
+ );
1088
+ case 'progress':
1089
+ return (
1090
+ <div className="space-y-4 py-4">
1091
+ <div className="rounded-lg border border-border bg-black overflow-hidden">
1092
+ <div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-secondary/30">
1093
+ <div className="flex gap-1.5">
1094
+ <div className="w-3 h-3 rounded-full bg-destructive/50" />
1095
+ <div className="w-3 h-3 rounded-full bg-warning/50" />
1096
+ <div className="w-3 h-3 rounded-full bg-success/50" />
1097
+ </div>
1098
+ <Terminal className="h-3.5 w-3.5 text-muted-foreground ml-2" />
1099
+ <span className="text-xs text-muted-foreground font-mono">
1100
+ {t('wizard.terminal')}
1101
+ </span>
1102
+ </div>
1103
+ <ScrollArea className="h-[220px] p-4" ref={outputRef}>
1104
+ <div className="space-y-0.5 font-mono">
1105
+ {output.map((line, i) => (
1106
+ <div
1107
+ key={i}
1108
+ className={cn(
1109
+ 'text-xs',
1110
+ line
1111
+ .toLowerCase()
1112
+ .includes(
1113
+ t('wizard.output.successKeyword').toLowerCase()
1114
+ ) ||
1115
+ line
1116
+ .toLowerCase()
1117
+ .includes(
1118
+ t('wizard.output.doneKeyword').toLowerCase()
1119
+ )
1120
+ ? 'text-green-400'
1121
+ : line
1122
+ .toLowerCase()
1123
+ .includes(
1124
+ t('wizard.output.errorKeyword').toLowerCase()
1125
+ ) ||
1126
+ line
1127
+ .toLowerCase()
1128
+ .includes(
1129
+ t('wizard.output.failedKeyword').toLowerCase()
1130
+ )
1131
+ ? 'text-red-400'
1132
+ : line.startsWith('$')
1133
+ ? 'text-cyan-300'
1134
+ : line.startsWith('[HedHog]')
1135
+ ? 'text-zinc-300'
1136
+ : 'text-zinc-100'
1137
+ )}
1138
+ >
1139
+ {line || '\u00A0'}
1140
+ </div>
1141
+ ))}
1142
+ {isRunning && (
1143
+ <div className="flex items-center gap-2 text-muted-foreground">
1144
+ <Loader2 className="h-3 w-3 animate-spin" />
1145
+ <span className="text-xs">{t('wizard.processing')}</span>
1146
+ </div>
1147
+ )}
1148
+ </div>
1149
+ </ScrollArea>
1150
+ </div>
1151
+ {completed && !hasError && (
1152
+ <div className="flex items-center gap-2 p-3 rounded-lg bg-success/10 border border-success/30">
1153
+ <CheckCircle2 className="h-4 w-4 text-success" />
1154
+ <span className="text-sm text-success">
1155
+ {t('wizard.success')}
1156
+ </span>
1157
+ </div>
1158
+ )}
1159
+ {hasError && (
1160
+ <div className="flex items-center gap-2 p-3 rounded-lg bg-destructive/10 border border-destructive/30">
1161
+ <AlertCircle className="h-4 w-4 text-destructive" />
1162
+ <span className="text-sm text-destructive">
1163
+ {t('wizard.error')}
1164
+ </span>
1165
+ </div>
1166
+ )}
1167
+ </div>
1168
+ );
1169
+ default:
1170
+ return null;
1171
+ }
1172
+ };
1173
+
1174
+ return (
1175
+ <Dialog open={open} onOpenChange={handleDialogOpenChange}>
1176
+ <DialogContent className="sm:max-w-[550px] bg-card border-border">
1177
+ <DialogHeader>
1178
+ <div className="flex items-center gap-3">
1179
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent/20 text-accent">
1180
+ {config.icon}
1181
+ </div>
1182
+ <div>
1183
+ <DialogTitle className="text-foreground">
1184
+ {config.title}
1185
+ </DialogTitle>
1186
+ <DialogDescription className="text-muted-foreground">
1187
+ {step.title}
1188
+ </DialogDescription>
1189
+ </div>
1190
+ </div>
1191
+ </DialogHeader>
1192
+ <div className="flex items-center justify-center gap-2 py-2">
1193
+ {config.steps.map((s, i) => (
1194
+ <div key={s.id} className="flex items-center">
1195
+ <div
1196
+ className={cn(
1197
+ 'flex h-8 w-8 items-center justify-center rounded-full text-xs font-medium transition-colors',
1198
+ i < currentStep
1199
+ ? 'bg-accent text-accent-foreground'
1200
+ : i === currentStep
1201
+ ? 'bg-accent text-accent-foreground ring-2 ring-accent/30'
1202
+ : 'bg-secondary text-muted-foreground'
1203
+ )}
1204
+ >
1205
+ {i < currentStep ? <CheckCircle2 className="h-4 w-4" /> : i + 1}
1206
+ </div>
1207
+ {i < config.steps.length - 1 && (
1208
+ <div
1209
+ className={cn(
1210
+ 'w-8 h-0.5 mx-1',
1211
+ i < currentStep ? 'bg-accent' : 'bg-secondary'
1212
+ )}
1213
+ />
1214
+ )}
1215
+ </div>
1216
+ ))}
1217
+ </div>
1218
+ {renderStep()}
1219
+ <div className="flex justify-between pt-4 border-t border-border">
1220
+ <Button
1221
+ variant="outline"
1222
+ onClick={() => currentStep > 0 && setCurrentStep(currentStep - 1)}
1223
+ disabled={currentStep === 0 || isRunning}
1224
+ className="border-border bg-transparent"
1225
+ >
1226
+ <ArrowLeft className="mr-2 h-4 w-4" />
1227
+ {t('wizard.actions.back')}
1228
+ </Button>
1229
+ {currentStep === config.steps.length - 1 ? (
1230
+ <Button
1231
+ onClick={handleClose}
1232
+ disabled={isRunning}
1233
+ className="bg-accent text-accent-foreground hover:bg-accent/90"
1234
+ >
1235
+ {completed ? t('wizard.actions.done') : t('wizard.actions.close')}
1236
+ </Button>
1237
+ ) : (
1238
+ <Button
1239
+ onClick={handleNext}
1240
+ disabled={isRunning || !canProceed()}
1241
+ className="bg-accent text-accent-foreground hover:bg-accent/90"
1242
+ >
1243
+ {t('wizard.actions.next')}
1244
+ <ArrowRight className="ml-2 h-4 w-4" />
1245
+ </Button>
1246
+ )}
1247
+ </div>
1248
+ </DialogContent>
1249
+ </Dialog>
1250
+ );
1251
+ }