@hed-hog/core 0.0.215 → 0.0.216
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.
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +5 -5
- package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +1 -1
- package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +312 -0
- package/hedhog/frontend/app/dashboard/components/dashboard-grid.tsx.ejs +54 -0
- package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +132 -0
- package/hedhog/frontend/app/dashboard/components/dynamic-widget.tsx.ejs +88 -0
- package/hedhog/frontend/app/dashboard/components/index.ts.ejs +6 -0
- package/hedhog/frontend/app/dashboard/components/stats.tsx.ejs +93 -0
- package/hedhog/frontend/app/dashboard/components/widget-wrapper.tsx.ejs +150 -0
- package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +184 -0
- package/hedhog/frontend/app/dashboard/components/widgets/active-users-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +219 -0
- package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +191 -0
- package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +309 -0
- package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +111 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +445 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-chart.tsx.ejs +149 -0
- package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +296 -0
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-card.tsx.ejs +61 -0
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +152 -0
- package/hedhog/frontend/app/dashboard/components/widgets/profile-card.tsx.ejs +186 -0
- package/hedhog/frontend/app/dashboard/components/widgets/session-activity-chart.tsx.ejs +183 -0
- package/hedhog/frontend/app/dashboard/components/widgets/sessions-today-card.tsx.ejs +62 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +340 -0
- package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +275 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-growth-chart.tsx.ejs +210 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +130 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +233 -0
- package/hedhog/frontend/messages/en.json +143 -1
- package/hedhog/frontend/messages/pt.json +143 -1
- package/package.json +3 -3
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from '@/components/ui/card';
|
|
12
|
+
import { Input } from '@/components/ui/input';
|
|
13
|
+
import { Label } from '@/components/ui/label';
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from '@/components/ui/select';
|
|
21
|
+
import {
|
|
22
|
+
AlertCircle,
|
|
23
|
+
CheckCircle2,
|
|
24
|
+
Cloud,
|
|
25
|
+
FolderOpen,
|
|
26
|
+
HardDrive,
|
|
27
|
+
} from 'lucide-react';
|
|
28
|
+
import { useState } from 'react';
|
|
29
|
+
|
|
30
|
+
type StorageProvider = 'local' | 's3' | 'gcs' | 'azure';
|
|
31
|
+
|
|
32
|
+
const providerList: {
|
|
33
|
+
value: StorageProvider;
|
|
34
|
+
label: string;
|
|
35
|
+
icon: React.ReactNode;
|
|
36
|
+
description: string;
|
|
37
|
+
}[] = [
|
|
38
|
+
{
|
|
39
|
+
value: 'local',
|
|
40
|
+
label: 'Local',
|
|
41
|
+
icon: <FolderOpen className="h-4 w-4" />,
|
|
42
|
+
description: 'Disco do servidor',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: 's3',
|
|
46
|
+
label: 'AWS S3',
|
|
47
|
+
icon: <Cloud className="h-4 w-4" />,
|
|
48
|
+
description: 'Amazon S3 Bucket',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: 'gcs',
|
|
52
|
+
label: 'Google Cloud',
|
|
53
|
+
icon: <Cloud className="h-4 w-4" />,
|
|
54
|
+
description: 'Google Cloud Storage',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: 'azure',
|
|
58
|
+
label: 'Azure Blob',
|
|
59
|
+
icon: <Cloud className="h-4 w-4" />,
|
|
60
|
+
description: 'Azure Blob Storage',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export default function StorageConfig() {
|
|
65
|
+
const [provider, setProvider] = useState<StorageProvider>('local');
|
|
66
|
+
const [localPath, setLocalPath] = useState('/var/data/uploads');
|
|
67
|
+
const [localMaxSize, setLocalMaxSize] = useState('50');
|
|
68
|
+
|
|
69
|
+
const [s3Bucket, setS3Bucket] = useState('');
|
|
70
|
+
const [s3Region, setS3Region] = useState('us-east-1');
|
|
71
|
+
const [s3AccessKey, setS3AccessKey] = useState('');
|
|
72
|
+
const [s3SecretKey, setS3SecretKey] = useState('');
|
|
73
|
+
const [s3Prefix, setS3Prefix] = useState('uploads/');
|
|
74
|
+
|
|
75
|
+
const [gcsBucket, setGcsBucket] = useState('');
|
|
76
|
+
const [gcsProjectId, setGcsProjectId] = useState('');
|
|
77
|
+
const [gcsKeyFile, setGcsKeyFile] = useState('');
|
|
78
|
+
|
|
79
|
+
const [azureAccount, setAzureAccount] = useState('');
|
|
80
|
+
const [azureKey, setAzureKey] = useState('');
|
|
81
|
+
const [azureContainer, setAzureContainer] = useState('');
|
|
82
|
+
const [azureEndpoint, setAzureEndpoint] = useState('');
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Card className="h-full">
|
|
86
|
+
<CardHeader>
|
|
87
|
+
<div className="flex items-center justify-between">
|
|
88
|
+
<div className="flex items-center gap-3">
|
|
89
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-violet-50">
|
|
90
|
+
<HardDrive className="h-5 w-5 text-violet-600" />
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<CardTitle className="text-base">
|
|
94
|
+
Armazenamento de Arquivos
|
|
95
|
+
</CardTitle>
|
|
96
|
+
<CardDescription>
|
|
97
|
+
Configure onde os arquivos do sistema serao armazenados
|
|
98
|
+
</CardDescription>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<Badge variant="secondary" className="bg-emerald-50 text-emerald-700">
|
|
102
|
+
<CheckCircle2 className="mr-1 h-3 w-3" />
|
|
103
|
+
Configurado
|
|
104
|
+
</Badge>
|
|
105
|
+
</div>
|
|
106
|
+
</CardHeader>
|
|
107
|
+
<CardContent className="space-y-6">
|
|
108
|
+
{/* Provider Selector */}
|
|
109
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
110
|
+
{providerList.map((p) => (
|
|
111
|
+
<button
|
|
112
|
+
key={p.value}
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={() => setProvider(p.value)}
|
|
115
|
+
className={`flex flex-col items-center gap-1.5 rounded-lg border-2 p-3 text-center transition-all ${
|
|
116
|
+
provider === p.value
|
|
117
|
+
? 'border-foreground bg-foreground/3'
|
|
118
|
+
: 'border-border hover:border-foreground/20'
|
|
119
|
+
}`}
|
|
120
|
+
>
|
|
121
|
+
<div
|
|
122
|
+
className={`flex h-8 w-8 items-center justify-center rounded-full ${
|
|
123
|
+
provider === p.value
|
|
124
|
+
? 'bg-foreground text-background'
|
|
125
|
+
: 'bg-muted text-muted-foreground'
|
|
126
|
+
}`}
|
|
127
|
+
>
|
|
128
|
+
{p.icon}
|
|
129
|
+
</div>
|
|
130
|
+
<span className="text-xs font-medium">{p.label}</span>
|
|
131
|
+
</button>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Local Fields */}
|
|
136
|
+
{provider === 'local' && (
|
|
137
|
+
<div className="space-y-4">
|
|
138
|
+
<div className="space-y-2">
|
|
139
|
+
<Label htmlFor="local-path">Caminho do diretorio</Label>
|
|
140
|
+
<Input
|
|
141
|
+
id="local-path"
|
|
142
|
+
value={localPath}
|
|
143
|
+
onChange={(e) => setLocalPath(e.target.value)}
|
|
144
|
+
placeholder="/var/data/uploads"
|
|
145
|
+
className="font-mono text-sm"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="space-y-2">
|
|
149
|
+
<Label htmlFor="local-max-size">
|
|
150
|
+
Tamanho maximo por arquivo (MB)
|
|
151
|
+
</Label>
|
|
152
|
+
<Input
|
|
153
|
+
id="local-max-size"
|
|
154
|
+
type="number"
|
|
155
|
+
value={localMaxSize}
|
|
156
|
+
onChange={(e) => setLocalMaxSize(e.target.value)}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
|
160
|
+
<div className="flex items-start gap-2">
|
|
161
|
+
<AlertCircle className="mt-0.5 h-4 w-4 text-blue-600" />
|
|
162
|
+
<p className="text-xs text-blue-800">
|
|
163
|
+
Certifique-se de que o diretorio existe e que o servidor tem
|
|
164
|
+
permissoes de leitura e escrita.
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* AWS S3 Fields */}
|
|
172
|
+
{provider === 's3' && (
|
|
173
|
+
<div className="space-y-4">
|
|
174
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
175
|
+
<div className="space-y-2">
|
|
176
|
+
<Label htmlFor="s3-bucket">Bucket</Label>
|
|
177
|
+
<Input
|
|
178
|
+
id="s3-bucket"
|
|
179
|
+
value={s3Bucket}
|
|
180
|
+
onChange={(e) => setS3Bucket(e.target.value)}
|
|
181
|
+
placeholder="meu-bucket"
|
|
182
|
+
className="font-mono text-sm"
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="space-y-2">
|
|
186
|
+
<Label>Regiao</Label>
|
|
187
|
+
<Select value={s3Region} onValueChange={setS3Region}>
|
|
188
|
+
<SelectTrigger>
|
|
189
|
+
<SelectValue />
|
|
190
|
+
</SelectTrigger>
|
|
191
|
+
<SelectContent>
|
|
192
|
+
<SelectItem value="us-east-1">
|
|
193
|
+
US East (N. Virginia)
|
|
194
|
+
</SelectItem>
|
|
195
|
+
<SelectItem value="us-west-2">US West (Oregon)</SelectItem>
|
|
196
|
+
<SelectItem value="eu-west-1">EU (Ireland)</SelectItem>
|
|
197
|
+
<SelectItem value="eu-central-1">EU (Frankfurt)</SelectItem>
|
|
198
|
+
<SelectItem value="sa-east-1">
|
|
199
|
+
South America (Sao Paulo)
|
|
200
|
+
</SelectItem>
|
|
201
|
+
</SelectContent>
|
|
202
|
+
</Select>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<div className="space-y-2">
|
|
206
|
+
<Label htmlFor="s3-access-key">Access Key ID</Label>
|
|
207
|
+
<Input
|
|
208
|
+
id="s3-access-key"
|
|
209
|
+
value={s3AccessKey}
|
|
210
|
+
onChange={(e) => setS3AccessKey(e.target.value)}
|
|
211
|
+
placeholder="AKIAIOSFODNN7EXAMPLE"
|
|
212
|
+
className="font-mono text-sm"
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
<div className="space-y-2">
|
|
216
|
+
<Label htmlFor="s3-secret-key">Secret Access Key</Label>
|
|
217
|
+
<Input
|
|
218
|
+
id="s3-secret-key"
|
|
219
|
+
type="password"
|
|
220
|
+
value={s3SecretKey}
|
|
221
|
+
onChange={(e) => setS3SecretKey(e.target.value)}
|
|
222
|
+
className="font-mono text-sm"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="space-y-2">
|
|
226
|
+
<Label htmlFor="s3-prefix">Prefixo (pasta)</Label>
|
|
227
|
+
<Input
|
|
228
|
+
id="s3-prefix"
|
|
229
|
+
value={s3Prefix}
|
|
230
|
+
onChange={(e) => setS3Prefix(e.target.value)}
|
|
231
|
+
placeholder="uploads/"
|
|
232
|
+
className="font-mono text-sm"
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{/* Google Cloud Storage Fields */}
|
|
239
|
+
{provider === 'gcs' && (
|
|
240
|
+
<div className="space-y-4">
|
|
241
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
242
|
+
<div className="space-y-2">
|
|
243
|
+
<Label htmlFor="gcs-project-id">Project ID</Label>
|
|
244
|
+
<Input
|
|
245
|
+
id="gcs-project-id"
|
|
246
|
+
value={gcsProjectId}
|
|
247
|
+
onChange={(e) => setGcsProjectId(e.target.value)}
|
|
248
|
+
placeholder="meu-projeto-123"
|
|
249
|
+
className="font-mono text-sm"
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
<div className="space-y-2">
|
|
253
|
+
<Label htmlFor="gcs-bucket">Bucket</Label>
|
|
254
|
+
<Input
|
|
255
|
+
id="gcs-bucket"
|
|
256
|
+
value={gcsBucket}
|
|
257
|
+
onChange={(e) => setGcsBucket(e.target.value)}
|
|
258
|
+
placeholder="meu-bucket-gcs"
|
|
259
|
+
className="font-mono text-sm"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="space-y-2">
|
|
264
|
+
<Label htmlFor="gcs-key-file">
|
|
265
|
+
Chave JSON da Service Account
|
|
266
|
+
</Label>
|
|
267
|
+
<textarea
|
|
268
|
+
id="gcs-key-file"
|
|
269
|
+
className="flex min-h-[100px] w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
270
|
+
value={gcsKeyFile}
|
|
271
|
+
onChange={(e) => setGcsKeyFile(e.target.value)}
|
|
272
|
+
placeholder='{"type": "service_account", ...}'
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{/* Azure Blob Storage Fields */}
|
|
279
|
+
{provider === 'azure' && (
|
|
280
|
+
<div className="space-y-4">
|
|
281
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
282
|
+
<div className="space-y-2">
|
|
283
|
+
<Label htmlFor="azure-account">Storage Account Name</Label>
|
|
284
|
+
<Input
|
|
285
|
+
id="azure-account"
|
|
286
|
+
value={azureAccount}
|
|
287
|
+
onChange={(e) => setAzureAccount(e.target.value)}
|
|
288
|
+
placeholder="meuaccount"
|
|
289
|
+
className="font-mono text-sm"
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
<div className="space-y-2">
|
|
293
|
+
<Label htmlFor="azure-container">Container</Label>
|
|
294
|
+
<Input
|
|
295
|
+
id="azure-container"
|
|
296
|
+
value={azureContainer}
|
|
297
|
+
onChange={(e) => setAzureContainer(e.target.value)}
|
|
298
|
+
placeholder="arquivos"
|
|
299
|
+
className="font-mono text-sm"
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<div className="space-y-2">
|
|
304
|
+
<Label htmlFor="azure-key">Access Key</Label>
|
|
305
|
+
<Input
|
|
306
|
+
id="azure-key"
|
|
307
|
+
type="password"
|
|
308
|
+
value={azureKey}
|
|
309
|
+
onChange={(e) => setAzureKey(e.target.value)}
|
|
310
|
+
className="font-mono text-sm"
|
|
311
|
+
/>
|
|
312
|
+
</div>
|
|
313
|
+
<div className="space-y-2">
|
|
314
|
+
<Label htmlFor="azure-endpoint">
|
|
315
|
+
Endpoint personalizado (opcional)
|
|
316
|
+
</Label>
|
|
317
|
+
<Input
|
|
318
|
+
id="azure-endpoint"
|
|
319
|
+
value={azureEndpoint}
|
|
320
|
+
onChange={(e) => setAzureEndpoint(e.target.value)}
|
|
321
|
+
placeholder="https://meuaccount.blob.core.windows.net"
|
|
322
|
+
className="font-mono text-sm"
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
|
|
328
|
+
{/* Actions */}
|
|
329
|
+
<div className="flex items-center justify-end border-t pt-4">
|
|
330
|
+
<div className="flex gap-2">
|
|
331
|
+
<Button variant="outline" size="sm">
|
|
332
|
+
Testar conexao
|
|
333
|
+
</Button>
|
|
334
|
+
<Button size="sm">Salvar</Button>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</CardContent>
|
|
338
|
+
</Card>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
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 { Input } from '@/components/ui/input';
|
|
12
|
+
import { Label } from '@/components/ui/label';
|
|
13
|
+
import { ImageIcon, Palette, Upload } from 'lucide-react';
|
|
14
|
+
import { useRef, useState } from 'react';
|
|
15
|
+
|
|
16
|
+
const PRESET_COLORS = [
|
|
17
|
+
{ name: 'Azul', value: '#3b82f6' },
|
|
18
|
+
{ name: 'Indigo', value: '#6366f1' },
|
|
19
|
+
{ name: 'Violeta', value: '#8b5cf6' },
|
|
20
|
+
{ name: 'Rosa', value: '#ec4899' },
|
|
21
|
+
{ name: 'Vermelho', value: '#ef4444' },
|
|
22
|
+
{ name: 'Laranja', value: '#f97316' },
|
|
23
|
+
{ name: 'Amber', value: '#f59e0b' },
|
|
24
|
+
{ name: 'Esmeralda', value: '#10b981' },
|
|
25
|
+
{ name: 'Teal', value: '#14b8a6' },
|
|
26
|
+
{ name: 'Ciano', value: '#06b6d4' },
|
|
27
|
+
{ name: 'Cinza', value: '#6b7280' },
|
|
28
|
+
{ name: 'Zinc', value: '#18181b' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export default function ThemeConfig() {
|
|
32
|
+
const [title, setTitle] = useState('HeroAdmin');
|
|
33
|
+
const [slogan, setSlogan] = useState('Painel de Controle Inteligente');
|
|
34
|
+
const [primaryColor, setPrimaryColor] = useState('#3b82f6');
|
|
35
|
+
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
|
36
|
+
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
|
|
37
|
+
const logoRef = useRef<HTMLInputElement>(null);
|
|
38
|
+
const faviconRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
|
|
40
|
+
function handleFileChange(
|
|
41
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
42
|
+
setter: (url: string | null) => void
|
|
43
|
+
) {
|
|
44
|
+
const file = e.target.files?.[0];
|
|
45
|
+
if (file) {
|
|
46
|
+
const reader = new FileReader();
|
|
47
|
+
reader.onloadend = () => setter(reader.result as string);
|
|
48
|
+
reader.readAsDataURL(file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Card className="h-full">
|
|
54
|
+
<CardHeader>
|
|
55
|
+
<div className="flex items-center gap-3">
|
|
56
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-rose-50">
|
|
57
|
+
<Palette className="h-5 w-5 text-rose-600" />
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<CardTitle className="text-base">Aparencia e Tema</CardTitle>
|
|
61
|
+
<CardDescription>
|
|
62
|
+
Personalize a identidade visual do sistema
|
|
63
|
+
</CardDescription>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</CardHeader>
|
|
67
|
+
<CardContent className="space-y-6">
|
|
68
|
+
{/* Preview */}
|
|
69
|
+
<div className="rounded-lg border bg-muted/30 p-4">
|
|
70
|
+
<p className="mb-3 text-xs font-medium text-muted-foreground">
|
|
71
|
+
Pre-visualizacao
|
|
72
|
+
</p>
|
|
73
|
+
<div className="flex items-center gap-3 rounded-lg border bg-background p-3">
|
|
74
|
+
<div
|
|
75
|
+
className="flex h-10 w-10 items-center justify-center rounded-lg"
|
|
76
|
+
style={{ backgroundColor: primaryColor }}
|
|
77
|
+
>
|
|
78
|
+
{logoPreview ? (
|
|
79
|
+
<img
|
|
80
|
+
src={logoPreview}
|
|
81
|
+
alt="Logo"
|
|
82
|
+
className="h-6 w-6 rounded object-contain"
|
|
83
|
+
/>
|
|
84
|
+
) : (
|
|
85
|
+
<span className="text-sm font-bold text-white">
|
|
86
|
+
{title.charAt(0)}
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<span
|
|
92
|
+
className="text-sm font-bold"
|
|
93
|
+
style={{ color: primaryColor }}
|
|
94
|
+
>
|
|
95
|
+
{title || 'Titulo do Sistema'}
|
|
96
|
+
</span>
|
|
97
|
+
<p className="text-xs text-muted-foreground">
|
|
98
|
+
{slogan || 'Slogan do sistema'}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
{faviconPreview && (
|
|
102
|
+
<div className="ml-auto flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
103
|
+
<img
|
|
104
|
+
src={faviconPreview}
|
|
105
|
+
alt="Favicon"
|
|
106
|
+
className="h-4 w-4 object-contain"
|
|
107
|
+
/>
|
|
108
|
+
Favicon
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Logo + Favicon */}
|
|
115
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
116
|
+
<div className="space-y-2">
|
|
117
|
+
<Label>Logo do sistema</Label>
|
|
118
|
+
<div
|
|
119
|
+
onClick={() => logoRef.current?.click()}
|
|
120
|
+
onKeyDown={(e) => {
|
|
121
|
+
if (e.key === 'Enter') logoRef.current?.click();
|
|
122
|
+
}}
|
|
123
|
+
role="button"
|
|
124
|
+
tabIndex={0}
|
|
125
|
+
className="flex h-28 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed transition-colors hover:border-foreground/30 hover:bg-muted/50"
|
|
126
|
+
>
|
|
127
|
+
{logoPreview ? (
|
|
128
|
+
<img
|
|
129
|
+
src={logoPreview}
|
|
130
|
+
alt="Logo"
|
|
131
|
+
className="h-12 w-12 object-contain"
|
|
132
|
+
/>
|
|
133
|
+
) : (
|
|
134
|
+
<>
|
|
135
|
+
<Upload className="h-5 w-5 text-muted-foreground" />
|
|
136
|
+
<span className="text-xs text-muted-foreground">
|
|
137
|
+
Clique para enviar
|
|
138
|
+
</span>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
<input
|
|
143
|
+
ref={logoRef}
|
|
144
|
+
type="file"
|
|
145
|
+
accept="image/*"
|
|
146
|
+
className="hidden"
|
|
147
|
+
onChange={(e) => handleFileChange(e, setLogoPreview)}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
<Label>Favicon</Label>
|
|
152
|
+
<div
|
|
153
|
+
onClick={() => faviconRef.current?.click()}
|
|
154
|
+
onKeyDown={(e) => {
|
|
155
|
+
if (e.key === 'Enter') faviconRef.current?.click();
|
|
156
|
+
}}
|
|
157
|
+
role="button"
|
|
158
|
+
tabIndex={0}
|
|
159
|
+
className="flex h-28 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed transition-colors hover:border-foreground/30 hover:bg-muted/50"
|
|
160
|
+
>
|
|
161
|
+
{faviconPreview ? (
|
|
162
|
+
<img
|
|
163
|
+
src={faviconPreview}
|
|
164
|
+
alt="Favicon"
|
|
165
|
+
className="h-8 w-8 object-contain"
|
|
166
|
+
/>
|
|
167
|
+
) : (
|
|
168
|
+
<>
|
|
169
|
+
<ImageIcon className="h-5 w-5 text-muted-foreground" />
|
|
170
|
+
<span className="text-xs text-muted-foreground">
|
|
171
|
+
16x16 ou 32x32 px
|
|
172
|
+
</span>
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
<input
|
|
177
|
+
ref={faviconRef}
|
|
178
|
+
type="file"
|
|
179
|
+
accept="image/png,image/ico,image/x-icon,image/svg+xml"
|
|
180
|
+
className="hidden"
|
|
181
|
+
onChange={(e) => handleFileChange(e, setFaviconPreview)}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* Title + Slogan */}
|
|
187
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
188
|
+
<div className="space-y-2">
|
|
189
|
+
<Label htmlFor="sys-title">Titulo do sistema</Label>
|
|
190
|
+
<Input
|
|
191
|
+
id="sys-title"
|
|
192
|
+
value={title}
|
|
193
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
194
|
+
placeholder="Meu Sistema"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
<div className="space-y-2">
|
|
198
|
+
<Label htmlFor="sys-slogan">Slogan</Label>
|
|
199
|
+
<Input
|
|
200
|
+
id="sys-slogan"
|
|
201
|
+
value={slogan}
|
|
202
|
+
onChange={(e) => setSlogan(e.target.value)}
|
|
203
|
+
placeholder="Seu slogan aqui"
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Color Picker */}
|
|
209
|
+
<div className="space-y-3">
|
|
210
|
+
<Label>Cor primaria</Label>
|
|
211
|
+
<div className="flex items-center gap-3">
|
|
212
|
+
<div
|
|
213
|
+
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border"
|
|
214
|
+
style={{ backgroundColor: primaryColor }}
|
|
215
|
+
>
|
|
216
|
+
<input
|
|
217
|
+
type="color"
|
|
218
|
+
value={primaryColor}
|
|
219
|
+
onChange={(e) => setPrimaryColor(e.target.value)}
|
|
220
|
+
className="h-full w-full cursor-pointer opacity-0"
|
|
221
|
+
title="Selecionar cor"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<Input
|
|
225
|
+
value={primaryColor}
|
|
226
|
+
onChange={(e) => setPrimaryColor(e.target.value)}
|
|
227
|
+
className="w-28 font-mono text-sm uppercase"
|
|
228
|
+
maxLength={7}
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
<div className="flex flex-wrap gap-2 pt-1">
|
|
232
|
+
{PRESET_COLORS.map((c) => (
|
|
233
|
+
<button
|
|
234
|
+
key={c.value}
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={() => setPrimaryColor(c.value)}
|
|
237
|
+
className={`group relative flex h-7 w-7 items-center justify-center rounded-full border-2 transition-all ${
|
|
238
|
+
primaryColor === c.value
|
|
239
|
+
? 'border-foreground scale-110'
|
|
240
|
+
: 'border-transparent hover:scale-105'
|
|
241
|
+
}`}
|
|
242
|
+
title={c.name}
|
|
243
|
+
>
|
|
244
|
+
<span
|
|
245
|
+
className="h-5 w-5 rounded-full"
|
|
246
|
+
style={{ backgroundColor: c.value }}
|
|
247
|
+
/>
|
|
248
|
+
</button>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Actions */}
|
|
254
|
+
<div className="flex items-center justify-end border-t pt-4">
|
|
255
|
+
<div className="flex gap-2">
|
|
256
|
+
<Button
|
|
257
|
+
variant="outline"
|
|
258
|
+
size="sm"
|
|
259
|
+
onClick={() => {
|
|
260
|
+
setTitle('HeroAdmin');
|
|
261
|
+
setSlogan('Painel de Controle Inteligente');
|
|
262
|
+
setPrimaryColor('#3b82f6');
|
|
263
|
+
setLogoPreview(null);
|
|
264
|
+
setFaviconPreview(null);
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
Restaurar padrao
|
|
268
|
+
</Button>
|
|
269
|
+
<Button size="sm">Salvar</Button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</CardContent>
|
|
273
|
+
</Card>
|
|
274
|
+
);
|
|
275
|
+
}
|