@hed-hog/core 0.0.185 → 0.0.190

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.
Files changed (55) hide show
  1. package/hedhog/frontend/app/account/2fa/page.tsx.ejs +5 -0
  2. package/hedhog/frontend/app/account/accounts/page.tsx.ejs +5 -0
  3. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +356 -0
  4. package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +379 -0
  5. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +184 -0
  6. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +144 -0
  7. package/hedhog/frontend/app/account/components/email-request-dialog.tsx.ejs +96 -0
  8. package/hedhog/frontend/app/account/components/mfa-add-buttons.tsx.ejs +43 -0
  9. package/hedhog/frontend/app/account/components/mfa-method-card.tsx.ejs +115 -0
  10. package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +236 -0
  11. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +209 -0
  12. package/hedhog/frontend/app/account/components/recovery-codes-dialog.tsx.ejs +192 -0
  13. package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +372 -0
  14. package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +337 -0
  15. package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +393 -0
  16. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +332 -0
  17. package/hedhog/frontend/app/account/email/page.tsx.ejs +5 -0
  18. package/hedhog/frontend/app/account/hooks/use-mfa-methods.ts.ejs +27 -0
  19. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +461 -0
  20. package/hedhog/frontend/app/account/layout.tsx.ejs +105 -0
  21. package/hedhog/frontend/app/account/lib/mfa-utils.tsx.ejs +37 -0
  22. package/hedhog/frontend/app/account/page.tsx.ejs +5 -0
  23. package/hedhog/frontend/app/account/password/page.tsx.ejs +5 -0
  24. package/hedhog/frontend/app/account/profile/page.tsx.ejs +5 -0
  25. package/hedhog/frontend/app/account/sessions/page.tsx.ejs +5 -0
  26. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +490 -0
  27. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +62 -0
  28. package/hedhog/frontend/app/configurations/layout.tsx.ejs +316 -0
  29. package/hedhog/frontend/app/configurations/page.tsx.ejs +35 -0
  30. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +351 -0
  31. package/hedhog/frontend/app/dashboard/[slug]/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +62 -0
  33. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +45 -0
  34. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +196 -0
  35. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +63 -0
  36. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +516 -0
  37. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +753 -0
  38. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +516 -0
  39. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +489 -0
  40. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +621 -0
  41. package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -0
  42. package/hedhog/frontend/app/mail/log/page.tsx.ejs +312 -0
  43. package/hedhog/frontend/app/mail/template/page.tsx.ejs +1177 -0
  44. package/hedhog/frontend/app/preferences/page.tsx.ejs +448 -0
  45. package/hedhog/frontend/app/roles/menus.tsx.ejs +504 -0
  46. package/hedhog/frontend/app/roles/page.tsx.ejs +814 -0
  47. package/hedhog/frontend/app/roles/routes.tsx.ejs +397 -0
  48. package/hedhog/frontend/app/roles/users.tsx.ejs +306 -0
  49. package/hedhog/frontend/app/users/active-session.tsx.ejs +159 -0
  50. package/hedhog/frontend/app/users/identifiers.tsx.ejs +279 -0
  51. package/hedhog/frontend/app/users/page.tsx.ejs +1257 -0
  52. package/hedhog/frontend/app/users/permissions.tsx.ejs +155 -0
  53. package/hedhog/frontend/messages/en.json +1080 -0
  54. package/hedhog/frontend/messages/pt.json +1135 -0
  55. package/package.json +4 -4
@@ -0,0 +1,490 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { Checkbox } from '@/components/ui/checkbox';
3
+ import { Input } from '@/components/ui/input';
4
+ import { Label } from '@/components/ui/label';
5
+ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from '@/components/ui/select';
13
+ import { Switch } from '@/components/ui/switch';
14
+ import { revalidateSystemIcon } from '@/lib/revalidate-icon';
15
+ import { cn } from '@/lib/utils';
16
+ import {
17
+ Setting,
18
+ SettingComponentEnum,
19
+ SettingTypeEnum,
20
+ } from '@hed-hog/api-types';
21
+ import { useApp } from '@hed-hog/next-app-provider';
22
+ import { Upload } from 'lucide-react';
23
+ import { useTranslations } from 'next-intl';
24
+ import { useEffect, useRef, useState } from 'react';
25
+
26
+ type SettingFieldProps = {
27
+ setting: Setting;
28
+ className?: string;
29
+ };
30
+
31
+ export const SettingField = ({ setting, className }: SettingFieldProps) => {
32
+ const t = useTranslations('core.Configurations');
33
+ const { showToastHandler, setSettingValue, request } = useApp();
34
+ const component =
35
+ setting.component?.replaceAll('_', '-') || SettingComponentEnum.INPUT_TEXT;
36
+ const [localValue, setLocalValue] = useState(setting.value);
37
+ const debounceTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
38
+ const isTextInputRef = useRef<boolean>(false);
39
+
40
+ const isThemeSetting = (slug: string) => {
41
+ return slug.startsWith('theme-') || slug === 'menu-width';
42
+ };
43
+
44
+ const applyThemeSettings = async () => {
45
+ try {
46
+ const response: any = await request({
47
+ url: '/setting/initial',
48
+ method: 'GET',
49
+ });
50
+
51
+ const newSettings = response.data?.setting || {};
52
+ localStorage.setItem('settings', JSON.stringify(newSettings));
53
+
54
+ const root = document.documentElement;
55
+ const themeMode = newSettings['theme-mode'] || 'system';
56
+ let currentTheme: 'light' | 'dark' = 'light';
57
+
58
+ if (themeMode === 'system') {
59
+ const prefersDark = window.matchMedia(
60
+ '(prefers-color-scheme: dark)'
61
+ ).matches;
62
+ currentTheme = prefersDark ? 'dark' : 'light';
63
+ } else {
64
+ currentTheme = themeMode as 'light' | 'dark';
65
+ }
66
+
67
+ root.classList.remove('light', 'dark');
68
+ root.classList.add(currentTheme);
69
+
70
+ const existingStyle = document.getElementById('theme-custom-styles');
71
+ if (existingStyle) {
72
+ existingStyle.remove();
73
+ }
74
+
75
+ const styleTag = document.createElement('style');
76
+ styleTag.id = 'theme-custom-styles';
77
+ let cssRules = '';
78
+
79
+ const addRule = (varName: string, settingKey: string) => {
80
+ const hexColor = newSettings[settingKey];
81
+ if (
82
+ hexColor &&
83
+ typeof hexColor === 'string' &&
84
+ hexColor.startsWith('#')
85
+ ) {
86
+ cssRules += ` ${varName}: ${hexColor} !important;\n`;
87
+ }
88
+ };
89
+
90
+ if (currentTheme === 'light') {
91
+ cssRules += '@supports (color: lab(0% 0 0)) {\n';
92
+ cssRules += ' html.light, .light, :root.light {\n';
93
+ addRule('--primary', 'theme-primary-light');
94
+ addRule('--bprogress-color', 'theme-primary-light');
95
+ addRule('--primary-foreground', 'theme-primary-foreground-light');
96
+ addRule('--secondary', 'theme-secondary-light');
97
+ addRule('--secondary-foreground', 'theme-secondary-foreground-light');
98
+ addRule('--accent', 'theme-accent-light');
99
+ addRule('--accent-foreground', 'theme-accent-foreground-light');
100
+ addRule('--muted', 'theme-muted-light');
101
+ addRule('--muted-foreground', 'theme-muted-foreground-light');
102
+ addRule('--background', 'theme-background-light');
103
+ addRule('--foreground', 'theme-background-foreground-light');
104
+ addRule('--card', 'theme-card-light');
105
+ addRule('--card-foreground', 'theme-card-foreground-light');
106
+ } else {
107
+ cssRules += '@supports (color: lab(0% 0 0)) {\n';
108
+ cssRules += ' html.dark, .dark, :root.dark {\n';
109
+ addRule('--primary', 'theme-primary-dark');
110
+ addRule('--bprogress-color', 'theme-primary-dark');
111
+ addRule('--primary-foreground', 'theme-primary-foreground-dark');
112
+ addRule('--secondary', 'theme-secondary-dark');
113
+ addRule('--secondary-foreground', 'theme-secondary-foreground-dark');
114
+ addRule('--accent', 'theme-accent-dark');
115
+ addRule('--accent-foreground', 'theme-accent-foreground-dark');
116
+ addRule('--muted', 'theme-muted-dark');
117
+ addRule('--muted-foreground', 'theme-muted-foreground-dark');
118
+ addRule('--background', 'theme-background-dark');
119
+ addRule('--foreground', 'theme-background-foreground-dark');
120
+ addRule('--card', 'theme-card-dark');
121
+ addRule('--card-foreground', 'theme-card-foreground-dark');
122
+ }
123
+
124
+ if (newSettings['theme-radius']) {
125
+ cssRules += ` --radius: ${newSettings['theme-radius']}rem !important;\n`;
126
+ }
127
+ if (newSettings['theme-text-size']) {
128
+ cssRules += ` font-size: ${newSettings['theme-text-size']}rem !important;\n`;
129
+ }
130
+
131
+ cssRules += ' }\n';
132
+ cssRules += '}\n';
133
+
134
+ if (newSettings['theme-font']) {
135
+ cssRules += `:root {\n`;
136
+ cssRules += ` --font-sans: ${newSettings['theme-font']} !important;\n`;
137
+ cssRules += `}\n`;
138
+ cssRules += `html {\n`;
139
+ cssRules += ` font-family: ${newSettings['theme-font']} !important;\n`;
140
+ cssRules += `}\n`;
141
+ }
142
+
143
+ styleTag.textContent = cssRules;
144
+ document.head.appendChild(styleTag);
145
+ } catch (error) {
146
+ console.error('Error applying theme settings:', error);
147
+ }
148
+ };
149
+
150
+ useEffect(() => {
151
+ if (!isTextInputRef.current) return;
152
+
153
+ if (debounceTimerRef.current) {
154
+ clearTimeout(debounceTimerRef.current);
155
+ }
156
+
157
+ debounceTimerRef.current = setTimeout(() => {
158
+ if (localValue !== setting.value) {
159
+ setSettingValue(setting.slug, localValue)
160
+ .then(async () => {
161
+ showToastHandler('success', t('settingUpdated'));
162
+
163
+ if (['icon-url'].includes(setting.slug)) {
164
+ await revalidateSystemIcon();
165
+ showToastHandler('success', t('iconRevalidated'));
166
+ window.location.reload();
167
+ }
168
+
169
+ if (isThemeSetting(setting.slug)) {
170
+ await applyThemeSettings();
171
+ }
172
+ })
173
+ .catch(() => {
174
+ showToastHandler('error', t('settingUpdateFailed'));
175
+ });
176
+ }
177
+ }, 500);
178
+
179
+ return () => {
180
+ if (debounceTimerRef.current) {
181
+ clearTimeout(debounceTimerRef.current);
182
+ }
183
+ };
184
+ }, [localValue]);
185
+
186
+ const handleChangeValue = (newValue: any, immediate = false) => {
187
+ setLocalValue(newValue);
188
+ if (immediate) {
189
+ setSettingValue(setting.slug, newValue)
190
+ .then(async () => {
191
+ showToastHandler('success', t('settingUpdated'));
192
+ if (isThemeSetting(setting.slug)) {
193
+ await applyThemeSettings();
194
+ }
195
+ })
196
+ .catch(() => {
197
+ showToastHandler('error', t('settingUpdateFailed'));
198
+ });
199
+ }
200
+ };
201
+
202
+ const getParsedValue = () => {
203
+ const value = localValue;
204
+ if (value === undefined || value === null) return value;
205
+
206
+ switch (setting.type) {
207
+ case SettingTypeEnum.NUMBER:
208
+ return typeof value === 'number' ? value : parseFloat(value) || 0;
209
+ case SettingTypeEnum.BOOLEAN:
210
+ return typeof value === 'boolean'
211
+ ? value
212
+ : value === 'true' || value === '1';
213
+ case SettingTypeEnum.ARRAY:
214
+ case SettingTypeEnum.JSON:
215
+ try {
216
+ return typeof value === 'string' ? JSON.parse(value) : value;
217
+ } catch {
218
+ return value;
219
+ }
220
+ default:
221
+ return value;
222
+ }
223
+ };
224
+
225
+ const formatValue = (val: any) => {
226
+ switch (setting.type) {
227
+ case SettingTypeEnum.ARRAY:
228
+ case SettingTypeEnum.JSON:
229
+ return typeof val === 'string' ? val : JSON.stringify(val);
230
+ case SettingTypeEnum.BOOLEAN:
231
+ return val ? 'true' : 'false';
232
+ case SettingTypeEnum.NUMBER:
233
+ return val?.toString() || '0';
234
+ default:
235
+ return val;
236
+ }
237
+ };
238
+
239
+ const parsedValue = getParsedValue();
240
+ switch (component) {
241
+ case SettingComponentEnum.INPUT_TEXT:
242
+ isTextInputRef.current = true;
243
+ return (
244
+ <Input
245
+ type="text"
246
+ value={parsedValue || ''}
247
+ onChange={(e) => handleChangeValue(formatValue(e.target.value))}
248
+ className={cn(className, 'bg-background min-w-[200px]')}
249
+ />
250
+ );
251
+
252
+ case SettingComponentEnum.INPUT_NUMBER:
253
+ isTextInputRef.current = true;
254
+ return (
255
+ <Input
256
+ type="number"
257
+ value={parsedValue || 0}
258
+ onChange={(e) =>
259
+ handleChangeValue(formatValue(parseFloat(e.target.value) || 0))
260
+ }
261
+ className={cn(className, 'bg-background min-w-[200px]')}
262
+ />
263
+ );
264
+
265
+ case SettingComponentEnum.INPUT_SECRET:
266
+ isTextInputRef.current = true;
267
+ return (
268
+ <Input
269
+ type="password"
270
+ value={parsedValue || ''}
271
+ onChange={(e) => handleChangeValue(formatValue(e.target.value))}
272
+ className={cn(className, 'bg-background min-w-[200px]')}
273
+ />
274
+ );
275
+
276
+ case SettingComponentEnum.INPUT_FILE:
277
+ isTextInputRef.current = true;
278
+ const fileInputRef = useRef<HTMLInputElement>(null);
279
+ const [isUploading, setIsUploading] = useState(false);
280
+
281
+ const handleFileUpload = async (
282
+ e: React.ChangeEvent<HTMLInputElement>
283
+ ) => {
284
+ const file = e.target.files?.[0];
285
+ if (!file) return;
286
+
287
+ setIsUploading(true);
288
+ try {
289
+ const formData = new FormData();
290
+ formData.append('file', file);
291
+
292
+ const response: any = await request({
293
+ url: '/file',
294
+ method: 'POST',
295
+ data: formData,
296
+ headers: {
297
+ 'Content-Type': 'multipart/form-data',
298
+ },
299
+ });
300
+
301
+ const fileUrl =
302
+ String(process.env.NEXT_PUBLIC_API_BASE_URL) +
303
+ '/file/open/' +
304
+ response.data?.id;
305
+ handleChangeValue(formatValue(fileUrl));
306
+ showToastHandler('success', t('fileUploaded'));
307
+ } catch (error) {
308
+ showToastHandler('error', t('fileUploadFailed'));
309
+ } finally {
310
+ setIsUploading(false);
311
+ if (fileInputRef.current) {
312
+ fileInputRef.current.value = '';
313
+ }
314
+ }
315
+ };
316
+
317
+ return (
318
+ <div className="flex flex-col gap-2 min-w-[200px]">
319
+ {parsedValue && (
320
+ <div>
321
+ <img
322
+ src={parsedValue || ''}
323
+ alt={t('uploadedFileAlt')}
324
+ className="w-[200px]"
325
+ />
326
+ </div>
327
+ )}
328
+ <div className="flex items-center gap-2">
329
+ <input
330
+ ref={fileInputRef}
331
+ type="file"
332
+ onChange={handleFileUpload}
333
+ className="hidden"
334
+ id={`file-${setting.id}`}
335
+ />
336
+ <Button
337
+ type="button"
338
+ variant="outline"
339
+ size="icon"
340
+ onClick={() => fileInputRef.current?.click()}
341
+ disabled={isUploading}
342
+ className="shrink-0"
343
+ >
344
+ <Upload className="h-4 w-4" />
345
+ </Button>
346
+ <Input
347
+ type="text"
348
+ value={parsedValue || ''}
349
+ onChange={(e) => handleChangeValue(formatValue(e.target.value))}
350
+ className="flex-1 bg-background w-[150px]"
351
+ placeholder={t('fileUrlPlaceholder')}
352
+ disabled={isUploading}
353
+ />
354
+ </div>
355
+ </div>
356
+ );
357
+
358
+ case SettingComponentEnum.SWITCH:
359
+ return (
360
+ <Switch
361
+ checked={parsedValue || false}
362
+ onCheckedChange={(checked) =>
363
+ handleChangeValue(formatValue(checked), true)
364
+ }
365
+ className={cn(className)}
366
+ />
367
+ );
368
+
369
+ case SettingComponentEnum.CHECKBOX:
370
+ if (setting.type === SettingTypeEnum.ARRAY) {
371
+ const options = setting.setting_list || [];
372
+ const selectedValues: string[] = Array.isArray(parsedValue)
373
+ ? parsedValue
374
+ : [];
375
+ return (
376
+ <div
377
+ className={cn('flex flex-col space-y-2 min-w-[200px]', className)}
378
+ >
379
+ {options.map((option) => (
380
+ <div key={option.id} className="flex items-center space-x-2">
381
+ <Checkbox
382
+ className="bg-background"
383
+ checked={selectedValues.includes(option.value)}
384
+ onCheckedChange={(checked) => {
385
+ let newValues = [...selectedValues];
386
+ if (checked) {
387
+ newValues.push(option.value);
388
+ } else {
389
+ newValues = newValues.filter(
390
+ (val) => val !== option.value
391
+ );
392
+ }
393
+ handleChangeValue(formatValue(newValues), true);
394
+ }}
395
+ />
396
+ <Label>{option.value}</Label>
397
+ </div>
398
+ ))}
399
+ </div>
400
+ );
401
+ }
402
+ return (
403
+ <Checkbox
404
+ checked={parsedValue || false}
405
+ onCheckedChange={(checked) =>
406
+ handleChangeValue(formatValue(checked), true)
407
+ }
408
+ className={cn(className, 'min-w-[200px]')}
409
+ />
410
+ );
411
+
412
+ case SettingComponentEnum.COMBOBOX:
413
+ const options = setting.setting_list || [];
414
+ return (
415
+ <Select
416
+ value={parsedValue?.toString() || ''}
417
+ onValueChange={(val) => handleChangeValue(formatValue(val), true)}
418
+ >
419
+ <SelectTrigger
420
+ className={cn('w-full bg-background min-w-[200px]', className)}
421
+ >
422
+ <SelectValue placeholder={t('selectOption')} />
423
+ </SelectTrigger>
424
+ <SelectContent>
425
+ {options.map((option) => (
426
+ <SelectItem key={option.id} value={option.value}>
427
+ {option.value}
428
+ </SelectItem>
429
+ ))}
430
+ </SelectContent>
431
+ </Select>
432
+ );
433
+
434
+ case SettingComponentEnum.RADIO:
435
+ const radioOptions = setting.setting_list || [];
436
+ return (
437
+ <RadioGroup
438
+ value={parsedValue?.toString() || ''}
439
+ onValueChange={(val) => handleChangeValue(formatValue(val), true)}
440
+ className={cn(className, 'min-w-[200px]')}
441
+ >
442
+ {radioOptions.map((option) => (
443
+ <div key={option.id} className="flex items-center space-x-2">
444
+ <RadioGroupItem
445
+ value={option.value}
446
+ id={`radio-${option.id}`}
447
+ className="bg-background"
448
+ />
449
+ <Label htmlFor={`radio-${option.id}`}>{option.value}</Label>
450
+ </div>
451
+ ))}
452
+ </RadioGroup>
453
+ );
454
+
455
+ case SettingComponentEnum.COLOR_PICKER:
456
+ isTextInputRef.current = true;
457
+ return (
458
+ <div className="flex items-center gap-2 min-w-[200px] max-w-[200px]">
459
+ <Input
460
+ type="color"
461
+ value={parsedValue || '#000000'}
462
+ onChange={(e) => handleChangeValue(formatValue(e.target.value))}
463
+ className={cn(
464
+ 'h-10 w-10 border-none p-0 cursor-pointer',
465
+ className
466
+ )}
467
+ />
468
+ <Input
469
+ type="text"
470
+ value={parsedValue || '#000000'}
471
+ onChange={(e) => handleChangeValue(formatValue(e.target.value))}
472
+ className="flex-1 bg-background"
473
+ placeholder="#000000"
474
+ maxLength={7}
475
+ minLength={7}
476
+ />
477
+ </div>
478
+ );
479
+
480
+ default:
481
+ return (
482
+ <Input
483
+ type="text"
484
+ value={parsedValue || ''}
485
+ onChange={(e) => handleChangeValue(formatValue(e.target.value))}
486
+ className={cn(className, 'bg-background')}
487
+ />
488
+ );
489
+ }
490
+ };
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { Card, CardContent } from '@/components/ui/card';
4
+ import { PaginatedResult } from '@hed-hog/api-pagination';
5
+ import { Setting } from '@hed-hog/api-types';
6
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
7
+ import { Loader } from 'lucide-react';
8
+ import { useParams } from 'next/navigation';
9
+ import { SettingField } from './components/setting-field';
10
+
11
+ export default function Page() {
12
+ const params = useParams();
13
+ const slug = params?.slug;
14
+
15
+ const { request, currentLocaleCode } = useApp();
16
+ const { data: settings, isLoading } = useQuery<PaginatedResult<Setting>>({
17
+ queryKey: [`setting`, slug, currentLocaleCode],
18
+ queryFn: async () => {
19
+ const response = await request<PaginatedResult<Setting>>({
20
+ url: `/setting/group/${slug}`,
21
+ });
22
+ return response.data;
23
+ },
24
+ });
25
+
26
+ if (!slug) {
27
+ return <></>;
28
+ }
29
+
30
+ return (
31
+ <div className="space-y-6">
32
+ <form className="space-y-4">
33
+ <Card className="border-none bg-accent">
34
+ {isLoading && (
35
+ <CardContent className="flex h-32 w-full items-center justify-center">
36
+ <Loader className="animate-spin text-muted-foreground w-4 h-4" />
37
+ </CardContent>
38
+ )}
39
+ {(settings?.data || []).map((setting: Setting) => (
40
+ <CardContent
41
+ key={setting.id}
42
+ className="flex-col gap-4 border-b last:border-0 border-border pb-4"
43
+ >
44
+ <div className="flex justify-between items-center">
45
+ <div className="flex-col">
46
+ <label className="font-semibold">{setting.name}</label>
47
+ <p className="text-muted-foreground text-sm">
48
+ {setting.description}
49
+ </p>
50
+ </div>
51
+
52
+ <div className="h-full flex items-center">
53
+ <SettingField setting={setting} />
54
+ </div>
55
+ </div>
56
+ </CardContent>
57
+ ))}
58
+ </Card>
59
+ </form>
60
+ </div>
61
+ );
62
+ }