diff --git a/apps/frontend/src/plugins/core/langs/en.json b/apps/frontend/src/plugins/core/langs/en.json index 4d2c257ff..3673d0703 100644 --- a/apps/frontend/src/plugins/core/langs/en.json +++ b/apps/frontend/src/plugins/core/langs/en.json @@ -207,6 +207,7 @@ "title": "Change Avatar", "desc": "Show your personality with a new avatar.", "submit": "Change Avatar", + "success": "Your avatar has been changed.", "types": { "upload": "Upload a new avatar", "delete": "Delete avatar" diff --git a/packages/backend/src/core/auth/settings/settings.module.ts b/packages/backend/src/core/auth/settings/settings.module.ts index a54727d70..02889df57 100644 --- a/packages/backend/src/core/auth/settings/settings.module.ts +++ b/packages/backend/src/core/auth/settings/settings.module.ts @@ -2,8 +2,13 @@ import { Module } from '@nestjs/common'; import { DevicesSettingsAuthModule } from './devices/devices.module'; import { FilesSettingsAuthModule } from './files/files.module'; +import { UserSettingsAuthModule } from './user/user.module'; @Module({ - imports: [DevicesSettingsAuthModule, FilesSettingsAuthModule], + imports: [ + DevicesSettingsAuthModule, + FilesSettingsAuthModule, + UserSettingsAuthModule, + ], }) export class SettingsAuthModule {} diff --git a/packages/backend/src/core/auth/settings/user/services/upload_avatar.service.ts b/packages/backend/src/core/auth/settings/user/services/upload_avatar.service.ts new file mode 100644 index 000000000..1e359535b --- /dev/null +++ b/packages/backend/src/core/auth/settings/user/services/upload_avatar.service.ts @@ -0,0 +1,58 @@ +import { core_files_avatars } from '@/database/schema/users'; +import { FilesHelperService } from '@/helpers/files/files-helper.service'; +import { InternalDatabaseService } from '@/utils/database/internal_database.service'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { UploadAvatarUserSettingsAuthBody } from 'vitnode-shared/auth/settings/user.dto'; +import { User } from 'vitnode-shared/user.dto'; + +@Injectable() +export class UploadAvatarUserSettingsAuthService { + constructor( + private readonly databaseService: InternalDatabaseService, + private readonly filesHelper: FilesHelperService, + ) {} + + async uploadAvatar({ + body: { delete_avatar }, + currentUser, + files: { avatar }, + }: { + body: Omit; + currentUser: User; + files: Pick; + }): Promise { + if (!delete_avatar && !avatar) { + throw new BadRequestException('No avatar provided'); + } + + const avatarFromDB = + await this.databaseService.db.query.core_files_avatars.findFirst({ + where: (table, { eq }) => eq(table.user_id, currentUser.id), + }); + + if (avatarFromDB || (delete_avatar && avatarFromDB)) { + await this.filesHelper.delete({ + dir_folder: avatarFromDB.dir_folder, + file_name: avatarFromDB.file_name, + }); + + await this.databaseService.db + .delete(core_files_avatars) + .where(eq(core_files_avatars.user_id, currentUser.id)); + + if (delete_avatar) return; + } + + const file = await this.filesHelper.upload({ + file: avatar, + folder: 'avatars', + plugin_code: 'core', + }); + + await this.databaseService.db.insert(core_files_avatars).values({ + ...file, + user_id: currentUser.id, + }); + } +} diff --git a/packages/backend/src/core/auth/settings/user/user.controller.ts b/packages/backend/src/core/auth/settings/user/user.controller.ts new file mode 100644 index 000000000..f718bae74 --- /dev/null +++ b/packages/backend/src/core/auth/settings/user/user.controller.ts @@ -0,0 +1,51 @@ +import { Controllers } from '@/helpers/controller.decorator'; +import { FilesValidationPipe } from '@/helpers/files/files.pipe'; +import { UploadFilesMethod } from '@/helpers/upload-files.decorator'; +import { CurrentUser } from '@/helpers/user.decorator'; +import { Body, Put, UploadedFiles } from '@nestjs/common'; +import { ApiOkResponse } from '@nestjs/swagger'; +import { UploadAvatarUserSettingsAuthBody } from 'vitnode-shared/auth/settings/user.dto'; +import { User } from 'vitnode-shared/user.dto'; + +import { UploadAvatarUserSettingsAuthService } from './services/upload_avatar.service'; + +@Controllers({ + plugin_name: 'Core', + plugin_code: 'core', + route: 'auth/settings/user', + isProtect: true, +}) +export class UserSettingsAuthController { + constructor( + private readonly uploadAvatarService: UploadAvatarUserSettingsAuthService, + ) {} + + @ApiOkResponse({ + description: 'Upload or delete avatar', + }) + @Put('avatar') + @UploadFilesMethod({ + fields: ['avatar'], + }) + async uploadAvatar( + @UploadedFiles( + new FilesValidationPipe({ + avatar: { + maxSize: 1024 * 1024 * 2, // 2 MB + acceptMimeType: ['image/png', 'image/jpeg', 'image/webp'], + isOptional: true, + maxCount: 1, + }, + }), + ) + files: Pick, + @Body() body: UploadAvatarUserSettingsAuthBody, + @CurrentUser() currentUser: User, + ): Promise { + await this.uploadAvatarService.uploadAvatar({ + body, + files, + currentUser, + }); + } +} diff --git a/packages/backend/src/core/auth/settings/user/user.module.ts b/packages/backend/src/core/auth/settings/user/user.module.ts new file mode 100644 index 000000000..040e3dca8 --- /dev/null +++ b/packages/backend/src/core/auth/settings/user/user.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { UploadAvatarUserSettingsAuthService } from './services/upload_avatar.service'; +import { UserSettingsAuthController } from './user.controller'; + +@Module({ + providers: [UploadAvatarUserSettingsAuthService], + controllers: [UserSettingsAuthController], +}) +export class UserSettingsAuthModule {} diff --git a/packages/backend/src/database/schema/users.ts b/packages/backend/src/database/schema/users.ts index a8a0b4991..0c59f8e6c 100644 --- a/packages/backend/src/database/schema/users.ts +++ b/packages/backend/src/database/schema/users.ts @@ -99,9 +99,12 @@ export const core_files_avatars = pgTable('core_files_avatars', t => ({ file_size: t.integer().notNull(), mimetype: t.varchar({ length: 255 }).notNull(), extension: t.varchar({ length: 32 }).notNull(), - user_id: t.integer().references(() => core_users.id, { - onDelete: 'cascade', - }), + user_id: t + .integer() + .references(() => core_users.id, { + onDelete: 'cascade', + }) + .unique(), })); export const core_files_avatars_relations = relations( diff --git a/packages/backend/src/helpers/user.service.ts b/packages/backend/src/helpers/user.service.ts index e576e66cb..e97a21ba1 100644 --- a/packages/backend/src/helpers/user.service.ts +++ b/packages/backend/src/helpers/user.service.ts @@ -103,6 +103,7 @@ export class UserHelper { language: user.language, name: user.name, name_seo: user.name_seo, + avatar: user.avatar, }; if (!withDangerousData) { diff --git a/packages/frontend/next.config.mjs b/packages/frontend/next.config.mjs index 14c376403..6fd905881 100644 --- a/packages/frontend/next.config.mjs +++ b/packages/frontend/next.config.mjs @@ -35,7 +35,7 @@ const nextConfig = config => { }, experimental: { ...(config.experimental || {}), - reactCompiler: true, + // reactCompiler: true, }, transpilePackages: [ ...transpilePackages, diff --git a/packages/frontend/src/components/editor/toolbar/custom/link/content.tsx b/packages/frontend/src/components/editor/toolbar/custom/link/content.tsx index 782f28cae..861f6a969 100644 --- a/packages/frontend/src/components/editor/toolbar/custom/link/content.tsx +++ b/packages/frontend/src/components/editor/toolbar/custom/link/content.tsx @@ -10,7 +10,7 @@ import { Input } from '@/components/ui/input'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslations } from 'next-intl'; import { useForm } from 'react-hook-form'; -import * as z from 'zod'; +import { z } from 'zod'; import { useEditorState } from '../../../hooks/use-editor-state'; diff --git a/packages/frontend/src/components/form/auto-form.tsx b/packages/frontend/src/components/form/auto-form.tsx index 8a971e01c..67536503c 100644 --- a/packages/frontend/src/components/form/auto-form.tsx +++ b/packages/frontend/src/components/form/auto-form.tsx @@ -10,7 +10,7 @@ import { useForm, UseFormReturn, } from 'react-hook-form'; -import * as z from 'zod'; +import { z } from 'zod'; import { Button } from '../ui/button'; import { useDialog } from '../ui/dialog'; diff --git a/packages/frontend/src/components/form/fields/combobox.tsx b/packages/frontend/src/components/form/fields/combobox.tsx index d88b63a24..265dedc33 100644 --- a/packages/frontend/src/components/form/fields/combobox.tsx +++ b/packages/frontend/src/components/form/fields/combobox.tsx @@ -20,7 +20,7 @@ import { Check } from 'lucide-react'; import { useTranslations } from 'next-intl'; import React from 'react'; import { StringLanguage } from 'vitnode-shared/string-language.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { getBaseSchema } from '../utils'; import { AutoFormLabel } from './common/label'; diff --git a/packages/frontend/src/components/form/fields/radio-group.tsx b/packages/frontend/src/components/form/fields/radio-group.tsx index b25d18521..756fd1a70 100644 --- a/packages/frontend/src/components/form/fields/radio-group.tsx +++ b/packages/frontend/src/components/form/fields/radio-group.tsx @@ -1,7 +1,7 @@ import { AutoFormComponentProps } from '@/components/form/auto-form'; import { FormControl, FormItem, FormLabel } from '@/components/ui/form'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; -import * as z from 'zod'; +import { z } from 'zod'; import { getBaseSchema } from '../utils'; import { AutoFormLabel } from './common/label'; diff --git a/packages/frontend/src/components/form/fields/select.tsx b/packages/frontend/src/components/form/fields/select.tsx index 751b6d377..2b5e1dd39 100644 --- a/packages/frontend/src/components/form/fields/select.tsx +++ b/packages/frontend/src/components/form/fields/select.tsx @@ -8,7 +8,7 @@ import { SelectValue, } from '@/components/ui/select'; import { useTranslations } from 'next-intl'; -import * as z from 'zod'; +import { z } from 'zod'; import { getBaseSchema } from '../utils'; import { AutoFormLabel } from './common/label'; diff --git a/packages/frontend/src/components/form/fields/utils/item.tsx b/packages/frontend/src/components/form/fields/utils/item.tsx index 40d2c1f5d..fac788452 100644 --- a/packages/frontend/src/components/form/fields/utils/item.tsx +++ b/packages/frontend/src/components/form/fields/utils/item.tsx @@ -1,7 +1,7 @@ import { FormField, FormMessage } from '@/components/ui/form'; import React from 'react'; import { Control, FieldPath, FieldValues, UseFormWatch } from 'react-hook-form'; -import * as z from 'zod'; +import { z } from 'zod'; import { AutoFormComponentProps } from '../../auto-form'; import resolveDependencies, { diff --git a/packages/frontend/src/components/form/utils.ts b/packages/frontend/src/components/form/utils.ts index dde7ecf95..e5d2bd54c 100644 --- a/packages/frontend/src/components/form/utils.ts +++ b/packages/frontend/src/components/form/utils.ts @@ -1,5 +1,5 @@ import { DefaultValues, FieldValues, UseFormWatch } from 'react-hook-form'; -import * as z from 'zod'; +import { z } from 'zod'; import { DependencyType } from './auto-form'; diff --git a/packages/frontend/src/helpers/zod.ts b/packages/frontend/src/helpers/zod.ts index 442505d61..e16aae222 100644 --- a/packages/frontend/src/helpers/zod.ts +++ b/packages/frontend/src/helpers/zod.ts @@ -1,4 +1,4 @@ -import * as z from 'zod'; +import { z } from 'zod'; export const zodLanguageInput = z.array( z.object({ diff --git a/packages/frontend/src/views/admin/views/core/langs/create-edit/hooks/use-create-edit-lang-admin.ts b/packages/frontend/src/views/admin/views/core/langs/create-edit/hooks/use-create-edit-lang-admin.ts index 3bab441c0..ad7095746 100644 --- a/packages/frontend/src/views/admin/views/core/langs/create-edit/hooks/use-create-edit-lang-admin.ts +++ b/packages/frontend/src/views/admin/views/core/langs/create-edit/hooks/use-create-edit-lang-admin.ts @@ -2,7 +2,7 @@ import { useDialog } from '@/components/ui/dialog'; import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { CreateEditLangAdmin } from '../create-edit'; import { locales } from '../locales'; diff --git a/packages/frontend/src/views/admin/views/core/langs/table/actions/delete/hooks/use-delete-lang-admin.ts b/packages/frontend/src/views/admin/views/core/langs/table/actions/delete/hooks/use-delete-lang-admin.ts index d211c3347..c8fb3ec2f 100644 --- a/packages/frontend/src/views/admin/views/core/langs/table/actions/delete/hooks/use-delete-lang-admin.ts +++ b/packages/frontend/src/views/admin/views/core/langs/table/actions/delete/hooks/use-delete-lang-admin.ts @@ -2,7 +2,7 @@ import { useAlertDialog } from '@/components/ui/alert-dialog'; import { usePathname, useRouter } from '@/navigation'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { ContentDeleteActionsTableLangsCoreAdmin } from '../content'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/plugins/actions/create/hooks/use-create-edit-plugin-admin.ts b/packages/frontend/src/views/admin/views/core/plugins/actions/create/hooks/use-create-edit-plugin-admin.ts index a7a832f61..78bf7e2c4 100644 --- a/packages/frontend/src/views/admin/views/core/plugins/actions/create/hooks/use-create-edit-plugin-admin.ts +++ b/packages/frontend/src/views/admin/views/core/plugins/actions/create/hooks/use-create-edit-plugin-admin.ts @@ -4,7 +4,7 @@ import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; import { ShowPluginAdmin } from 'vitnode-shared/admin/plugins.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationCreateApi } from './mutation-create-api'; import { mutationEditApi } from './mutation-edit-api'; diff --git a/packages/frontend/src/views/admin/views/core/plugins/dev/nav/create-edit/hooks/use-create-nav-plugin-admin.ts b/packages/frontend/src/views/admin/views/core/plugins/dev/nav/create-edit/hooks/use-create-nav-plugin-admin.ts index a330a8d2f..f24b0c309 100644 --- a/packages/frontend/src/views/admin/views/core/plugins/dev/nav/create-edit/hooks/use-create-nav-plugin-admin.ts +++ b/packages/frontend/src/views/admin/views/core/plugins/dev/nav/create-edit/hooks/use-create-nav-plugin-admin.ts @@ -3,7 +3,7 @@ import { zodTag } from '@/helpers/zod'; import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { useDevPluginAdmin } from '../../../hooks/use-dev-plugin'; import { CreateEditNavDevPluginAdmin } from '../create-edit'; diff --git a/packages/frontend/src/views/admin/views/core/plugins/dev/permissions-admin/create-edit/hooks/use-create-edit-permission-admin-plugin-admin.ts b/packages/frontend/src/views/admin/views/core/plugins/dev/permissions-admin/create-edit/hooks/use-create-edit-permission-admin-plugin-admin.ts index 950c2ce17..43e4f6440 100644 --- a/packages/frontend/src/views/admin/views/core/plugins/dev/permissions-admin/create-edit/hooks/use-create-edit-permission-admin-plugin-admin.ts +++ b/packages/frontend/src/views/admin/views/core/plugins/dev/permissions-admin/create-edit/hooks/use-create-edit-permission-admin-plugin-admin.ts @@ -2,7 +2,7 @@ import { useDialog } from '@/components/ui/dialog'; import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { useDevPluginAdmin } from '../../../hooks/use-dev-plugin'; import { PermissionsAdminWithI18n } from '../../permissions-admin'; diff --git a/packages/frontend/src/views/admin/views/core/security/spam/captcha/hooks/use-captcha-security-admin.ts b/packages/frontend/src/views/admin/views/core/security/spam/captcha/hooks/use-captcha-security-admin.ts index 81f6475a4..539194e97 100644 --- a/packages/frontend/src/views/admin/views/core/security/spam/captcha/hooks/use-captcha-security-admin.ts +++ b/packages/frontend/src/views/admin/views/core/security/spam/captcha/hooks/use-captcha-security-admin.ts @@ -2,7 +2,7 @@ import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; import { CaptchaTypeEnum } from 'vitnode-shared/utils/global'; -import * as z from 'zod'; +import { z } from 'zod'; import { ContentCaptchaSpamSecurityAdmin } from '../content'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/settings/authorization/hooks/use-authorization-settings-form-admin.ts b/packages/frontend/src/views/admin/views/core/settings/authorization/hooks/use-authorization-settings-form-admin.ts index 388a50421..ca85d7119 100644 --- a/packages/frontend/src/views/admin/views/core/settings/authorization/hooks/use-authorization-settings-form-admin.ts +++ b/packages/frontend/src/views/admin/views/core/settings/authorization/hooks/use-authorization-settings-form-admin.ts @@ -1,7 +1,7 @@ import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { ShowAuthSettingsAdminObj } from 'vitnode-shared/admin/settings/auth.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/settings/authorization/methods/create-edit/hooks/use-create-method-auth-admin-api.ts b/packages/frontend/src/views/admin/views/core/settings/authorization/methods/create-edit/hooks/use-create-method-auth-admin-api.ts index 39ee80228..38455a9ba 100644 --- a/packages/frontend/src/views/admin/views/core/settings/authorization/methods/create-edit/hooks/use-create-method-auth-admin-api.ts +++ b/packages/frontend/src/views/admin/views/core/settings/authorization/methods/create-edit/hooks/use-create-method-auth-admin-api.ts @@ -2,7 +2,7 @@ import { useDialog } from '@/components/ui/dialog'; import { useTranslations } from 'next-intl'; import React from 'react'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { ContentCreateEditMethodsAuthSettingsAdmin } from '../content'; import { createMutationApi } from './create-mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/settings/email/actions/testing/hooks/use-testing-email-admin.tsx b/packages/frontend/src/views/admin/views/core/settings/email/actions/testing/hooks/use-testing-email-admin.tsx index 151b19a45..2b70d2275 100644 --- a/packages/frontend/src/views/admin/views/core/settings/email/actions/testing/hooks/use-testing-email-admin.tsx +++ b/packages/frontend/src/views/admin/views/core/settings/email/actions/testing/hooks/use-testing-email-admin.tsx @@ -1,7 +1,7 @@ import { useDialog } from '@/components/ui/dialog'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/settings/email/hooks/use-email-settings-form-admin.ts b/packages/frontend/src/views/admin/views/core/settings/email/hooks/use-email-settings-form-admin.ts index 7eebbca22..b23884ac5 100644 --- a/packages/frontend/src/views/admin/views/core/settings/email/hooks/use-email-settings-form-admin.ts +++ b/packages/frontend/src/views/admin/views/core/settings/email/hooks/use-email-settings-form-admin.ts @@ -8,7 +8,7 @@ import { EditEmailSettingsAdminBody, ShowEmailSettingsAdminObj, } from 'vitnode-shared/admin/settings/email.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { ContentEmailSettingsAdmin } from '../content'; import { revalidateApi } from './revalidate-api'; diff --git a/packages/frontend/src/views/admin/views/core/settings/legal/create_edit/hooks/use-create-edit-legal-admin.ts b/packages/frontend/src/views/admin/views/core/settings/legal/create_edit/hooks/use-create-edit-legal-admin.ts index 903bb57f7..4c3a1a8ab 100644 --- a/packages/frontend/src/views/admin/views/core/settings/legal/create_edit/hooks/use-create-edit-legal-admin.ts +++ b/packages/frontend/src/views/admin/views/core/settings/legal/create_edit/hooks/use-create-edit-legal-admin.ts @@ -6,7 +6,7 @@ import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; import { CreateLegalSettingsAdminBody } from 'vitnode-shared/admin/settings/legal.dto'; import { LegalsObj } from 'vitnode-shared/legal.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { createMutationApi } from './create-mutation-api'; import { editMutationApi } from './edit-mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/settings/main/hooks/use-settings-core-admin.ts b/packages/frontend/src/views/admin/views/core/settings/main/hooks/use-settings-core-admin.ts index 1ccf9457c..c6dc154c6 100644 --- a/packages/frontend/src/views/admin/views/core/settings/main/hooks/use-settings-core-admin.ts +++ b/packages/frontend/src/views/admin/views/core/settings/main/hooks/use-settings-core-admin.ts @@ -2,7 +2,7 @@ import { zodLanguageInput } from '@/helpers/zod'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { ShowMiddlewareObj } from 'vitnode-shared/middleware.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/settings/metadata/hooks/use-metadata-settings-admin-api.ts b/packages/frontend/src/views/admin/views/core/settings/metadata/hooks/use-metadata-settings-admin-api.ts index df94f80b3..10a702c3e 100644 --- a/packages/frontend/src/views/admin/views/core/settings/metadata/hooks/use-metadata-settings-admin-api.ts +++ b/packages/frontend/src/views/admin/views/core/settings/metadata/hooks/use-metadata-settings-admin-api.ts @@ -9,7 +9,7 @@ import { ShowMetadataAdminObj, } from 'vitnode-shared/admin/settings/metadata.dto'; import { ManifestDisplay } from 'vitnode-shared/admin/settings/metadata.enum'; -import * as z from 'zod'; +import { z } from 'zod'; import { revalidateAllApi } from '../../../diagnostic/actions/clear_cache/hooks/revalidate-all-api'; import { ContentMetadataSettingsAdmin } from '../content'; diff --git a/packages/frontend/src/views/admin/views/core/styles/editor/hooks/use-editor-admin.ts b/packages/frontend/src/views/admin/views/core/styles/editor/hooks/use-editor-admin.ts index 025c2dbeb..bb42345b4 100644 --- a/packages/frontend/src/views/admin/views/core/styles/editor/hooks/use-editor-admin.ts +++ b/packages/frontend/src/views/admin/views/core/styles/editor/hooks/use-editor-admin.ts @@ -3,7 +3,7 @@ import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; import { ShowMiddlewareObj } from 'vitnode-shared/middleware.dto'; import { AllowTypeFilesEnum } from 'vitnode-shared/utils/global'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/styles/nav/create-edit/hooks/use-create-edit-nav-admin.ts b/packages/frontend/src/views/admin/views/core/styles/nav/create-edit/hooks/use-create-edit-nav-admin.ts index 0daf19f34..3f1f7e657 100644 --- a/packages/frontend/src/views/admin/views/core/styles/nav/create-edit/hooks/use-create-edit-nav-admin.ts +++ b/packages/frontend/src/views/admin/views/core/styles/nav/create-edit/hooks/use-create-edit-nav-admin.ts @@ -3,7 +3,7 @@ import { zodLanguageInput } from '@/helpers/zod'; import { useTextLang } from '@/hooks/use-text-lang'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { ContentCreateEditNavAdmin } from '../create-edit'; import { createMutationApi } from './create-mutation-api'; diff --git a/packages/frontend/src/views/admin/views/core/styles/theme-editor/hooks/use-theme-editor.ts b/packages/frontend/src/views/admin/views/core/styles/theme-editor/hooks/use-theme-editor.ts index 45ffe8f88..812d5d2ea 100644 --- a/packages/frontend/src/views/admin/views/core/styles/theme-editor/hooks/use-theme-editor.ts +++ b/packages/frontend/src/views/admin/views/core/styles/theme-editor/hooks/use-theme-editor.ts @@ -2,7 +2,7 @@ import { FilesInputValue } from '@/components/ui/file-input'; import { zodFile } from '@/helpers/zod'; import React from 'react'; import { UseFormReturn } from 'react-hook-form'; -import * as z from 'zod'; +import { z } from 'zod'; export enum ThemeEditorIds { dark = 'vitnode_logo_dark', diff --git a/packages/frontend/src/views/admin/views/core/styles/theme-editor/wrapper.tsx b/packages/frontend/src/views/admin/views/core/styles/theme-editor/wrapper.tsx index eb9828de6..f63581627 100644 --- a/packages/frontend/src/views/admin/views/core/styles/theme-editor/wrapper.tsx +++ b/packages/frontend/src/views/admin/views/core/styles/theme-editor/wrapper.tsx @@ -13,7 +13,7 @@ import { EditThemeEditorStylesAdminBody, EditThemeEditorStylesAdminObj, } from 'vitnode-shared/admin/styles/theme-editor.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { revalidateAllApi } from '../../diagnostic/actions/clear_cache/hooks/revalidate-all-api'; import { diff --git a/packages/frontend/src/views/admin/views/members/create/hooks/use-create-user-admin.ts b/packages/frontend/src/views/admin/views/members/create/hooks/use-create-user-admin.ts index dbd1095c1..f62a3630d 100644 --- a/packages/frontend/src/views/admin/views/members/create/hooks/use-create-user-admin.ts +++ b/packages/frontend/src/views/admin/views/members/create/hooks/use-create-user-admin.ts @@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl'; import React from 'react'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/members/groups/create-edit-form/hooks/use-create-edit-form-groups-members-admin.ts b/packages/frontend/src/views/admin/views/members/groups/create-edit-form/hooks/use-create-edit-form-groups-members-admin.ts index 6ff681eef..b6708f690 100644 --- a/packages/frontend/src/views/admin/views/members/groups/create-edit-form/hooks/use-create-edit-form-groups-members-admin.ts +++ b/packages/frontend/src/views/admin/views/members/groups/create-edit-form/hooks/use-create-edit-form-groups-members-admin.ts @@ -5,7 +5,7 @@ import { usePathname, useRouter } from '@/navigation'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { CreateGroupsMembersAdminBody } from 'vitnode-shared/admin/members/groups.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { CreateEditFormGroupsMembersAdmin } from '../create-edit-form-groups-members-admin'; import { mutationCreateApi } from './mutation-create-api'; diff --git a/packages/frontend/src/views/admin/views/members/groups/table/actions/delete/hooks/use-delete-group-admin.ts b/packages/frontend/src/views/admin/views/members/groups/table/actions/delete/hooks/use-delete-group-admin.ts index 8a776ad45..8bcd11d8c 100644 --- a/packages/frontend/src/views/admin/views/members/groups/table/actions/delete/hooks/use-delete-group-admin.ts +++ b/packages/frontend/src/views/admin/views/members/groups/table/actions/delete/hooks/use-delete-group-admin.ts @@ -4,7 +4,7 @@ import { usePathname, useRouter } from '@/navigation'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { GroupsMembersAdminObj } from 'vitnode-shared/admin/members/groups.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/members/staff/admin/create-edit-form/hooks/use-form.ts b/packages/frontend/src/views/admin/views/members/staff/admin/create-edit-form/hooks/use-form.ts index 3ffa64c09..82ceb4278 100644 --- a/packages/frontend/src/views/admin/views/members/staff/admin/create-edit-form/hooks/use-form.ts +++ b/packages/frontend/src/views/admin/views/members/staff/admin/create-edit-form/hooks/use-form.ts @@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; import { AdminStaffMembersAdminObj } from 'vitnode-shared/admin/members/staff/admin.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { createMutationApi, editMutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/admin/views/members/user/actions/edit/edit.tsx b/packages/frontend/src/views/admin/views/members/user/actions/edit/edit.tsx index 675b6ba94..296180f24 100644 --- a/packages/frontend/src/views/admin/views/members/user/actions/edit/edit.tsx +++ b/packages/frontend/src/views/admin/views/members/user/actions/edit/edit.tsx @@ -11,7 +11,7 @@ import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; import { UserMembersAdmin } from 'vitnode-shared/admin/members/users.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/theme/views/auth/sign/in/hooks/use-sign-in-admin-view.ts b/packages/frontend/src/views/theme/views/auth/sign/in/hooks/use-sign-in-admin-view.ts index f7a2819a8..cb16d8a1a 100644 --- a/packages/frontend/src/views/theme/views/auth/sign/in/hooks/use-sign-in-admin-view.ts +++ b/packages/frontend/src/views/theme/views/auth/sign/in/hooks/use-sign-in-admin-view.ts @@ -1,7 +1,7 @@ import { useTranslations } from 'next-intl'; import React from 'react'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/theme/views/auth/sign/in/hooks/use-sign-in-view.ts b/packages/frontend/src/views/theme/views/auth/sign/in/hooks/use-sign-in-view.ts index 871ece197..1aef881ab 100644 --- a/packages/frontend/src/views/theme/views/auth/sign/in/hooks/use-sign-in-view.ts +++ b/packages/frontend/src/views/theme/views/auth/sign/in/hooks/use-sign-in-view.ts @@ -1,7 +1,7 @@ import { useTranslations } from 'next-intl'; import React from 'react'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/theme/views/auth/sign/sso/callback/name-form.tsx b/packages/frontend/src/views/theme/views/auth/sign/sso/callback/name-form.tsx index 22932430b..f44133cb9 100644 --- a/packages/frontend/src/views/theme/views/auth/sign/sso/callback/name-form.tsx +++ b/packages/frontend/src/views/theme/views/auth/sign/sso/callback/name-form.tsx @@ -8,7 +8,7 @@ import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; import { SSOCallbackAuthObj } from 'vitnode-shared/auth/sso.dto'; -import * as z from 'zod'; +import { z } from 'zod'; import { nameRegex } from '../../up/hooks/use-sign-up-view'; import { mutationApi } from './hooks/mutation-api'; diff --git a/packages/frontend/src/views/theme/views/auth/sign/up/hooks/use-sign-up-view.ts b/packages/frontend/src/views/theme/views/auth/sign/up/hooks/use-sign-up-view.ts index 3f5570cc0..10e8b8b84 100644 --- a/packages/frontend/src/views/theme/views/auth/sign/up/hooks/use-sign-up-view.ts +++ b/packages/frontend/src/views/theme/views/auth/sign/up/hooks/use-sign-up-view.ts @@ -3,7 +3,7 @@ import { useSignUp } from '@/views/theme/views/auth/sign/up/use-sign-up'; import { useTranslations } from 'next-intl'; import { UseFormReturn } from 'react-hook-form'; import { toast } from 'sonner'; -import * as z from 'zod'; +import { z } from 'zod'; import { mutationApi } from './mutation-api'; diff --git a/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/content.tsx b/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/content.tsx index b2a89c868..4b79df5d5 100644 --- a/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/content.tsx +++ b/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/content.tsx @@ -9,7 +9,8 @@ import { CopperChangeAvatar } from './cropper/cropper'; import { useChangeAvatar } from './hooks/use-change-avatar'; export const ContentChangeAvatar = () => { - const { formSchema, cropperRef } = useChangeAvatar(); + const { formSchema, cropperRef, onSubmit, setValues, values } = + useChangeAvatar(); const { user } = useSession(); const t = useTranslations('core.settings.overview.change_avatar'); @@ -30,6 +31,12 @@ export const ContentChangeAvatar = () => { targetField: 'type', when: () => !user.avatar, }, + { + sourceField: 'file', + type: DependencyType.HIDES, + targetField: 'file', + when: (file: string) => !!file, + }, ]} fields={[ { @@ -50,25 +57,25 @@ export const ContentChangeAvatar = () => { }, { id: 'file', - component: props => - !props.field.value ? ( - - ) : ( - - ), + component: props => ( + + ), }, ]} formSchema={formSchema} + onSubmit={onSubmit} + onValuesChange={setValues} submitButton={props => } - /> + > + {values.file && values.file instanceof File && ( + + )} + ); }; diff --git a/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/cropper/cropper.tsx b/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/cropper/cropper.tsx index 531cd6e90..995419bfb 100644 --- a/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/cropper/cropper.tsx +++ b/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/cropper/cropper.tsx @@ -14,7 +14,6 @@ export const CopperChangeAvatar = ({ aspectRatio={1} autoCropArea={1} background={false} - checkOrientation={false} minCropBoxHeight={100} minCropBoxWidth={100} ref={cropperRef} diff --git a/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/hooks/use-change-avatar.ts b/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/hooks/use-change-avatar.ts index 62d27579c..e718b8da7 100644 --- a/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/hooks/use-change-avatar.ts +++ b/packages/frontend/src/views/theme/views/settings/views/overview/change-avatar/hooks/use-change-avatar.ts @@ -1,9 +1,20 @@ +import { fetcherClient } from '@/api/fetcher-client'; +import { useDialog } from '@/components/ui/dialog'; import { zodFile } from '@/helpers/zod'; +import { useSession } from '@/hooks/use-session'; +import { revalidateAllApi } from '@/views/admin/views/core/diagnostic/actions/clear_cache/hooks/revalidate-all-api'; +import { useTranslations } from 'next-intl'; import React from 'react'; import { ReactCropperElement } from 'react-cropper'; +import { toast } from 'sonner'; +import { UploadAvatarUserSettingsAuthBody } from 'vitnode-shared/auth/settings/user.dto'; import { z } from 'zod'; export const useChangeAvatar = () => { + const t = useTranslations('core.settings.overview.change_avatar'); + const tErrors = useTranslations('core.global.errors'); + const { setOpen } = useDialog(); + const { user } = useSession(); const cropperRef = React.useRef(null); const formSchema = z .object({ @@ -17,6 +28,45 @@ export const useChangeAvatar = () => { return true; }); + const [values, setValues] = React.useState< + Partial> + >({}); - return { formSchema, cropperRef }; + const onSubmit = async (values: z.infer) => { + if (!user) return; + + const formData = new FormData(); + if (values.type === 'upload') { + const cropper = cropperRef.current?.cropper; + if (!cropper) return; + + const blob = await fetch(cropper.getCroppedCanvas().toDataURL()).then( + async res => res.blob(), + ); + const file = new File([blob], `${user.id}.webp`, { + type: blob.type, + }); + + formData.append('avatar', file); + } else { + formData.append('delete_avatar', 'true'); + } + + try { + await fetcherClient({ + method: 'PUT', + url: '/core/auth/settings/user/avatar', + body: formData, + }); + await revalidateAllApi(); + toast.success(t('success')); + setOpen?.(false); + } catch (_) { + toast.error(tErrors('title'), { + description: tErrors('internal_server_error'), + }); + } + }; + + return { formSchema, cropperRef, onSubmit, setValues, values }; }; diff --git a/packages/shared/src/auth/settings/user.dto.ts b/packages/shared/src/auth/settings/user.dto.ts new file mode 100644 index 000000000..257718548 --- /dev/null +++ b/packages/shared/src/auth/settings/user.dto.ts @@ -0,0 +1,14 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class UploadAvatarUserSettingsAuthBody { + @ApiPropertyOptional({ type: 'string', format: 'binary' }) + avatar?: Express.Multer.File; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + @Transform(({ value }) => value === 'true') + delete_avatar: boolean; +}