@hed-hog/core 0.0.297 → 0.0.299
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/dist/auth/auth.controller.d.ts +10 -10
- package/dist/auth/auth.service.d.ts +10 -10
- package/dist/dashboard/dashboard/dashboard.controller.d.ts +3 -0
- package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard/dashboard.service.d.ts +3 -0
- package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +12 -0
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component/dashboard-component.controller.js +22 -0
- package/dist/dashboard/dashboard-component/dashboard-component.controller.js.map +1 -1
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +15 -0
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component/dashboard-component.service.js +110 -3
- package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
- package/dist/dashboard/dashboard-component/dto/create.dto.d.ts +1 -0
- package/dist/dashboard/dashboard-component/dto/create.dto.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component/dto/create.dto.js +5 -0
- package/dist/dashboard/dashboard-component/dto/create.dto.js.map +1 -1
- package/dist/dashboard/dashboard-component/dto/update.dto.d.ts +1 -0
- package/dist/dashboard/dashboard-component/dto/update.dto.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component/dto/update.dto.js +5 -0
- package/dist/dashboard/dashboard-component/dto/update.dto.js.map +1 -1
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +1 -0
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +1 -0
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +21 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.module.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.module.js +6 -1
- package/dist/dashboard/dashboard-core/dashboard-core.module.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +180 -2
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +619 -9
- package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
- package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +1 -0
- package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +1 -0
- package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
- package/dist/file/file.controller.d.ts.map +1 -1
- package/dist/file/file.controller.js +16 -0
- package/dist/file/file.controller.js.map +1 -1
- package/dist/file/file.service.d.ts +7 -1
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +38 -1
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/provider/s3.provider.d.ts +1 -0
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +38 -29
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +2 -1
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/user/constants/user.constants.d.ts +1 -0
- package/dist/user/constants/user.constants.d.ts.map +1 -1
- package/dist/user/constants/user.constants.js +2 -1
- package/dist/user/constants/user.constants.js.map +1 -1
- package/dist/user/user.controller.d.ts +10 -10
- package/dist/user/user.service.d.ts +30 -30
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +2 -1
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/dashboard_item.yaml +10 -10
- package/hedhog/data/route.yaml +20 -0
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +212 -34
- package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +3 -0
- package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +136 -23
- package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +266 -85
- package/hedhog/frontend/app/dashboard/components/widgets/core..gitkeep.ejs +11 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.account-security.tsx.ejs +192 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.email-notifications.tsx.ejs +226 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.locale-config.tsx.ejs +168 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.mail-config.tsx.ejs +199 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.oauth-config.tsx.ejs +175 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.profile-card.tsx.ejs +186 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.storage-config.tsx.ejs +196 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.theme-config.tsx.ejs +213 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.user-roles.tsx.ejs +132 -0
- package/hedhog/frontend/app/dashboard/components/widgets/core.user-sessions.tsx.ejs +236 -0
- package/hedhog/frontend/app/dashboard/components/widgets/finance.alerts.tsx.ejs +108 -0
- package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-balance-kpi.tsx.ejs +66 -0
- package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-flow-chart.tsx.ejs +122 -0
- package/hedhog/frontend/app/dashboard/components/widgets/finance.default-kpi.tsx.ejs +63 -0
- package/hedhog/frontend/app/dashboard/components/widgets/finance.payable-30d-kpi.tsx.ejs +73 -0
- package/hedhog/frontend/app/dashboard/components/widgets/finance.receivable-30d-kpi.tsx.ejs +73 -0
- package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-payable.tsx.ejs +123 -0
- package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-receivable.tsx.ejs +118 -0
- package/hedhog/frontend/messages/en.json +93 -0
- package/hedhog/frontend/messages/pt.json +93 -0
- package/hedhog/frontend/public/dashboard-previews/.gitkeep +12 -0
- package/hedhog/frontend/public/dashboard-previews/account-security.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/active-users-card.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/activity-timeline.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/cash-balance-kpi.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/cash-flow-chart.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/default-kpi.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/email-notifications.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/financial-alerts.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/login-history-chart.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/mail-sent-card.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/mail-sent-chart.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/menus-card.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/payable-30d-kpi.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/permissions-card.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/permissions-chart.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/profile-card.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/receivable-30d-kpi.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/routes-card.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/session-activity-chart.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/sessions-today-card.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/stat-access-level.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/stat-actions-today.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/stat-consecutive-days.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/stat-online-time.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/upcoming-payable.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/upcoming-receivable.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/user-growth-chart.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/user-roles.png +0 -0
- package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/account-security.tsx.ejs +33 -29
- package/hedhog/frontend/widgets/active-users-card.tsx.ejs +58 -0
- package/hedhog/frontend/widgets/activity-timeline.tsx.ejs +223 -0
- package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/email-notifications.tsx.ejs +85 -61
- package/hedhog/frontend/widgets/locale-config.tsx.ejs +168 -0
- package/hedhog/frontend/widgets/login-history-chart.tsx.ejs +115 -0
- package/hedhog/frontend/widgets/mail-config.tsx.ejs +199 -0
- package/hedhog/frontend/widgets/mail-sent-card.tsx.ejs +58 -0
- package/hedhog/frontend/widgets/mail-sent-chart.tsx.ejs +149 -0
- package/hedhog/frontend/widgets/menus-card.tsx.ejs +58 -0
- package/hedhog/frontend/widgets/oauth-config.tsx.ejs +175 -0
- package/hedhog/frontend/widgets/permissions-card.tsx.ejs +61 -0
- package/hedhog/frontend/widgets/permissions-chart.tsx.ejs +156 -0
- package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/profile-card.tsx.ejs +3 -3
- package/hedhog/frontend/widgets/routes-card.tsx.ejs +58 -0
- package/hedhog/frontend/widgets/session-activity-chart.tsx.ejs +183 -0
- package/hedhog/frontend/widgets/sessions-today-card.tsx.ejs +62 -0
- package/hedhog/frontend/widgets/stat-access-level.tsx.ejs +57 -0
- package/hedhog/frontend/widgets/stat-actions-today.tsx.ejs +57 -0
- package/hedhog/frontend/widgets/stat-consecutive-days.tsx.ejs +57 -0
- package/hedhog/frontend/widgets/stat-online-time.tsx.ejs +57 -0
- package/hedhog/frontend/widgets/storage-config.tsx.ejs +196 -0
- package/hedhog/frontend/widgets/theme-config.tsx.ejs +213 -0
- package/hedhog/frontend/widgets/user-growth-chart.tsx.ejs +210 -0
- package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-roles.tsx.ejs +12 -14
- package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-sessions.tsx.ejs +1 -1
- package/hedhog/table/dashboard_component.yaml +7 -0
- package/hedhog/table/mail_sent_user.yaml +75 -0
- package/package.json +4 -4
- package/src/dashboard/dashboard-component/dashboard-component.controller.ts +36 -12
- package/src/dashboard/dashboard-component/dashboard-component.service.ts +150 -3
- package/src/dashboard/dashboard-component/dto/create.dto.ts +4 -0
- package/src/dashboard/dashboard-component/dto/update.dto.ts +4 -0
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
- package/src/dashboard/dashboard-core/dashboard-core.module.ts +6 -1
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +874 -8
- package/src/file/file.controller.ts +37 -13
- package/src/file/file.service.ts +47 -5
- package/src/file/provider/s3.provider.ts +39 -29
- package/src/oauth/oauth.service.ts +8 -7
- package/src/user/constants/user.constants.ts +1 -0
- package/src/user/user.service.ts +2 -1
- package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +0 -309
- package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +0 -445
- package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +0 -296
- package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +0 -340
- package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +0 -275
- /package/hedhog/frontend/app/dashboard/components/widgets/{active-users-card.tsx.ejs → core.active-users-card.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{activity-timeline.tsx.ejs → core.activity-timeline.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{login-history-chart.tsx.ejs → core.login-history-chart.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{mail-sent-card.tsx.ejs → core.mail-sent-card.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{mail-sent-chart.tsx.ejs → core.mail-sent-chart.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{menus-card.tsx.ejs → core.menus-card.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{permissions-card.tsx.ejs → core.permissions-card.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{permissions-chart.tsx.ejs → core.permissions-chart.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{routes-card.tsx.ejs → core.routes-card.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{session-activity-chart.tsx.ejs → core.session-activity-chart.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{sessions-today-card.tsx.ejs → core.sessions-today-card.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{stat-access-level.tsx.ejs → core.stat-access-level.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{stat-actions-today.tsx.ejs → core.stat-actions-today.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{stat-consecutive-days.tsx.ejs → core.stat-consecutive-days.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{stat-online-time.tsx.ejs → core.stat-online-time.tsx.ejs} +0 -0
- /package/hedhog/frontend/app/dashboard/components/widgets/{user-growth-chart.tsx.ejs → core.user-growth-chart.tsx.ejs} +0 -0
|
@@ -2,19 +2,19 @@ import { Public, Role } from '@hed-hog/api';
|
|
|
2
2
|
import { getLocaleText, Locale } from '@hed-hog/api-locale';
|
|
3
3
|
import { Pagination } from '@hed-hog/api-pagination';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
Body,
|
|
6
|
+
Controller,
|
|
7
|
+
Delete,
|
|
8
|
+
forwardRef,
|
|
9
|
+
Get,
|
|
10
|
+
Inject,
|
|
11
|
+
Param,
|
|
12
|
+
ParseIntPipe,
|
|
13
|
+
Post,
|
|
14
|
+
Put,
|
|
15
|
+
Res,
|
|
16
|
+
UploadedFile,
|
|
17
|
+
UseInterceptors,
|
|
18
18
|
} from '@nestjs/common';
|
|
19
19
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
20
20
|
import { Response } from 'express';
|
|
@@ -127,6 +127,30 @@ export class FileController {
|
|
|
127
127
|
|
|
128
128
|
try {
|
|
129
129
|
|
|
130
|
+
const numericId = Number(token);
|
|
131
|
+
const isNumericId = Number.isInteger(numericId) && String(numericId) === token;
|
|
132
|
+
|
|
133
|
+
if (isNumericId && numericId > 0) {
|
|
134
|
+
const { stream, filename, mimetype, size } =
|
|
135
|
+
await this.fileService.openById(locale, numericId);
|
|
136
|
+
|
|
137
|
+
const encodedFilename = encodeURIComponent(filename);
|
|
138
|
+
|
|
139
|
+
res.set({
|
|
140
|
+
'Content-Type': mimetype || 'application/octet-stream',
|
|
141
|
+
'Content-Disposition':
|
|
142
|
+
`inline; filename="${filename}"; ` +
|
|
143
|
+
`filename*=UTF-8''${encodedFilename}`,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (typeof size === 'number') {
|
|
147
|
+
res.set('Content-Length', `${size}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
stream.pipe(res);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
const { stream, filename, mimetype, size } =
|
|
131
155
|
await this.fileService.download(locale, token);
|
|
132
156
|
|
package/src/file/file.service.ts
CHANGED
|
@@ -258,11 +258,26 @@ export class FileService implements OnModuleInit {
|
|
|
258
258
|
|
|
259
259
|
const provider = await this.getProvider();
|
|
260
260
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
const providerPath =
|
|
262
|
+
storage === EnumProvider.LOCAL ? file.path : new URL(file.path).pathname;
|
|
263
|
+
|
|
264
|
+
let providerBuffer: Buffer;
|
|
265
|
+
try {
|
|
266
|
+
providerBuffer = await provider.buffer(providerPath);
|
|
267
|
+
} catch (error: any) {
|
|
268
|
+
const missingInStorage =
|
|
269
|
+
error?.name === 'NoSuchKey' ||
|
|
270
|
+
error?.Code === 'NoSuchKey' ||
|
|
271
|
+
error?.$metadata?.httpStatusCode === 404;
|
|
272
|
+
|
|
273
|
+
if (missingInStorage) {
|
|
274
|
+
throw new NotFoundException(`File not found in storage: ${fileId}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const buffer = Buffer.from(providerBuffer);
|
|
266
281
|
|
|
267
282
|
return {
|
|
268
283
|
file,
|
|
@@ -453,6 +468,33 @@ export class FileService implements OnModuleInit {
|
|
|
453
468
|
}
|
|
454
469
|
}
|
|
455
470
|
|
|
471
|
+
async openById(locale: string, fileId: number) {
|
|
472
|
+
const file = await this.prismaService.file.findFirst({
|
|
473
|
+
where: { id: fileId },
|
|
474
|
+
include: {
|
|
475
|
+
file_mimetype: true,
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (!file) {
|
|
480
|
+
throw new NotFoundException(
|
|
481
|
+
getLocaleText('file.download.not_found', locale, `Not found: ${fileId}`),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const provider = await this.getProvider();
|
|
486
|
+
const metadata = await provider.metaData(file.path);
|
|
487
|
+
const stream = await provider.readStream(file.path);
|
|
488
|
+
const size = this.getFileSizeFromMetadata(metadata);
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
stream,
|
|
492
|
+
filename: file?.filename || 'download',
|
|
493
|
+
mimetype: file?.file_mimetype?.name || 'application/octet-stream',
|
|
494
|
+
size,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
456
498
|
async tempURL(filepath: string, expires = 3600) {
|
|
457
499
|
return (await this.getProvider()).tempURL(filepath, expires);
|
|
458
500
|
}
|
|
@@ -21,15 +21,33 @@ export class S3Provider extends AbstractProvider {
|
|
|
21
21
|
this.initValidation();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
private resolveS3Key(filepath: string): string {
|
|
25
|
+
if (!filepath) {
|
|
26
|
+
throw new Error('Invalid filepath for S3');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(filepath);
|
|
31
|
+
const keyFromUrl = url.pathname.split('/').filter(Boolean).join('/');
|
|
32
|
+
if (!keyFromUrl) {
|
|
33
|
+
throw new Error(`Invalid filepath "${filepath}" for S3`);
|
|
34
|
+
}
|
|
35
|
+
return keyFromUrl;
|
|
36
|
+
} catch {
|
|
37
|
+
const keyFromPath = filepath.split('/').filter(Boolean).join('/');
|
|
38
|
+
if (!keyFromPath) {
|
|
39
|
+
throw new Error(`Invalid filepath "${filepath}" for S3`);
|
|
40
|
+
}
|
|
41
|
+
return keyFromPath;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
async buffer(filepath: string): Promise<Buffer> {
|
|
25
46
|
const s3 = await this.getClient();
|
|
26
|
-
const
|
|
27
|
-
if (!url.pathname) {
|
|
28
|
-
throw new Error(`Invalid filepath "${filepath}" for S3`);
|
|
29
|
-
}
|
|
47
|
+
const key = this.resolveS3Key(filepath);
|
|
30
48
|
const command = new GetObjectCommand({
|
|
31
49
|
Bucket: this.setting['storage-s3-bucket'],
|
|
32
|
-
Key:
|
|
50
|
+
Key: key,
|
|
33
51
|
});
|
|
34
52
|
const result = await s3.send(command);
|
|
35
53
|
// @ts-ignore
|
|
@@ -123,37 +141,38 @@ export class S3Provider extends AbstractProvider {
|
|
|
123
141
|
fileContent: string,
|
|
124
142
|
): Promise<any> {
|
|
125
143
|
const s3 = await this.getClient();
|
|
144
|
+
const storedFilename = this.getFilename(filename);
|
|
145
|
+
const key = [destination, storedFilename].join('/');
|
|
126
146
|
const command = new PutObjectCommand({
|
|
127
147
|
Bucket: this.setting['storage-s3-bucket'],
|
|
128
|
-
Key:
|
|
148
|
+
Key: key,
|
|
129
149
|
Body: fileContent,
|
|
130
150
|
});
|
|
131
151
|
await s3.send(command);
|
|
132
152
|
// S3 v3 não retorna Location diretamente, construa a URL:
|
|
133
|
-
return `https://${this.setting['storage-s3-bucket']}.s3.${this.setting['storage-s3-region']}.amazonaws.com/${
|
|
153
|
+
return `https://${this.setting['storage-s3-bucket']}.s3.${this.setting['storage-s3-region']}.amazonaws.com/${key}`;
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
async upload(destination: string, file: MulterFile): Promise<any> {
|
|
137
157
|
const s3 = await this.getClient();
|
|
158
|
+
const storedFilename = this.getFilename(file.originalname);
|
|
159
|
+
const key = [destination, storedFilename].join('/');
|
|
138
160
|
const command = new PutObjectCommand({
|
|
139
161
|
Bucket: this.setting['storage-s3-bucket'],
|
|
140
|
-
Key:
|
|
162
|
+
Key: key,
|
|
141
163
|
Body: file.buffer,
|
|
142
164
|
});
|
|
143
165
|
await s3.send(command);
|
|
144
|
-
return `https://${this.setting['storage-s3-bucket']}.s3.${this.setting['storage-s3-region']}.amazonaws.com/${
|
|
166
|
+
return `https://${this.setting['storage-s3-bucket']}.s3.${this.setting['storage-s3-region']}.amazonaws.com/${key}`;
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
async delete(filepath: string): Promise<any> {
|
|
148
170
|
const s3 = await this.getClient();
|
|
149
|
-
const
|
|
150
|
-
if (!url.pathname) {
|
|
151
|
-
throw new Error(`Invalid filepath "${filepath}" for S3`);
|
|
152
|
-
}
|
|
171
|
+
const key = this.resolveS3Key(filepath);
|
|
153
172
|
await s3.send(
|
|
154
173
|
new DeleteObjectCommand({
|
|
155
174
|
Bucket: this.setting['storage-s3-bucket'],
|
|
156
|
-
Key:
|
|
175
|
+
Key: key,
|
|
157
176
|
}),
|
|
158
177
|
);
|
|
159
178
|
return true;
|
|
@@ -161,13 +180,10 @@ export class S3Provider extends AbstractProvider {
|
|
|
161
180
|
|
|
162
181
|
async readStream(filepath: string): Promise<Readable> {
|
|
163
182
|
const s3 = await this.getClient();
|
|
164
|
-
const
|
|
165
|
-
if (!url.pathname) {
|
|
166
|
-
throw new Error(`Invalid filepath "${filepath}" for S3`);
|
|
167
|
-
}
|
|
183
|
+
const key = this.resolveS3Key(filepath);
|
|
168
184
|
const command = new GetObjectCommand({
|
|
169
185
|
Bucket: this.setting['storage-s3-bucket'],
|
|
170
|
-
Key:
|
|
186
|
+
Key: key,
|
|
171
187
|
});
|
|
172
188
|
const result = await s3.send(command);
|
|
173
189
|
// @ts-ignore
|
|
@@ -176,13 +192,10 @@ export class S3Provider extends AbstractProvider {
|
|
|
176
192
|
|
|
177
193
|
async metaData(filepath: string): Promise<any> {
|
|
178
194
|
const s3 = await this.getClient();
|
|
179
|
-
const
|
|
180
|
-
if (!url.pathname) {
|
|
181
|
-
throw new Error(`Invalid filepath "${filepath}" for S3`);
|
|
182
|
-
}
|
|
195
|
+
const key = this.resolveS3Key(filepath);
|
|
183
196
|
const command = new HeadObjectCommand({
|
|
184
197
|
Bucket: this.setting['storage-s3-bucket'],
|
|
185
|
-
Key:
|
|
198
|
+
Key: key,
|
|
186
199
|
});
|
|
187
200
|
const result = await s3.send(command);
|
|
188
201
|
return result; // returns metadata
|
|
@@ -190,13 +203,10 @@ export class S3Provider extends AbstractProvider {
|
|
|
190
203
|
|
|
191
204
|
async tempURL(filepath: string, expires = 3600): Promise<string> {
|
|
192
205
|
const s3Client = await this.getS3Client();
|
|
193
|
-
const
|
|
194
|
-
if (!url.pathname) {
|
|
195
|
-
throw new Error(`Invalid filepath "${filepath}" for S3`);
|
|
196
|
-
}
|
|
206
|
+
const key = this.resolveS3Key(filepath);
|
|
197
207
|
const command = new GetObjectCommand({
|
|
198
208
|
Bucket: this.setting['storage-s3-bucket'],
|
|
199
|
-
Key:
|
|
209
|
+
Key: key,
|
|
200
210
|
});
|
|
201
211
|
// Use S3Client instance for getSignedUrl
|
|
202
212
|
return await getSignedUrl(s3Client as any, command, { expiresIn: expires });
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { PrismaService, user_account_provider_52222e2ecb_enum } from '@hed-hog/api-prisma';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
BadRequestException,
|
|
4
|
+
ConflictException,
|
|
5
|
+
Inject,
|
|
6
|
+
Injectable,
|
|
7
|
+
NotFoundException,
|
|
8
|
+
forwardRef
|
|
9
9
|
} from '@nestjs/common';
|
|
10
10
|
import { AuthService } from '../auth/auth.service';
|
|
11
11
|
import { FileService } from '../file/file.service';
|
|
@@ -13,6 +13,7 @@ import { MailService } from '../mail/mail.service';
|
|
|
13
13
|
import { SecurityService } from '../security/security.service';
|
|
14
14
|
import { SettingService } from '../setting/setting.service';
|
|
15
15
|
import { TokenService } from '../token/token.service';
|
|
16
|
+
import { USER_AVATAR_UPLOAD_DESTINATION } from '../user/constants/user.constants';
|
|
16
17
|
import { UserService } from '../user/user.service';
|
|
17
18
|
import { OAuthProvider } from './interfaces/OAuthProvider';
|
|
18
19
|
import { FacebookProvider } from './providers/facebook.provider';
|
|
@@ -385,7 +386,7 @@ export class OAuthService {
|
|
|
385
386
|
size: buffer.length,
|
|
386
387
|
} as any;
|
|
387
388
|
|
|
388
|
-
const savedFile = await this.file.upload(
|
|
389
|
+
const savedFile = await this.file.upload(USER_AVATAR_UPLOAD_DESTINATION, file);
|
|
389
390
|
return savedFile.id;
|
|
390
391
|
}
|
|
391
392
|
}
|
package/src/user/user.service.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { ChallengeService } from '../challenge/challenge.service';
|
|
|
12
12
|
import { DeleteDTO } from '../dto/delete.dto';
|
|
13
13
|
import { FileService } from '../file/file.service';
|
|
14
14
|
import { SecurityService } from '../security/security.service';
|
|
15
|
+
import { USER_AVATAR_UPLOAD_DESTINATION } from './constants/user.constants';
|
|
15
16
|
import { CreateWithEmailAndPasswordDTO } from './dto/create-with-email-and-password.dto';
|
|
16
17
|
import { ResetPasswordDTO } from './dto/reset-password.dto';
|
|
17
18
|
import { UpdateDTO } from './dto/update.dto';
|
|
@@ -272,7 +273,7 @@ export class UserService {
|
|
|
272
273
|
async changeAvatar(locale: string, userId: number, avatar: MulterFile) {
|
|
273
274
|
const user = await this.validateUserExists(locale, userId);
|
|
274
275
|
|
|
275
|
-
const newFile = await this.file.upload(
|
|
276
|
+
const newFile = await this.file.upload(USER_AVATAR_UPLOAD_DESTINATION, avatar);
|
|
276
277
|
|
|
277
278
|
await this.deleteOldAvatar(locale, user.photo_id);
|
|
278
279
|
|
|
@@ -1,309 +0,0 @@
|
|
|
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 { Checkbox } from '@/components/ui/checkbox';
|
|
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 { Switch } from '@/components/ui/switch';
|
|
22
|
-
import { Calendar, CheckCircle2, Clock, Globe, Type } from 'lucide-react';
|
|
23
|
-
import { useState } from 'react';
|
|
24
|
-
|
|
25
|
-
interface Language {
|
|
26
|
-
code: string;
|
|
27
|
-
label: string;
|
|
28
|
-
flag: string;
|
|
29
|
-
enabled: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const availableLanguages: Language[] = [
|
|
33
|
-
{ code: 'pt-BR', label: 'Portugues (Brasil)', flag: '🇧🇷', enabled: true },
|
|
34
|
-
{ code: 'en-US', label: 'Ingles (EUA)', flag: '🇺🇸', enabled: true },
|
|
35
|
-
{ code: 'es-ES', label: 'Espanhol (Espanha)', flag: '🇪🇸', enabled: false },
|
|
36
|
-
{ code: 'fr-FR', label: 'Frances (Franca)', flag: '🇫🇷', enabled: false },
|
|
37
|
-
{ code: 'de-DE', label: 'Alemao (Alemanha)', flag: '🇩🇪', enabled: false },
|
|
38
|
-
{ code: 'it-IT', label: 'Italiano (Italia)', flag: '🇮🇹', enabled: false },
|
|
39
|
-
{ code: 'ja-JP', label: 'Japones (Japao)', flag: '🇯🇵', enabled: false },
|
|
40
|
-
{ code: 'zh-CN', label: 'Chines (Simplificado)', flag: '🇨🇳', enabled: false },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const timezones = [
|
|
44
|
-
{ value: 'America/Sao_Paulo', label: 'America/Sao_Paulo (GMT-3)' },
|
|
45
|
-
{ value: 'America/New_York', label: 'America/New_York (GMT-5)' },
|
|
46
|
-
{ value: 'America/Chicago', label: 'America/Chicago (GMT-6)' },
|
|
47
|
-
{ value: 'America/Denver', label: 'America/Denver (GMT-7)' },
|
|
48
|
-
{ value: 'America/Los_Angeles', label: 'America/Los_Angeles (GMT-8)' },
|
|
49
|
-
{ value: 'Europe/London', label: 'Europe/London (GMT+0)' },
|
|
50
|
-
{ value: 'Europe/Paris', label: 'Europe/Paris (GMT+1)' },
|
|
51
|
-
{ value: 'Europe/Berlin', label: 'Europe/Berlin (GMT+1)' },
|
|
52
|
-
{ value: 'Asia/Tokyo', label: 'Asia/Tokyo (GMT+9)' },
|
|
53
|
-
{ value: 'Asia/Shanghai', label: 'Asia/Shanghai (GMT+8)' },
|
|
54
|
-
{ value: 'Australia/Sydney', label: 'Australia/Sydney (GMT+11)' },
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const dateFormats = [
|
|
58
|
-
{ value: 'DD/MM/YYYY', label: 'DD/MM/YYYY', example: '14/02/2026' },
|
|
59
|
-
{ value: 'MM/DD/YYYY', label: 'MM/DD/YYYY', example: '02/14/2026' },
|
|
60
|
-
{ value: 'YYYY-MM-DD', label: 'YYYY-MM-DD', example: '2026-02-14' },
|
|
61
|
-
{ value: 'DD.MM.YYYY', label: 'DD.MM.YYYY', example: '14.02.2026' },
|
|
62
|
-
{ value: 'DD-MM-YYYY', label: 'DD-MM-YYYY', example: '14-02-2026' },
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
const timeFormats = [
|
|
66
|
-
{ value: 'HH:mm', label: '24 horas', example: '14:30' },
|
|
67
|
-
{ value: 'hh:mm A', label: '12 horas', example: '02:30 PM' },
|
|
68
|
-
{ value: 'HH:mm:ss', label: '24 horas com segundos', example: '14:30:45' },
|
|
69
|
-
{
|
|
70
|
-
value: 'hh:mm:ss A',
|
|
71
|
-
label: '12 horas com segundos',
|
|
72
|
-
example: '02:30:45 PM',
|
|
73
|
-
},
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
export default function LocaleConfig() {
|
|
77
|
-
const [languages, setLanguages] = useState<Language[]>(availableLanguages);
|
|
78
|
-
const [defaultLang, setDefaultLang] = useState('pt-BR');
|
|
79
|
-
const [timezone, setTimezone] = useState('America/Sao_Paulo');
|
|
80
|
-
const [dateFormat, setDateFormat] = useState('DD/MM/YYYY');
|
|
81
|
-
const [timeFormat, setTimeFormat] = useState('HH:mm');
|
|
82
|
-
const [autoDetect, setAutoDetect] = useState(true);
|
|
83
|
-
|
|
84
|
-
const enabledLanguages = languages.filter((l) => l.enabled);
|
|
85
|
-
|
|
86
|
-
function toggleLanguage(code: string) {
|
|
87
|
-
setLanguages((prev) =>
|
|
88
|
-
prev.map((l) => {
|
|
89
|
-
if (l.code === code) {
|
|
90
|
-
if (l.enabled && l.code === defaultLang) return l;
|
|
91
|
-
return { ...l, enabled: !l.enabled };
|
|
92
|
-
}
|
|
93
|
-
return l;
|
|
94
|
-
})
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<Card className="h-full">
|
|
100
|
-
<CardHeader>
|
|
101
|
-
<div className="flex items-center justify-between">
|
|
102
|
-
<div className="flex items-center gap-3">
|
|
103
|
-
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-50">
|
|
104
|
-
<Globe className="h-5 w-5 text-indigo-600" />
|
|
105
|
-
</div>
|
|
106
|
-
<div>
|
|
107
|
-
<CardTitle className="text-base">Localizacao</CardTitle>
|
|
108
|
-
<CardDescription>
|
|
109
|
-
Idiomas, fuso horario e formatos regionais
|
|
110
|
-
</CardDescription>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
<Badge variant="secondary" className="bg-indigo-50 text-indigo-700">
|
|
114
|
-
{enabledLanguages.length} idioma
|
|
115
|
-
{enabledLanguages.length !== 1 ? 's' : ''}
|
|
116
|
-
</Badge>
|
|
117
|
-
</div>
|
|
118
|
-
</CardHeader>
|
|
119
|
-
<CardContent className="space-y-6">
|
|
120
|
-
{/* Languages */}
|
|
121
|
-
<div className="space-y-3">
|
|
122
|
-
<div className="flex items-center gap-2">
|
|
123
|
-
<Type className="h-4 w-4 text-muted-foreground" />
|
|
124
|
-
<Label className="text-sm font-medium">Idiomas habilitados</Label>
|
|
125
|
-
</div>
|
|
126
|
-
<div className="grid grid-cols-2 gap-2">
|
|
127
|
-
{languages.map((lang) => (
|
|
128
|
-
<label
|
|
129
|
-
key={lang.code}
|
|
130
|
-
className={`flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors ${
|
|
131
|
-
lang.enabled
|
|
132
|
-
? 'border-foreground/20 bg-foreground/2'
|
|
133
|
-
: 'border-border hover:border-foreground/10'
|
|
134
|
-
}`}
|
|
135
|
-
>
|
|
136
|
-
<Checkbox
|
|
137
|
-
checked={lang.enabled}
|
|
138
|
-
onCheckedChange={() => toggleLanguage(lang.code)}
|
|
139
|
-
disabled={lang.enabled && lang.code === defaultLang}
|
|
140
|
-
/>
|
|
141
|
-
<span className="text-base leading-none">{lang.flag}</span>
|
|
142
|
-
<div className="flex flex-1 flex-col">
|
|
143
|
-
<span className="text-sm font-medium">{lang.label}</span>
|
|
144
|
-
<span className="text-xs text-muted-foreground">
|
|
145
|
-
{lang.code}
|
|
146
|
-
</span>
|
|
147
|
-
</div>
|
|
148
|
-
{lang.code === defaultLang && (
|
|
149
|
-
<Badge variant="outline" className="text-[10px] h-5">
|
|
150
|
-
Padrao
|
|
151
|
-
</Badge>
|
|
152
|
-
)}
|
|
153
|
-
</label>
|
|
154
|
-
))}
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
{/* Default Language */}
|
|
159
|
-
<div className="space-y-2">
|
|
160
|
-
<Label>Idioma padrao</Label>
|
|
161
|
-
<Select value={defaultLang} onValueChange={setDefaultLang}>
|
|
162
|
-
<SelectTrigger>
|
|
163
|
-
<SelectValue />
|
|
164
|
-
</SelectTrigger>
|
|
165
|
-
<SelectContent>
|
|
166
|
-
{enabledLanguages.map((lang) => (
|
|
167
|
-
<SelectItem key={lang.code} value={lang.code}>
|
|
168
|
-
<span className="flex items-center gap-2">
|
|
169
|
-
<span>{lang.flag}</span>
|
|
170
|
-
<span>{lang.label}</span>
|
|
171
|
-
</span>
|
|
172
|
-
</SelectItem>
|
|
173
|
-
))}
|
|
174
|
-
</SelectContent>
|
|
175
|
-
</Select>
|
|
176
|
-
<p className="text-xs text-muted-foreground">
|
|
177
|
-
Idioma exibido quando o idioma do usuario nao esta disponivel.
|
|
178
|
-
</p>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
{/* Timezone */}
|
|
182
|
-
<div className="space-y-2">
|
|
183
|
-
<div className="flex items-center gap-2">
|
|
184
|
-
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
185
|
-
<Label>Fuso horario do sistema</Label>
|
|
186
|
-
</div>
|
|
187
|
-
<Select value={timezone} onValueChange={setTimezone}>
|
|
188
|
-
<SelectTrigger>
|
|
189
|
-
<SelectValue />
|
|
190
|
-
</SelectTrigger>
|
|
191
|
-
<SelectContent>
|
|
192
|
-
{timezones.map((tz) => (
|
|
193
|
-
<SelectItem key={tz.value} value={tz.value}>
|
|
194
|
-
{tz.label}
|
|
195
|
-
</SelectItem>
|
|
196
|
-
))}
|
|
197
|
-
</SelectContent>
|
|
198
|
-
</Select>
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
{/* Date & Time Formats */}
|
|
202
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
203
|
-
<div className="space-y-2">
|
|
204
|
-
<div className="flex items-center gap-2">
|
|
205
|
-
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
206
|
-
<Label>Formato de data</Label>
|
|
207
|
-
</div>
|
|
208
|
-
<Select value={dateFormat} onValueChange={setDateFormat}>
|
|
209
|
-
<SelectTrigger>
|
|
210
|
-
<SelectValue />
|
|
211
|
-
</SelectTrigger>
|
|
212
|
-
<SelectContent>
|
|
213
|
-
{dateFormats.map((df) => (
|
|
214
|
-
<SelectItem key={df.value} value={df.value}>
|
|
215
|
-
<span className="flex items-center gap-2">
|
|
216
|
-
<span className="font-mono text-xs">{df.value}</span>
|
|
217
|
-
<span className="text-muted-foreground">
|
|
218
|
-
{'('}
|
|
219
|
-
{df.example}
|
|
220
|
-
{')'}
|
|
221
|
-
</span>
|
|
222
|
-
</span>
|
|
223
|
-
</SelectItem>
|
|
224
|
-
))}
|
|
225
|
-
</SelectContent>
|
|
226
|
-
</Select>
|
|
227
|
-
</div>
|
|
228
|
-
<div className="space-y-2">
|
|
229
|
-
<div className="flex items-center gap-2">
|
|
230
|
-
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
231
|
-
<Label>Formato de hora</Label>
|
|
232
|
-
</div>
|
|
233
|
-
<Select value={timeFormat} onValueChange={setTimeFormat}>
|
|
234
|
-
<SelectTrigger>
|
|
235
|
-
<SelectValue />
|
|
236
|
-
</SelectTrigger>
|
|
237
|
-
<SelectContent>
|
|
238
|
-
{timeFormats.map((tf) => (
|
|
239
|
-
<SelectItem key={tf.value} value={tf.value}>
|
|
240
|
-
<span className="flex items-center gap-2">
|
|
241
|
-
<span>{tf.label}</span>
|
|
242
|
-
<span className="text-muted-foreground font-mono text-xs">
|
|
243
|
-
{'('}
|
|
244
|
-
{tf.example}
|
|
245
|
-
{')'}
|
|
246
|
-
</span>
|
|
247
|
-
</span>
|
|
248
|
-
</SelectItem>
|
|
249
|
-
))}
|
|
250
|
-
</SelectContent>
|
|
251
|
-
</Select>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{/* Preview */}
|
|
256
|
-
<div className="rounded-lg border bg-muted/50 p-4">
|
|
257
|
-
<p className="mb-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
258
|
-
Pre-visualizacao
|
|
259
|
-
</p>
|
|
260
|
-
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm">
|
|
261
|
-
<span>
|
|
262
|
-
<span className="text-muted-foreground">Data: </span>
|
|
263
|
-
<span className="font-medium font-mono">
|
|
264
|
-
{dateFormats.find((d) => d.value === dateFormat)?.example}
|
|
265
|
-
</span>
|
|
266
|
-
</span>
|
|
267
|
-
<span>
|
|
268
|
-
<span className="text-muted-foreground">Hora: </span>
|
|
269
|
-
<span className="font-medium font-mono">
|
|
270
|
-
{timeFormats.find((t) => t.value === timeFormat)?.example}
|
|
271
|
-
</span>
|
|
272
|
-
</span>
|
|
273
|
-
<span>
|
|
274
|
-
<span className="text-muted-foreground">Fuso: </span>
|
|
275
|
-
<span className="font-medium font-mono">
|
|
276
|
-
{
|
|
277
|
-
timezones
|
|
278
|
-
.find((t) => t.value === timezone)
|
|
279
|
-
?.label.split(' ')[0]
|
|
280
|
-
}
|
|
281
|
-
</span>
|
|
282
|
-
</span>
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
|
|
286
|
-
{/* Actions */}
|
|
287
|
-
<div className="flex items-center justify-between border-t pt-4">
|
|
288
|
-
<div className="flex items-center gap-2">
|
|
289
|
-
<Switch
|
|
290
|
-
id="auto-detect-lang"
|
|
291
|
-
checked={autoDetect}
|
|
292
|
-
onCheckedChange={setAutoDetect}
|
|
293
|
-
/>
|
|
294
|
-
<Label
|
|
295
|
-
htmlFor="auto-detect-lang"
|
|
296
|
-
className="text-sm text-muted-foreground"
|
|
297
|
-
>
|
|
298
|
-
Detectar idioma do navegador
|
|
299
|
-
</Label>
|
|
300
|
-
</div>
|
|
301
|
-
<Button size="sm">
|
|
302
|
-
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" />
|
|
303
|
-
Salvar
|
|
304
|
-
</Button>
|
|
305
|
-
</div>
|
|
306
|
-
</CardContent>
|
|
307
|
-
</Card>
|
|
308
|
-
);
|
|
309
|
-
}
|