From f7eab636c71cd79856c9f9a6654d6d22a5811c2c Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Wed, 15 Apr 2026 03:01:50 +0300 Subject: [PATCH 01/19] Implemented Reset Password pages --- src/app/components/fields/password.tsx | 11 ++-- .../hooks/locales/translations/pages/guest.ts | 2 +- .../locales/bg-BG/components/form/errors.ts | 1 + .../locales/bg-BG/components/form/labels.ts | 1 + .../bg-BG/components/form/placeholders.ts | 3 +- .../bg-BG/pages/guest/signin/forgot.ts | 8 +++ .../locales/bg-BG/pages/guest/signin/reset.ts | 8 +++ .../locales/en-GB/components/form/errors.ts | 1 + .../locales/en-GB/components/form/labels.ts | 1 + .../en-GB/components/form/placeholders.ts | 3 +- .../en-GB/pages/guest/signin/forgot.ts | 8 +++ .../locales/en-GB/pages/guest/signin/reset.ts | 8 +++ src/app/locales/types/components/form.ts | 3 + src/app/locales/types/pages/guest/index.ts | 2 + src/app/locales/types/pages/guest/signin.ts | 14 +++++ src/app/locales/types/pages/guest/signup.ts | 38 +++++++++++++ .../guest/signin/login/hooks/useFields.tsx | 15 ++--- .../password/forgot/hooks/useFields.tsx | 37 ++++++++++++ .../signin/password/forgot/hooks/useForm.ts | 39 +++++++++++++ .../guest/signin/password/forgot/index.tsx | 57 +++++++++++++++++++ .../signin/password/reset/hooks/useFields.tsx | 30 ++++++++++ .../signin/password/reset/hooks/useForm.ts | 43 ++++++++++++++ .../guest/signin/password/reset/index.tsx | 57 +++++++++++++++++++ src/app/validators/email.ts | 26 +++++++++ src/app/validators/password.ts | 28 +++++++++ src/routeTree.gen.ts | 50 +++++++++++++++- src/routes/_guest/forgot-password.tsx | 7 +++ src/routes/_guest/reset-password.tsx | 12 ++++ vite.plugins.ts | 10 ++++ 29 files changed, 504 insertions(+), 19 deletions(-) create mode 100644 src/app/locales/bg-BG/pages/guest/signin/forgot.ts create mode 100644 src/app/locales/bg-BG/pages/guest/signin/reset.ts create mode 100644 src/app/locales/en-GB/pages/guest/signin/forgot.ts create mode 100644 src/app/locales/en-GB/pages/guest/signin/reset.ts create mode 100644 src/app/locales/types/pages/guest/signup.ts create mode 100644 src/app/pages/guest/signin/password/forgot/hooks/useFields.tsx create mode 100644 src/app/pages/guest/signin/password/forgot/hooks/useForm.ts create mode 100644 src/app/pages/guest/signin/password/forgot/index.tsx create mode 100644 src/app/pages/guest/signin/password/reset/hooks/useFields.tsx create mode 100644 src/app/pages/guest/signin/password/reset/hooks/useForm.ts create mode 100644 src/app/pages/guest/signin/password/reset/index.tsx create mode 100644 src/app/validators/email.ts create mode 100644 src/app/validators/password.ts create mode 100644 src/routes/_guest/forgot-password.tsx create mode 100644 src/routes/_guest/reset-password.tsx diff --git a/src/app/components/fields/password.tsx b/src/app/components/fields/password.tsx index 3f8f31f..dfd050c 100644 --- a/src/app/components/fields/password.tsx +++ b/src/app/components/fields/password.tsx @@ -1,17 +1,20 @@ import { useState } from 'react'; import { AnyFieldApi } from '@tanstack/react-form'; import { Eye, EyeOff, LucideIcon } from 'lucide-react'; +import { useFormTranslations } from '@/app/hooks/locales/translations/components'; import { Input } from '@/app/components/ui/input'; const ToggledIcon = ({ Icon }: { Icon: LucideIcon }) => ( ); -type Props = { api: AnyFieldApi; placeholder: string }; -const PasswordField = ({ api, placeholder }: Props) => { +type Props = { api: AnyFieldApi; placeholder?: string }; +const PasswordInput = ({ api, placeholder }: Props) => { const [isVisible, setIsVisible] = useState(false); const toggleVisibility = () => setIsVisible((prev) => !prev); + const tPlaceholders = useFormTranslations('placeholders'); + return (
{ value={api.state.value} onChange={({ target: { value } }) => api.handleChange(value)} onBlur={api.handleBlur} - placeholder={placeholder} + placeholder={placeholder ?? tPlaceholders('password')} /> @@ -29,4 +32,4 @@ const PasswordField = ({ api, placeholder }: Props) => { ); }; -export default PasswordField; +export default PasswordInput; diff --git a/src/app/hooks/locales/translations/pages/guest.ts b/src/app/hooks/locales/translations/pages/guest.ts index cbffdf1..3b52fb6 100644 --- a/src/app/hooks/locales/translations/pages/guest.ts +++ b/src/app/hooks/locales/translations/pages/guest.ts @@ -1,5 +1,5 @@ import { useTranslation } from '../../useTranslation'; -type Signin = 'login'; +type Signin = 'login' | 'forgot' | 'reset'; export const useSigninTranslations = (ns: N) => useTranslation(`pages.guest.signin.${ns}`).t; diff --git a/src/app/locales/bg-BG/components/form/errors.ts b/src/app/locales/bg-BG/components/form/errors.ts index 42c4474..e632bb9 100644 --- a/src/app/locales/bg-BG/components/form/errors.ts +++ b/src/app/locales/bg-BG/components/form/errors.ts @@ -3,4 +3,5 @@ import { Errors } from '@/app/locales/types/components/form'; export default { required: "Полето '{{field}}' е задължително", length: "Дължината на '{{field}}' трябва да е между {{min}} и {{max}}", + pattern: "Полето '{{field}}' не е валидно", } satisfies Errors; diff --git a/src/app/locales/bg-BG/components/form/labels.ts b/src/app/locales/bg-BG/components/form/labels.ts index b973d97..3070f79 100644 --- a/src/app/locales/bg-BG/components/form/labels.ts +++ b/src/app/locales/bg-BG/components/form/labels.ts @@ -2,6 +2,7 @@ import { Labels } from '@/app/locales/types/components/form'; export default { username: 'Потребителско име', + email: 'Имейл', password: 'Парола', 'forgot-password': 'Забравихте паролата си?', 'remember-me': 'Запомни ме', diff --git a/src/app/locales/bg-BG/components/form/placeholders.ts b/src/app/locales/bg-BG/components/form/placeholders.ts index 4fcd305..88f77f1 100644 --- a/src/app/locales/bg-BG/components/form/placeholders.ts +++ b/src/app/locales/bg-BG/components/form/placeholders.ts @@ -2,6 +2,7 @@ import { Placeholders } from '@/app/locales/types/components/form'; export default { username: 'Иван_Георгиев', + email: 'ivan@petrov.com', password: '54Р0Л4', - 'search-products': 'Потърси Продукт по Името му', + 'search-products': 'Потърси Продукт', } satisfies Placeholders; diff --git a/src/app/locales/bg-BG/pages/guest/signin/forgot.ts b/src/app/locales/bg-BG/pages/guest/signin/forgot.ts new file mode 100644 index 0000000..130ec59 --- /dev/null +++ b/src/app/locales/bg-BG/pages/guest/signin/forgot.ts @@ -0,0 +1,8 @@ +import { Forgot } from '@/app/locales/types/pages/guest/signin'; + +export default { + title: 'Забравихте Паролата си?', + subtitle: 'Ще Ви изпратим линк!', + button: 'Изпрати', + message: 'Проверете си кутията със съобщения!', +} satisfies Forgot; diff --git a/src/app/locales/bg-BG/pages/guest/signin/reset.ts b/src/app/locales/bg-BG/pages/guest/signin/reset.ts new file mode 100644 index 0000000..8a94be8 --- /dev/null +++ b/src/app/locales/bg-BG/pages/guest/signin/reset.ts @@ -0,0 +1,8 @@ +import { Reset } from '@/app/locales/types/pages/guest/signin'; + +export default { + title: 'Заявихте смяна на Паролата си?', + subtitle: 'Задайте нова!', + button: 'Сменете', + message: 'Парола сменена!', +} satisfies Reset; diff --git a/src/app/locales/en-GB/components/form/errors.ts b/src/app/locales/en-GB/components/form/errors.ts index c53ec4d..7220658 100644 --- a/src/app/locales/en-GB/components/form/errors.ts +++ b/src/app/locales/en-GB/components/form/errors.ts @@ -3,4 +3,5 @@ import { Errors } from '@/app/locales/types/components/form'; export default { required: "The field '{{field}}' is required", length: "The length of '{{field}}' must be between {{min}} and {{max}}", + pattern: "The field '{{field}}' is not valid", } satisfies Errors; diff --git a/src/app/locales/en-GB/components/form/labels.ts b/src/app/locales/en-GB/components/form/labels.ts index eb85b72..67dccf8 100644 --- a/src/app/locales/en-GB/components/form/labels.ts +++ b/src/app/locales/en-GB/components/form/labels.ts @@ -2,6 +2,7 @@ import { Labels } from '@/app/locales/types/components/form'; export default { username: 'Username', + email: 'Email', password: 'Password', 'forgot-password': 'Forgot your password?', 'remember-me': 'Remember me', diff --git a/src/app/locales/en-GB/components/form/placeholders.ts b/src/app/locales/en-GB/components/form/placeholders.ts index ebab446..23ab32d 100644 --- a/src/app/locales/en-GB/components/form/placeholders.ts +++ b/src/app/locales/en-GB/components/form/placeholders.ts @@ -2,6 +2,7 @@ import { Placeholders } from '@/app/locales/types/components/form'; export default { username: 'John_Doe', + email: 'john@doe.com', password: 'P455W0RD', - 'search-products': 'Search a Product by its Name', + 'search-products': 'Search for a Product', } satisfies Placeholders; diff --git a/src/app/locales/en-GB/pages/guest/signin/forgot.ts b/src/app/locales/en-GB/pages/guest/signin/forgot.ts new file mode 100644 index 0000000..edff83f --- /dev/null +++ b/src/app/locales/en-GB/pages/guest/signin/forgot.ts @@ -0,0 +1,8 @@ +import { Forgot } from '@/app/locales/types/pages/guest/signin'; + +export default { + title: 'Forgot your Password?', + subtitle: "We'll send you a link!", + button: 'Send', + message: 'Check your inbox!', +} satisfies Forgot; diff --git a/src/app/locales/en-GB/pages/guest/signin/reset.ts b/src/app/locales/en-GB/pages/guest/signin/reset.ts new file mode 100644 index 0000000..32aa9e0 --- /dev/null +++ b/src/app/locales/en-GB/pages/guest/signin/reset.ts @@ -0,0 +1,8 @@ +import { Reset } from '@/app/locales/types/pages/guest/signin'; + +export default { + title: 'Requestsed resetting your Password?', + subtitle: 'Set a new one!', + button: 'Reset', + message: 'Password reset!', +} satisfies Reset; diff --git a/src/app/locales/types/components/form.ts b/src/app/locales/types/components/form.ts index 566c6a2..cb7402c 100644 --- a/src/app/locales/types/components/form.ts +++ b/src/app/locales/types/components/form.ts @@ -1,10 +1,12 @@ export type Errors = { required: string; length: string; + pattern: string; }; export type Labels = { username: string; + email: string; password: string; 'remember-me': string; 'forgot-password': string; @@ -13,5 +15,6 @@ export type Labels = { export type Placeholders = { 'search-products': string; username: string; + email: string; password: string; }; diff --git a/src/app/locales/types/pages/guest/index.ts b/src/app/locales/types/pages/guest/index.ts index 4a78177..37831ac 100644 --- a/src/app/locales/types/pages/guest/index.ts +++ b/src/app/locales/types/pages/guest/index.ts @@ -2,4 +2,6 @@ import * as signin from './signin'; export type Translations = { 'pages.guest.signin.login': signin.Login; + 'pages.guest.signin.forgot': signin.Forgot; + 'pages.guest.signin.reset': signin.Reset; }; diff --git a/src/app/locales/types/pages/guest/signin.ts b/src/app/locales/types/pages/guest/signin.ts index 4bce2a6..a936af0 100644 --- a/src/app/locales/types/pages/guest/signin.ts +++ b/src/app/locales/types/pages/guest/signin.ts @@ -6,3 +6,17 @@ export type Login = { 'register-message': string; register: string; }; + +export type Forgot = { + title: string; + subtitle: string; + button: string; + message: string; +}; + +export type Reset = { + title: string; + subtitle: string; + button: string; + message: string; +}; diff --git a/src/app/locales/types/pages/guest/signup.ts b/src/app/locales/types/pages/guest/signup.ts new file mode 100644 index 0000000..661f6e9 --- /dev/null +++ b/src/app/locales/types/pages/guest/signup.ts @@ -0,0 +1,38 @@ +export type PickRole = { + title: string; + 'customer-subtitle': string; + 'customer-plus-1': string; + 'customer-plus-2': string; + 'customer-plus-3': string; + 'contributor-subtitle': string; + 'contributor-plus-1': string; + 'contributor-plus-2': string; + 'contributor-plus-3': string; + btn: string; +}; + +export type Register = { + 'go-back': string; + 'title-customer': string; + 'title-contributor': string; + btn: string; + 'login-message': string; + 'resend-message': string; + login: string; + resend: string; + 'email-sent': string; +}; + +export type ConfirmEmail = { + title: string; + btn: string; + success: string; + error: string; +}; + +export type RetryConfirmEmail = { + title: string; + btn: string; + success: string; + error: string; +}; diff --git a/src/app/pages/guest/signin/login/hooks/useFields.tsx b/src/app/pages/guest/signin/login/hooks/useFields.tsx index 9512728..dd272bf 100644 --- a/src/app/pages/guest/signin/login/hooks/useFields.tsx +++ b/src/app/pages/guest/signin/login/hooks/useFields.tsx @@ -2,7 +2,7 @@ import { Link } from '@tanstack/react-router'; import { useFormTranslations } from '@/app/hooks/locales/translations/components'; import { Label } from '@/app/components/ui/label'; import { Input } from '@/app/components/ui/input'; -import PasswordField from '@/app/components/fields/password'; +import PasswordInput from '@/app/components/fields/password'; import Error from '@/app/components/fields/error'; import { useForm } from './useForm'; @@ -41,13 +41,10 @@ export const useFields = () => { {tLabels('password')}
- + {tLabels('forgot-password')} @@ -81,9 +78,5 @@ export const useFields = () => { ), }; - return { - fields, - error, - handleSubmit, - }; + return { fields, error, handleSubmit }; }; diff --git a/src/app/pages/guest/signin/password/forgot/hooks/useFields.tsx b/src/app/pages/guest/signin/password/forgot/hooks/useFields.tsx new file mode 100644 index 0000000..f43e9b1 --- /dev/null +++ b/src/app/pages/guest/signin/password/forgot/hooks/useFields.tsx @@ -0,0 +1,37 @@ +import { useFormTranslations } from '@/app/hooks/locales/translations/components'; +import { Label } from '@/app/components/ui/label'; +import { Input } from '@/app/components/ui/input'; +import Error from '@/app/components/fields/error'; +import { useForm } from './useForm'; + +export const useFields = () => { + const tLabels = useFormTranslations('labels'); + const tPlaceholders = useFormTranslations('placeholders'); + + const { form, error, isSubmitted, handleSubmit } = useForm(); + const fields = { + Email: () => ( + + {(api) => ( + <> +
+ +
+ + api.handleChange(value) + } + placeholder={tPlaceholders('email')} + /> + + + )} +
+ ), + }; + + return { fields, error, isSubmitted, handleSubmit }; +}; diff --git a/src/app/pages/guest/signin/password/forgot/hooks/useForm.ts b/src/app/pages/guest/signin/password/forgot/hooks/useForm.ts new file mode 100644 index 0000000..be1fddd --- /dev/null +++ b/src/app/pages/guest/signin/password/forgot/hooks/useForm.ts @@ -0,0 +1,39 @@ +import { useState } from 'react'; +import { useForm as useTanStackForm } from '@tanstack/react-form'; +import { useMutation } from '@customcads/react-sdk'; +import * as form from '@/lib/utils/form'; +import { useFormTranslations } from '@/app/hooks/locales/translations/components'; +import { useForceLocaleRefresh } from '@/app/hooks/locales/useForceLocaleRefresh'; +import { schema } from '@/app/validators/email'; + +export const useForm = () => { + const [isSubmitted, setIsSubmitted] = useState(false); + + const { mutateAsync: sendEmail, error } = useMutation( + ({ identity }) => identity.forgotPassword, + ); + + const tErrors = useFormTranslations('errors'); + const tLabels = useFormTranslations('labels'); + + const formApi = useTanStackForm({ + defaultValues: { email: '' }, + onSubmit: async ({ value }) => { + await sendEmail({ email: value.email }); + }, + validators: { + onChange: schema({ tErrors, tLabels }), + }, + }); + useForceLocaleRefresh(() => formApi.validate('change')); + + return { + form: formApi, + error: form.extractError(error as unknown), + handleSubmit: (e: React.FormEvent) => { + setIsSubmitted(true); + return form.handleSubmit(e, formApi.handleSubmit); + }, + isSubmitted, + }; +}; diff --git a/src/app/pages/guest/signin/password/forgot/index.tsx b/src/app/pages/guest/signin/password/forgot/index.tsx new file mode 100644 index 0000000..e5fd554 --- /dev/null +++ b/src/app/pages/guest/signin/password/forgot/index.tsx @@ -0,0 +1,57 @@ +import { cn } from '@/lib/utils/tailwindcss'; +import { useSigninTranslations } from '@/app/hooks/locales/translations/pages/guest'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/app/components/ui/card'; +import { Button } from '@/app/components/ui/button'; +import * as page from '@/app/utils/page'; +import { useFields } from './hooks/useFields'; + +const ForgotPassword = () => { + const { fields, error, isSubmitted, handleSubmit } = useFields(); + const tForgot = useSigninTranslations('forgot'); + + return ( +
+
+ + + + {tForgot('title')} + + + {tForgot('subtitle')} + + + +
+
+ +
+
+
+ + {error && ( + + {error} + + )} + + {isSubmitted && !error && ( + + {tForgot('message')} + + )} + +
+
+
+ ); +}; + +export default ForgotPassword; diff --git a/src/app/pages/guest/signin/password/reset/hooks/useFields.tsx b/src/app/pages/guest/signin/password/reset/hooks/useFields.tsx new file mode 100644 index 0000000..faf78e9 --- /dev/null +++ b/src/app/pages/guest/signin/password/reset/hooks/useFields.tsx @@ -0,0 +1,30 @@ +import { useFormTranslations } from '@/app/hooks/locales/translations/components'; +import { Label } from '@/app/components/ui/label'; +import PasswordInput from '@/app/components/fields/password'; +import Error from '@/app/components/fields/error'; +import { useForm } from './useForm'; + +export const useFields = () => { + const tLabels = useFormTranslations('labels'); + + const { form, error, isSubmitted, handleSubmit } = useForm(); + const fields = { + Password: () => ( + + {(api) => ( + <> +
+ +
+ + + + )} +
+ ), + }; + + return { fields, error, isSubmitted, handleSubmit }; +}; diff --git a/src/app/pages/guest/signin/password/reset/hooks/useForm.ts b/src/app/pages/guest/signin/password/reset/hooks/useForm.ts new file mode 100644 index 0000000..42cf071 --- /dev/null +++ b/src/app/pages/guest/signin/password/reset/hooks/useForm.ts @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import { getRouteApi } from '@tanstack/react-router'; +import { useForm as useTanStackForm } from '@tanstack/react-form'; +import { useMutation } from '@customcads/react-sdk'; +import * as form from '@/lib/utils/form'; +import { useFormTranslations } from '@/app/hooks/locales/translations/components'; +import { useForceLocaleRefresh } from '@/app/hooks/locales/useForceLocaleRefresh'; +import { schema } from '@/app/validators/password'; + +const Route = getRouteApi('/_guest/reset-password'); + +export const useForm = () => { + const { email, token } = Route.useSearch(); + const [isSubmitted, setIsSubmitted] = useState(false); + + const { mutateAsync: resetPassword, error } = useMutation( + ({ identity }) => identity.resetPassword, + ); + + const tErrors = useFormTranslations('errors'); + const tLabels = useFormTranslations('labels'); + + const formApi = useTanStackForm({ + defaultValues: { password: '' }, + onSubmit: async ({ value }) => { + await resetPassword({ email, token, newPassword: value.password }); + }, + validators: { + onChange: schema({ tErrors, tLabels }), + }, + }); + useForceLocaleRefresh(() => formApi.validate('change')); + + return { + form: formApi, + error: form.extractError(error as unknown), + handleSubmit: (e: React.FormEvent) => { + setIsSubmitted(true); + return form.handleSubmit(e, formApi.handleSubmit); + }, + isSubmitted, + }; +}; diff --git a/src/app/pages/guest/signin/password/reset/index.tsx b/src/app/pages/guest/signin/password/reset/index.tsx new file mode 100644 index 0000000..a3c02b0 --- /dev/null +++ b/src/app/pages/guest/signin/password/reset/index.tsx @@ -0,0 +1,57 @@ +import { cn } from '@/lib/utils/tailwindcss'; +import { useSigninTranslations } from '@/app/hooks/locales/translations/pages/guest'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/app/components/ui/card'; +import { Button } from '@/app/components/ui/button'; +import * as page from '@/app/utils/page'; +import { useFields } from './hooks/useFields'; + +const ForgotPassword = () => { + const { fields, error, isSubmitted, handleSubmit } = useFields(); + const tForgot = useSigninTranslations('reset'); + + return ( +
+
+ + + + {tForgot('title')} + + + {tForgot('subtitle')} + + + +
+
+ +
+
+
+ + {error && ( + + {error} + + )} + + {isSubmitted && !error && ( + + {tForgot('message')} + + )} + +
+
+
+ ); +}; + +export default ForgotPassword; diff --git a/src/app/validators/email.ts b/src/app/validators/email.ts new file mode 100644 index 0000000..fb7614d --- /dev/null +++ b/src/app/validators/email.ts @@ -0,0 +1,26 @@ +import z from 'zod'; +import { TOptions } from 'i18next'; +import * as form from '@/app/locales/types/components/form'; +import { USERS as VALIDATIONS } from '@/app/constants/validations'; + +const { email } = VALIDATIONS; + +type Props = { + tErrors: (key: keyof form.Errors, options?: TOptions) => string; + tLabels: (key: keyof form.Labels, options?: TOptions) => string; +}; +export const schema = ({ tErrors, tLabels }: Props) => { + const args = { + email: { + field: tLabels('email'), + regex: email.regex, + }, + }; + + return z.object({ + email: z + .string() + .nonempty({ message: tErrors('required', args.email) }) + .regex(email.regex), + }); +}; diff --git a/src/app/validators/password.ts b/src/app/validators/password.ts new file mode 100644 index 0000000..8525248 --- /dev/null +++ b/src/app/validators/password.ts @@ -0,0 +1,28 @@ +import z from 'zod'; +import { TOptions } from 'i18next'; +import * as form from '@/app/locales/types/components/form'; +import { USERS as VALIDATIONS } from '@/app/constants/validations'; + +const { password } = VALIDATIONS; + +type Props = { + tErrors: (key: keyof form.Errors, options?: TOptions) => string; + tLabels: (key: keyof form.Labels, options?: TOptions) => string; +}; +export const schema = ({ tErrors, tLabels }: Props) => { + const args = { + password: { + field: tLabels('password'), + min: password.min, + max: password.max, + }, + }; + + return z.object({ + password: z + .string() + .nonempty({ message: tErrors('required', args.password) }) + .max(password.max, { message: tErrors('length', args.password) }) + .min(password.min, { message: tErrors('length', args.password) }), + }); +}; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 269787d..7943509 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -12,7 +12,9 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as GuestRouteImport } from './routes/_guest' import { Route as PublicIndexRouteImport } from './routes/_public/index' import { Route as PublicCartRouteImport } from './routes/_public/cart' +import { Route as GuestResetPasswordRouteImport } from './routes/_guest/reset-password' import { Route as GuestLoginRouteImport } from './routes/_guest/login' +import { Route as GuestForgotPasswordRouteImport } from './routes/_guest/forgot-password' import { Route as PublicGalleryIndexRouteImport } from './routes/_public/gallery/index' import { Route as PublicGalleryIdRouteImport } from './routes/_public/gallery/$id' import { Route as PublicEditorIdRouteImport } from './routes/_public/editor.$id' @@ -31,11 +33,21 @@ const PublicCartRoute = PublicCartRouteImport.update({ path: '/cart', getParentRoute: () => rootRouteImport, } as any) +const GuestResetPasswordRoute = GuestResetPasswordRouteImport.update({ + id: '/reset-password', + path: '/reset-password', + getParentRoute: () => GuestRoute, +} as any) const GuestLoginRoute = GuestLoginRouteImport.update({ id: '/login', path: '/login', getParentRoute: () => GuestRoute, } as any) +const GuestForgotPasswordRoute = GuestForgotPasswordRouteImport.update({ + id: '/forgot-password', + path: '/forgot-password', + getParentRoute: () => GuestRoute, +} as any) const PublicGalleryIndexRoute = PublicGalleryIndexRouteImport.update({ id: '/_public/gallery/', path: '/gallery/', @@ -53,7 +65,9 @@ const PublicEditorIdRoute = PublicEditorIdRouteImport.update({ } as any) export interface FileRoutesByFullPath { + '/forgot-password': typeof GuestForgotPasswordRoute '/login': typeof GuestLoginRoute + '/reset-password': typeof GuestResetPasswordRoute '/cart': typeof PublicCartRoute '/': typeof PublicIndexRoute '/editor/$id': typeof PublicEditorIdRoute @@ -61,7 +75,9 @@ export interface FileRoutesByFullPath { '/gallery': typeof PublicGalleryIndexRoute } export interface FileRoutesByTo { + '/forgot-password': typeof GuestForgotPasswordRoute '/login': typeof GuestLoginRoute + '/reset-password': typeof GuestResetPasswordRoute '/cart': typeof PublicCartRoute '/': typeof PublicIndexRoute '/editor/$id': typeof PublicEditorIdRoute @@ -71,7 +87,9 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/_guest': typeof GuestRouteWithChildren + '/_guest/forgot-password': typeof GuestForgotPasswordRoute '/_guest/login': typeof GuestLoginRoute + '/_guest/reset-password': typeof GuestResetPasswordRoute '/_public/cart': typeof PublicCartRoute '/_public/': typeof PublicIndexRoute '/_public/editor/$id': typeof PublicEditorIdRoute @@ -81,18 +99,30 @@ export interface FileRoutesById { export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: + | '/forgot-password' | '/login' + | '/reset-password' | '/cart' | '/' | '/editor/$id' | '/gallery/$id' | '/gallery' fileRoutesByTo: FileRoutesByTo - to: '/login' | '/cart' | '/' | '/editor/$id' | '/gallery/$id' | '/gallery' + to: + | '/forgot-password' + | '/login' + | '/reset-password' + | '/cart' + | '/' + | '/editor/$id' + | '/gallery/$id' + | '/gallery' id: | '__root__' | '/_guest' + | '/_guest/forgot-password' | '/_guest/login' + | '/_guest/reset-password' | '/_public/cart' | '/_public/' | '/_public/editor/$id' @@ -132,6 +162,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PublicCartRouteImport parentRoute: typeof rootRouteImport } + '/_guest/reset-password': { + id: '/_guest/reset-password' + path: '/reset-password' + fullPath: '/reset-password' + preLoaderRoute: typeof GuestResetPasswordRouteImport + parentRoute: typeof GuestRoute + } '/_guest/login': { id: '/_guest/login' path: '/login' @@ -139,6 +176,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof GuestLoginRouteImport parentRoute: typeof GuestRoute } + '/_guest/forgot-password': { + id: '/_guest/forgot-password' + path: '/forgot-password' + fullPath: '/forgot-password' + preLoaderRoute: typeof GuestForgotPasswordRouteImport + parentRoute: typeof GuestRoute + } '/_public/gallery/': { id: '/_public/gallery/' path: '/gallery' @@ -164,11 +208,15 @@ declare module '@tanstack/react-router' { } interface GuestRouteChildren { + GuestForgotPasswordRoute: typeof GuestForgotPasswordRoute GuestLoginRoute: typeof GuestLoginRoute + GuestResetPasswordRoute: typeof GuestResetPasswordRoute } const GuestRouteChildren: GuestRouteChildren = { + GuestForgotPasswordRoute: GuestForgotPasswordRoute, GuestLoginRoute: GuestLoginRoute, + GuestResetPasswordRoute: GuestResetPasswordRoute, } const GuestRouteWithChildren = GuestRoute._addFileChildren(GuestRouteChildren) diff --git a/src/routes/_guest/forgot-password.tsx b/src/routes/_guest/forgot-password.tsx new file mode 100644 index 0000000..624f089 --- /dev/null +++ b/src/routes/_guest/forgot-password.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import ForgotPassword from '@/app/pages/guest/signin/password/forgot'; + +export const Route = createFileRoute('/_guest/forgot-password')({ + component: ForgotPassword, + head: () => ({ meta: [{ title: 'CustomCADs | Forgot Password' }] }), +}); diff --git a/src/routes/_guest/reset-password.tsx b/src/routes/_guest/reset-password.tsx new file mode 100644 index 0000000..2ce98bf --- /dev/null +++ b/src/routes/_guest/reset-password.tsx @@ -0,0 +1,12 @@ +import z from 'zod'; +import { createFileRoute } from '@tanstack/react-router'; +import ResetPassword from '@/app/pages/guest/signin/password/reset'; + +export const Route = createFileRoute('/_guest/reset-password')({ + component: ResetPassword, + validateSearch: z.object({ + email: z.email(), + token: z.string(), + }), + head: () => ({ meta: [{ title: 'CustomCADs | Reset Password' }] }), +}); diff --git a/vite.plugins.ts b/vite.plugins.ts index eba44ee..8a7c36e 100644 --- a/vite.plugins.ts +++ b/vite.plugins.ts @@ -30,6 +30,16 @@ const tanStackStartPlugin = ({ mode }: { mode: string }) => prerender: { enabled: false, crawlLinks: false }, sitemap: { exclude: true }, }, + { + path: '/forgot-password', + prerender: { enabled: false, crawlLinks: false }, + sitemap: { exclude: true }, + }, + { + path: '/reset-password', + prerender: { enabled: false, crawlLinks: false }, + sitemap: { exclude: true }, + }, { path: '/gallery', prerender: { enabled: false, crawlLinks: false }, From 8b376103a458263ff0387c09f5d5c1daaabda2f9 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Thu, 16 Apr 2026 14:55:47 +0300 Subject: [PATCH 02/19] Significantly improved Validators system --- .../pages/guest/signin/login/hooks/useForm.ts | 9 +--- .../signin/password/forgot/hooks/useForm.ts | 2 +- .../signin/password/reset/hooks/useForm.ts | 2 +- src/app/types/schema.ts | 7 +++ src/app/validators/email.ts | 26 --------- src/app/validators/forgot-password.ts | 7 +++ src/app/validators/login.ts | 43 ++++----------- src/app/validators/password.ts | 28 ---------- src/app/validators/reset-password.ts | 7 +++ src/app/validators/schemas/email.ts | 19 +++++++ src/app/validators/schemas/index.ts | 3 ++ src/app/validators/schemas/name.ts | 54 +++++++++++++++++++ src/app/validators/schemas/password.ts | 45 ++++++++++++++++ 13 files changed, 155 insertions(+), 97 deletions(-) create mode 100644 src/app/types/schema.ts delete mode 100644 src/app/validators/email.ts create mode 100644 src/app/validators/forgot-password.ts delete mode 100644 src/app/validators/password.ts create mode 100644 src/app/validators/reset-password.ts create mode 100644 src/app/validators/schemas/email.ts create mode 100644 src/app/validators/schemas/index.ts create mode 100644 src/app/validators/schemas/name.ts create mode 100644 src/app/validators/schemas/password.ts diff --git a/src/app/pages/guest/signin/login/hooks/useForm.ts b/src/app/pages/guest/signin/login/hooks/useForm.ts index 7ff1987..dfa525e 100644 --- a/src/app/pages/guest/signin/login/hooks/useForm.ts +++ b/src/app/pages/guest/signin/login/hooks/useForm.ts @@ -9,14 +9,9 @@ import { useNotificationQueryData } from '@/app/hooks/features/notifications/use import { useCartTransfer } from '@/app/hooks/features/carts/useCartTransfer'; import { useFormTranslations } from '@/app/hooks/locales/translations/components'; import { useForceLocaleRefresh } from '@/app/hooks/locales/useForceLocaleRefresh'; -import { schema } from '@/app/validators/login'; +import { schema, Data } from '@/app/validators/login'; -type Fields = { - username: string; - password: string; - rememberMe: boolean; -}; -const defaultValues: Fields = { +const defaultValues: Data = { username: '', password: '', rememberMe: false, diff --git a/src/app/pages/guest/signin/password/forgot/hooks/useForm.ts b/src/app/pages/guest/signin/password/forgot/hooks/useForm.ts index be1fddd..2d14dd4 100644 --- a/src/app/pages/guest/signin/password/forgot/hooks/useForm.ts +++ b/src/app/pages/guest/signin/password/forgot/hooks/useForm.ts @@ -4,7 +4,7 @@ import { useMutation } from '@customcads/react-sdk'; import * as form from '@/lib/utils/form'; import { useFormTranslations } from '@/app/hooks/locales/translations/components'; import { useForceLocaleRefresh } from '@/app/hooks/locales/useForceLocaleRefresh'; -import { schema } from '@/app/validators/email'; +import { schema } from '@/app/validators/forgot-password'; export const useForm = () => { const [isSubmitted, setIsSubmitted] = useState(false); diff --git a/src/app/pages/guest/signin/password/reset/hooks/useForm.ts b/src/app/pages/guest/signin/password/reset/hooks/useForm.ts index 42cf071..06d6238 100644 --- a/src/app/pages/guest/signin/password/reset/hooks/useForm.ts +++ b/src/app/pages/guest/signin/password/reset/hooks/useForm.ts @@ -5,7 +5,7 @@ import { useMutation } from '@customcads/react-sdk'; import * as form from '@/lib/utils/form'; import { useFormTranslations } from '@/app/hooks/locales/translations/components'; import { useForceLocaleRefresh } from '@/app/hooks/locales/useForceLocaleRefresh'; -import { schema } from '@/app/validators/password'; +import { schema } from '@/app/validators/reset-password'; const Route = getRouteApi('/_guest/reset-password'); diff --git a/src/app/types/schema.ts b/src/app/types/schema.ts new file mode 100644 index 0000000..0ebbc50 --- /dev/null +++ b/src/app/types/schema.ts @@ -0,0 +1,7 @@ +import { TOptions } from 'i18next'; +import * as form from '@/app/locales/types/components/form'; + +export type Translators = { + tErrors: (key: keyof form.Errors, options?: TOptions) => string; + tLabels: (key: keyof form.Labels, options?: TOptions) => string; +}; diff --git a/src/app/validators/email.ts b/src/app/validators/email.ts deleted file mode 100644 index fb7614d..0000000 --- a/src/app/validators/email.ts +++ /dev/null @@ -1,26 +0,0 @@ -import z from 'zod'; -import { TOptions } from 'i18next'; -import * as form from '@/app/locales/types/components/form'; -import { USERS as VALIDATIONS } from '@/app/constants/validations'; - -const { email } = VALIDATIONS; - -type Props = { - tErrors: (key: keyof form.Errors, options?: TOptions) => string; - tLabels: (key: keyof form.Labels, options?: TOptions) => string; -}; -export const schema = ({ tErrors, tLabels }: Props) => { - const args = { - email: { - field: tLabels('email'), - regex: email.regex, - }, - }; - - return z.object({ - email: z - .string() - .nonempty({ message: tErrors('required', args.email) }) - .regex(email.regex), - }); -}; diff --git a/src/app/validators/forgot-password.ts b/src/app/validators/forgot-password.ts new file mode 100644 index 0000000..4e74866 --- /dev/null +++ b/src/app/validators/forgot-password.ts @@ -0,0 +1,7 @@ +import z from 'zod'; +import { Translators } from '@/app/types/schema'; +import { email } from './schemas'; + +type Props = Translators; +export const schema = ({ tErrors, tLabels }: Props) => + z.object({ ...email({ tErrors, tLabels }) }); diff --git a/src/app/validators/login.ts b/src/app/validators/login.ts index fcae387..cbb1202 100644 --- a/src/app/validators/login.ts +++ b/src/app/validators/login.ts @@ -1,38 +1,13 @@ import z from 'zod'; -import { TOptions } from 'i18next'; -import * as form from '@/app/locales/types/components/form'; -import { USERS as VALIDATIONS } from '@/app/constants/validations'; -const { name, password } = VALIDATIONS; +import { Translators } from '@/app/types/schema'; +import { name, password } from './schemas'; -type Props = { - tErrors: (key: keyof form.Errors, options?: TOptions) => string; - tLabels: (key: keyof form.Labels, options?: TOptions) => string; -}; -export const schema = ({ tErrors, tLabels }: Props) => { - const args = { - username: { - field: tLabels('username'), - min: name.min, - max: name.max, - }, - password: { - field: tLabels('password'), - min: password.min, - max: password.max, - }, - }; - - return z.object({ - username: z - .string() - .nonempty({ message: tErrors('required', args.username) }) - .max(name.max, { message: tErrors('length', args.username) }) - .min(name.min, { message: tErrors('length', args.username) }), - password: z - .string() - .nonempty({ message: tErrors('required', args.password) }) - .max(password.max, { message: tErrors('length', args.password) }) - .min(password.min, { message: tErrors('length', args.password) }), +type Props = Translators; +export const schema = ({ tErrors, tLabels }: Props) => + z.object({ rememberMe: z.boolean(), + username: name({ tErrors, tLabels }).username, + password: password({ tErrors, tLabels }).password, }); -}; + +export type Data = z.infer>; diff --git a/src/app/validators/password.ts b/src/app/validators/password.ts deleted file mode 100644 index 8525248..0000000 --- a/src/app/validators/password.ts +++ /dev/null @@ -1,28 +0,0 @@ -import z from 'zod'; -import { TOptions } from 'i18next'; -import * as form from '@/app/locales/types/components/form'; -import { USERS as VALIDATIONS } from '@/app/constants/validations'; - -const { password } = VALIDATIONS; - -type Props = { - tErrors: (key: keyof form.Errors, options?: TOptions) => string; - tLabels: (key: keyof form.Labels, options?: TOptions) => string; -}; -export const schema = ({ tErrors, tLabels }: Props) => { - const args = { - password: { - field: tLabels('password'), - min: password.min, - max: password.max, - }, - }; - - return z.object({ - password: z - .string() - .nonempty({ message: tErrors('required', args.password) }) - .max(password.max, { message: tErrors('length', args.password) }) - .min(password.min, { message: tErrors('length', args.password) }), - }); -}; diff --git a/src/app/validators/reset-password.ts b/src/app/validators/reset-password.ts new file mode 100644 index 0000000..7dc5743 --- /dev/null +++ b/src/app/validators/reset-password.ts @@ -0,0 +1,7 @@ +import z from 'zod'; +import { Translators } from '@/app/types/schema'; +import { password } from './schemas'; + +type Props = Translators; +export const schema = ({ tErrors, tLabels }: Props) => + z.object({ ...password({ tErrors, tLabels }) }); diff --git a/src/app/validators/schemas/email.ts b/src/app/validators/schemas/email.ts new file mode 100644 index 0000000..9b4388f --- /dev/null +++ b/src/app/validators/schemas/email.ts @@ -0,0 +1,19 @@ +import z from 'zod'; +import { Translators } from '@/app/types/schema'; +import { USERS as VALIDATIONS } from '@/app/constants/validations'; + +type Props = Translators; +export const schema = ({ tErrors, tLabels }: Props) => { + const args = { + email: { + field: tLabels('email'), + regex: VALIDATIONS.email.regex, + }, + }; + + return { + email: z + .email({ pattern: args.email.regex }) + .nonempty({ message: tErrors('required', args.email) }), + }; +}; diff --git a/src/app/validators/schemas/index.ts b/src/app/validators/schemas/index.ts new file mode 100644 index 0000000..12f8d8a --- /dev/null +++ b/src/app/validators/schemas/index.ts @@ -0,0 +1,3 @@ +export { schema as email } from './email'; +export { schema as name } from './name'; +export { schema as password } from './password'; diff --git a/src/app/validators/schemas/name.ts b/src/app/validators/schemas/name.ts new file mode 100644 index 0000000..6d300b0 --- /dev/null +++ b/src/app/validators/schemas/name.ts @@ -0,0 +1,54 @@ +import z from 'zod'; +import { Translators } from '@/app/types/schema'; +import { USERS as VALIDATIONS } from '@/app/constants/validations'; + +type Props = Translators; +export const schema = ({ tErrors, tLabels }: Props) => { + const args = { + username: { + field: tLabels('username'), + min: VALIDATIONS.name.min, + max: VALIDATIONS.name.max, + }, + firstName: { + field: tLabels('firstName'), + min: VALIDATIONS.name.min, + max: VALIDATIONS.name.max, + }, + lastName: { + field: tLabels('lastName'), + min: VALIDATIONS.name.min, + max: VALIDATIONS.name.max, + }, + }; + + return { + username: z + .string() + .nonempty({ message: tErrors('required', args.username) }) + .max(args.username.max, { + message: tErrors('length', args.username), + }) + .min(args.username.min, { + message: tErrors('length', args.username), + }), + firstName: z + .string() + .nonempty({ message: tErrors('required', args.firstName) }) + .max(args.firstName.max, { + message: tErrors('length', args.firstName), + }) + .min(args.firstName.min, { + message: tErrors('length', args.firstName), + }), + lastName: z + .string() + .nonempty({ message: tErrors('required', args.lastName) }) + .max(args.lastName.max, { + message: tErrors('length', args.lastName), + }) + .min(args.lastName.min, { + message: tErrors('length', args.lastName), + }), + }; +}; diff --git a/src/app/validators/schemas/password.ts b/src/app/validators/schemas/password.ts new file mode 100644 index 0000000..fb8ffd8 --- /dev/null +++ b/src/app/validators/schemas/password.ts @@ -0,0 +1,45 @@ +import z from 'zod'; +import { equalityHelper } from '@/lib/utils/form'; +import { Translators } from '@/app/types/schema'; +import { USERS as VALIDATIONS } from '@/app/constants/validations'; + +type Props = Translators; +export const schema = ({ tErrors, tLabels }: Props) => { + const args = { + password: { + field: tLabels('password'), + min: VALIDATIONS.password.min, + max: VALIDATIONS.password.max, + }, + confirmPassword: { + field: tLabels('confirm-password'), + min: VALIDATIONS.password.min, + max: VALIDATIONS.password.max, + }, + }; + + const equality = equalityHelper(); + + const password = z + .string() + .nonempty({ message: tErrors('required', args.password) }) + .max(args.password.max, { message: tErrors('length', args.password) }) + .min(args.password.min, { message: tErrors('length', args.password) }); + const confirmPassword = z + .string() + .nonempty({ message: tErrors('required', args.confirmPassword) }) + .max(args.confirmPassword.max, { + message: tErrors('length', args.confirmPassword), + }) + .min(args.confirmPassword.min, { + message: tErrors('length', args.confirmPassword), + }); + + return { + password: password.refine(equality.sync), + confirmPassword: confirmPassword.refine( + equality.check, + tErrors('equal-passwords'), + ), + }; +}; From 38e22fe19ba68c801736f224ebb56914b4f1c6da Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Fri, 17 Apr 2026 23:43:45 +0300 Subject: [PATCH 03/19] Created a Multi-Step Form abstraction --- .../form/multi-step/hooks/useSteps.ts | 30 ++++++++ src/app/components/form/multi-step/index.tsx | 76 +++++++++++++++++++ .../components/form/multi-step/indicator.tsx | 29 +++++++ src/app/utils/multi-step.ts | 18 +++++ src/types/form.ts | 6 ++ 5 files changed, 159 insertions(+) create mode 100644 src/app/components/form/multi-step/hooks/useSteps.ts create mode 100644 src/app/components/form/multi-step/index.tsx create mode 100644 src/app/components/form/multi-step/indicator.tsx create mode 100644 src/app/utils/multi-step.ts create mode 100644 src/types/form.ts diff --git a/src/app/components/form/multi-step/hooks/useSteps.ts b/src/app/components/form/multi-step/hooks/useSteps.ts new file mode 100644 index 0000000..fefb9ca --- /dev/null +++ b/src/app/components/form/multi-step/hooks/useSteps.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; +import { type Step } from '@/types/form'; + +type Props = { + steps: Array>; + onNext?: (step: Step) => void; + onInvalid?: VoidFunction; + onPrev?: (step: Step) => void; +}; +export const useSteps = ({ steps, onNext, onPrev, onInvalid }: Props) => { + const [current, setCurrent] = useState(0); + + return { + current: steps[current], + move: { + next: () => { + if (steps[current].validate() && current < steps.length - 1) { + setCurrent((s) => s + 1); + onNext?.(steps[current + 1]); + } else onInvalid?.(); + }, + prev: () => { + if (current > 0) { + setCurrent((s) => s - 1); + onPrev?.(steps[current - 1]); + } + }, + }, + }; +}; diff --git a/src/app/components/form/multi-step/index.tsx b/src/app/components/form/multi-step/index.tsx new file mode 100644 index 0000000..2009033 --- /dev/null +++ b/src/app/components/form/multi-step/index.tsx @@ -0,0 +1,76 @@ +import { type Step } from '@/types/form'; +import { type Children } from '@/types/react'; +import { cn } from '@/lib/utils/tailwindcss'; +import { Button } from '@/app/components/ui/button'; +import Indicator from './indicator'; + +type Props = Children & { + steps: { all: Array>; current: Step }; + move: { + next: VoidFunction; + prev: VoidFunction; + }; + text?: { + back: string; + next: string; + submit: string; + }; + onSubmit: (e: React.FormEvent) => void; +}; +// eslint-disable-next-line func-style +function MultiStepForm({ children, steps, move, text, onSubmit }: Props) { + const is = { + first: steps.current.index === 0, + last: steps.current.index === steps.all.length - 1, + }; + + return ( +
+
+ {steps.all.map((step) => ( + + ))} +
+ + {children} + +
+ + + +
+
+ ); +} + +export default MultiStepForm; diff --git a/src/app/components/form/multi-step/indicator.tsx b/src/app/components/form/multi-step/indicator.tsx new file mode 100644 index 0000000..83740e7 --- /dev/null +++ b/src/app/components/form/multi-step/indicator.tsx @@ -0,0 +1,29 @@ +import { cn } from '@/lib/utils/tailwindcss'; + +type Props = { label: string; index: number; currentStep: number }; +const Indicator = ({ label, index, currentStep }: Props) => { + return ( +
+
+ {index + 1} +
+ {label} +
+ ); +}; + +export default Indicator; diff --git a/src/app/utils/multi-step.ts b/src/app/utils/multi-step.ts new file mode 100644 index 0000000..37a22a2 --- /dev/null +++ b/src/app/utils/multi-step.ts @@ -0,0 +1,18 @@ +import { Step } from '@/types/form'; + +const typedEntries = >(obj: T) => { + return Object.entries(obj) as { + [K in keyof T]: [K, T[K]]; + }[keyof T][]; +}; + +export const generateSteps = ( + params: Record }>, + hasErrors: (fields: Field[]) => boolean, +): Step[] => + typedEntries(params).map(([key, value], i) => ({ + index: i, + key: key as Key, + label: value.label, + validate: () => !hasErrors(value.fields), + })); diff --git a/src/types/form.ts b/src/types/form.ts new file mode 100644 index 0000000..6540ef8 --- /dev/null +++ b/src/types/form.ts @@ -0,0 +1,6 @@ +export type Step = { + index: number; + key: K; + label: string; + validate: () => boolean; +}; From b35199d52ce8890bb1f78d2620c5e1bf6af4892e Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Sat, 18 Apr 2026 11:06:44 +0300 Subject: [PATCH 04/19] Register preparations/small fixes --- src/app/components/error/index.tsx | 2 +- src/app/components/fields/error.tsx | 3 +- src/app/components/fields/input.tsx | 15 +++++++ src/app/components/fields/password.tsx | 9 ++-- src/app/components/fields/radio.tsx | 2 +- src/app/components/form/error.tsx | 6 +++ src/app/components/layout/footer/heading.tsx | 2 +- src/app/components/sso/google.tsx | 26 +++++++++++ src/app/hooks/stores/useLocaleSync.ts | 4 +- src/app/locales/i18n.ts | 3 +- .../guest/signin/login/hooks/useFields.tsx | 18 +++----- src/app/pages/guest/signin/login/index.tsx | 27 +++--------- src/index.css | 2 +- src/lib/utils/form.ts | 14 +++++- src/routes/__root.tsx | 43 ++++++++++++------- 15 files changed, 110 insertions(+), 66 deletions(-) create mode 100644 src/app/components/fields/input.tsx create mode 100644 src/app/components/form/error.tsx create mode 100644 src/app/components/sso/google.tsx diff --git a/src/app/components/error/index.tsx b/src/app/components/error/index.tsx index d621901..e2b7f26 100644 --- a/src/app/components/error/index.tsx +++ b/src/app/components/error/index.tsx @@ -27,7 +27,7 @@ const ErrorPage = ({ status, error }: ErrorPageProps) => { {status === 401 && (
{tError('login_link')} diff --git a/src/app/components/fields/error.tsx b/src/app/components/fields/error.tsx index a3c0fd1..07edcf0 100644 --- a/src/app/components/fields/error.tsx +++ b/src/app/components/fields/error.tsx @@ -15,9 +15,10 @@ const Error = ({ meta, isSubmitted, hideIfPristine }: Props) => { if (!(isSubmitted || shouldShowError)) return; + const [error] = meta.errors; return ( - {meta.errors[0].message} + {error.message ?? error[0].message} ); }; diff --git a/src/app/components/fields/input.tsx b/src/app/components/fields/input.tsx new file mode 100644 index 0000000..654ae10 --- /dev/null +++ b/src/app/components/fields/input.tsx @@ -0,0 +1,15 @@ +import { AnyFieldApi } from '@tanstack/react-form'; +import { Input } from '@/app/components/ui/input'; + +type Props = { api: AnyFieldApi } & React.ComponentProps; +const FormInput = ({ api, ...props }: Props) => ( + api.handleChange(value)} + onBlur={api.handleBlur} + {...props} + /> +); + +export default FormInput; diff --git a/src/app/components/fields/password.tsx b/src/app/components/fields/password.tsx index dfd050c..d6b4f8e 100644 --- a/src/app/components/fields/password.tsx +++ b/src/app/components/fields/password.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { AnyFieldApi } from '@tanstack/react-form'; import { Eye, EyeOff, LucideIcon } from 'lucide-react'; import { useFormTranslations } from '@/app/hooks/locales/translations/components'; -import { Input } from '@/app/components/ui/input'; +import FormInput from './input'; const ToggledIcon = ({ Icon }: { Icon: LucideIcon }) => ( @@ -17,12 +17,9 @@ const PasswordInput = ({ api, placeholder }: Props) => { return (
- api.handleChange(value)} - onBlur={api.handleBlur} placeholder={placeholder ?? tPlaceholders('password')} /> diff --git a/src/app/components/fields/radio.tsx b/src/app/components/fields/radio.tsx index 7ebf8e2..5cb2f57 100644 --- a/src/app/components/fields/radio.tsx +++ b/src/app/components/fields/radio.tsx @@ -3,7 +3,7 @@ import { Label } from '@/app/components/ui/label'; type Props = { value: string; label?: string }; const RadioField = ({ value, label }: Props) => ( -
+
diff --git a/src/app/pages/guest/signup/register/hooks/useForm.ts b/src/app/pages/guest/signup/register/hooks/useForm.ts index 604b8be..44763d3 100644 --- a/src/app/pages/guest/signup/register/hooks/useForm.ts +++ b/src/app/pages/guest/signup/register/hooks/useForm.ts @@ -11,8 +11,8 @@ import { useSignupTranslations } from '@/app/hooks/locales/translations/pages/gu const defaultValues: Data = { role: 'Customer', - firstName: '', - lastName: '', + firstName: undefined, + lastName: undefined, username: '', email: '', password: '', diff --git a/src/app/pages/guest/signup/register/steps/credentials/index.tsx b/src/app/pages/guest/signup/register/steps/credentials/index.tsx index c9fdc1c..c050c61 100644 --- a/src/app/pages/guest/signup/register/steps/credentials/index.tsx +++ b/src/app/pages/guest/signup/register/steps/credentials/index.tsx @@ -1,54 +1,25 @@ -import { useFormTranslations } from '@/app/hooks/locales/translations/components'; -import { Label } from '@/app/components/ui/label'; -import FormInput from '@/app/components/fields/input'; -import PasswordInput from '@/app/components/fields/password'; -import Error from '@/app/components/fields/error'; +import FormField from '@/app/components/fields/field'; import { Form } from '../../hooks/useForm'; type Props = { form: Form; isSubmitted: boolean }; -const Credentials = ({ form, isSubmitted }: Props) => { - const tLabels = useFormTranslations('labels'); - const tPlaceholders = useFormTranslations('placeholders'); - - return ( -
+const Credentials = ({ form, isSubmitted }: Props) => ( +
+
- {(api) => ( -
-
- -
- - -
- )} + {(api) => }
+
+
- {(api) => ( -
- - - -
- )} + {(api) => }
+
+
- {(api) => ( -
- - - -
- )} + {(api) => }
- ); -}; +
+); export default Credentials; diff --git a/src/app/pages/guest/signup/register/steps/profile/index.tsx b/src/app/pages/guest/signup/register/steps/profile/index.tsx index 1f947a9..d4310cf 100644 --- a/src/app/pages/guest/signup/register/steps/profile/index.tsx +++ b/src/app/pages/guest/signup/register/steps/profile/index.tsx @@ -1,69 +1,27 @@ -import { useFormTranslations } from '@/app/hooks/locales/translations/components'; -import Error from '@/app/components/fields/error'; -import { Label } from '@/app/components/ui/label'; -import FormInput from '@/app/components/fields/input'; +import FormField from '@/app/components/fields/field'; import { type Form } from '../../hooks/useForm'; type Props = { form: Form; isSubmitted: boolean }; -export const ProfileInfo = ({ form, isSubmitted }: Props) => { - const tLabels = useFormTranslations('labels'); - const tPlaceholders = useFormTranslations('placeholders'); - - return ( -
-
+export const ProfileInfo = ({ form, isSubmitted }: Props) => ( +
+
+
- {(api) => ( -
- - - -
- )} + {(api) => }
+
+
- {(api) => ( -
- - - -
- )} + {(api) => }
+
+
- {(api) => ( -
- - - -
- )} + {(api) => }
- ); -}; +
+); export default ProfileInfo; diff --git a/src/app/validators/schemas/password.ts b/src/app/validators/schemas/password.ts index 0ae426b..26d482d 100644 --- a/src/app/validators/schemas/password.ts +++ b/src/app/validators/schemas/password.ts @@ -11,7 +11,7 @@ export const schema = ({ tErrors, tLabels }: Props) => { max: VALIDATIONS.password.max, }, confirmPassword: { - field: tLabels('confirm-password'), + field: tLabels('confirmPassword'), min: VALIDATIONS.password.min, max: VALIDATIONS.password.max, }, diff --git a/src/lib/utils/form.ts b/src/lib/utils/form.ts index a2ee667..4963240 100644 --- a/src/lib/utils/form.ts +++ b/src/lib/utils/form.ts @@ -5,9 +5,13 @@ export const zodHelpers = { emptyOrLength: (args: { min: number; max: number }, error: string) => z .string() + .optional() .refine( - (x) => - x === '' || (x.length >= args.min && x.length <= args.max), + (x) => { + if (!x) return true; + + return x.length >= args.min && x.length <= args.max; + }, { error }, ), passwordEquality: < @@ -29,18 +33,8 @@ export const zodHelpers = { export const fileHelper = (file: File) => file.size > 0; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const extractError = (error: any) => { - const data = error?.response?.data; - - if (data?.detail) return data.detail as string; - if (data?.message) { - const errors = data?.errors as Record; - - return Object.entries(errors) - .map(([, y]) => y.join('; ')) - .join('\n'); - } -}; +export const extractError = (error: any) => + error?.response?.data?.detail as string; export const doFieldsHaveErrors = ( getErrors: (field: TKeys) => { errors: unknown[] }, From 584ff3a55d9d9babce0e52b08e466b33b0c99942 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Sat, 2 May 2026 03:56:57 +0300 Subject: [PATCH 15/19] Refactored Translation hooks and added Roles locales --- src/app/components/error/index.tsx | 4 +-- .../components/preferences/language/index.tsx | 4 +-- src/app/hooks/locales/translations/common.ts | 12 +++------ src/app/locales/bg-BG/common/roles.ts | 9 +++++++ src/app/locales/en-GB/common/roles.ts | 9 +++++++ src/app/locales/types/common.ts | 27 +++++++++++++++---- src/app/locales/types/index.ts | 10 +------ .../pages/public/editor/info/dimensions.tsx | 4 +-- src/app/pages/public/gallery/list.tsx | 9 +++---- src/app/pages/public/product/index.tsx | 2 +- 10 files changed, 54 insertions(+), 36 deletions(-) create mode 100644 src/app/locales/bg-BG/common/roles.ts create mode 100644 src/app/locales/en-GB/common/roles.ts diff --git a/src/app/components/error/index.tsx b/src/app/components/error/index.tsx index e2b7f26..e953807 100644 --- a/src/app/components/error/index.tsx +++ b/src/app/components/error/index.tsx @@ -1,7 +1,7 @@ import { Link } from '@tanstack/react-router'; import { AppErrorFields } from '@/types/errors'; import { cn } from '@/lib/utils/tailwindcss'; -import { useErrorsTranslations } from '@/app/hooks/locales/translations/common'; +import { useCommonTranslations } from '@/app/hooks/locales/translations/common'; import * as page from '@/app/utils/page'; type ErrorPageProps = { @@ -9,7 +9,7 @@ type ErrorPageProps = { error?: AppErrorFields; }; const ErrorPage = ({ status, error }: ErrorPageProps) => { - const tError = useErrorsTranslations(); + const tError = useCommonTranslations('errors'); const { title, message, tip } = error ?? { title: tError(`${status ?? 'default'}_title`), message: tError(`${status ?? 'default'}_message`), diff --git a/src/app/components/preferences/language/index.tsx b/src/app/components/preferences/language/index.tsx index e78420d..e27ef86 100644 --- a/src/app/components/preferences/language/index.tsx +++ b/src/app/components/preferences/language/index.tsx @@ -1,6 +1,6 @@ import { Globe } from 'lucide-react'; import { ALLOWED_LANGUAGES, FLAGS } from '@/types/locale'; -import { useLocalesTranslations } from '@/app/hooks/locales/translations/common'; +import { useCommonTranslations } from '@/app/hooks/locales/translations/common'; import { useLocaleStore } from '@/app/hooks/stores/useLocaleStore'; import * as localeStore from '@/app/stores/locale'; import CustomIcon from '@/app/components/icon'; @@ -9,7 +9,7 @@ import Compobox from './compobox'; type Props = { placeholder?: string }; export const LanguageMenu = ({ placeholder }: Props) => { const { language } = useLocaleStore(); - const tLocales = useLocalesTranslations(); + const tLocales = useCommonTranslations('locales'); return ( useTranslation('common.locales').t; - -export const useLoadingTranslations = () => useTranslation('common.loading').t; - -export const useEmptyTranslations = () => useTranslation('common.empty').t; - -export const useErrorsTranslations = () => useTranslation('common.errors').t; - -export const useMetricsTranslation = () => useTranslation('common.metrics').t; +type Common = 'locales' | 'roles' | 'loading' | 'empty' | 'errors' | 'metrics'; +export const useCommonTranslations = (ns: N) => + useTranslation(`common.${ns}`).t; diff --git a/src/app/locales/bg-BG/common/roles.ts b/src/app/locales/bg-BG/common/roles.ts new file mode 100644 index 0000000..8778372 --- /dev/null +++ b/src/app/locales/bg-BG/common/roles.ts @@ -0,0 +1,9 @@ +import { Roles } from '@/app/locales/types/common'; + +export default { + Customer: 'Купувач', + Contributor: 'Сътрудник', + Creator: 'Създател', + Designer: 'Дизайнер', + Administrator: 'Администратор', +} satisfies Roles; diff --git a/src/app/locales/en-GB/common/roles.ts b/src/app/locales/en-GB/common/roles.ts new file mode 100644 index 0000000..bbbe57d --- /dev/null +++ b/src/app/locales/en-GB/common/roles.ts @@ -0,0 +1,9 @@ +import { Roles } from '@/app/locales/types/common'; + +export default { + Customer: 'Customer', + Contributor: 'Contributor', + Creator: 'Creator', + Designer: 'Designer', + Administrator: 'Administrator', +} satisfies Roles; diff --git a/src/app/locales/types/common.ts b/src/app/locales/types/common.ts index 3415c15..26adbd0 100644 --- a/src/app/locales/types/common.ts +++ b/src/app/locales/types/common.ts @@ -1,23 +1,40 @@ import { Language } from '@/types/locale'; -export type Locales = Record; +export type Common = { + 'common.locales': Locales; + 'common.roles': Roles; + 'common.metrics': Metrics; + 'common.loading': Loading; + 'common.empty': Empty; + 'common.errors': Errors; +}; + +type Locales = Record; + +type Roles = { + Customer: string; + Contributor: string; + Creator: string; + Designer: string; + Administrator: string; +}; -export type Loading = { +type Loading = { gallery: string; }; -export type Empty = { +type Empty = { products: string; }; -export type Metrics = { +type Metrics = { width: string; height: string; length: string; volume: string; }; -export type Errors = { +type Errors = { login_link: string; contact_support_link: string; '400_title': string; diff --git a/src/app/locales/types/index.ts b/src/app/locales/types/index.ts index a861f12..4995a80 100644 --- a/src/app/locales/types/index.ts +++ b/src/app/locales/types/index.ts @@ -1,13 +1,5 @@ import { Pages } from './pages'; import { Components } from './components'; -import * as common from './common'; - -export type Common = { - 'common.locales': common.Locales; - 'common.metrics': common.Metrics; - 'common.loading': common.Loading; - 'common.empty': common.Empty; - 'common.errors': common.Errors; -}; +import { Common } from './common'; export type Translations = Pages & Components & Common; diff --git a/src/app/pages/public/editor/info/dimensions.tsx b/src/app/pages/public/editor/info/dimensions.tsx index 68b5368..b7e3849 100644 --- a/src/app/pages/public/editor/info/dimensions.tsx +++ b/src/app/pages/public/editor/info/dimensions.tsx @@ -1,12 +1,12 @@ import type { Distance } from '@/types/units'; import * as units from '@/lib/utils/units'; import { useEditorStore } from '@/app/hooks/stores/useEditorStore'; -import { useMetricsTranslation } from '@/app/hooks/locales/translations/common'; +import { useCommonTranslations } from '@/app/hooks/locales/translations/common'; import * as calculate3D from '@/app/utils/calculate-3D'; type Props = { id: string; volume: number; distance: Distance }; const DimensionsInfo = ({ id, volume, distance }: Props) => { - const tMetrics = useMetricsTranslation(); + const tMetrics = useCommonTranslations('metrics'); const scale = useEditorStore(id, (state) => state.scale); const size = useEditorStore(id, (state) => state.size); diff --git a/src/app/pages/public/gallery/list.tsx b/src/app/pages/public/gallery/list.tsx index 1d102ac..ff1ba75 100644 --- a/src/app/pages/public/gallery/list.tsx +++ b/src/app/pages/public/gallery/list.tsx @@ -1,15 +1,12 @@ import { ApiResult, GalleryAllProductsResponse } from '@customcads/react-sdk'; import { cn } from '@/lib/utils/tailwindcss'; -import { - useEmptyTranslations, - useLoadingTranslations, -} from '@/app/hooks/locales/translations/common'; +import { useCommonTranslations } from '@/app/hooks/locales/translations/common'; import Item from './item'; type Props = { products?: ApiResult }; const List = ({ products }: Props) => { - const tLoading = useLoadingTranslations(); - const tEmpty = useEmptyTranslations(); + const tLoading = useCommonTranslations('loading'); + const tEmpty = useCommonTranslations('empty'); if (!products) { return ( diff --git a/src/app/pages/public/product/index.tsx b/src/app/pages/public/product/index.tsx index 2c74c7e..d57316e 100644 --- a/src/app/pages/public/product/index.tsx +++ b/src/app/pages/public/product/index.tsx @@ -11,7 +11,7 @@ const Route = getRouteApi('/_public/gallery/$id'); const Product = () => { const loader = Route.useLoaderData(); const query = useQuery(({ products }) => - products.gallery.single({ id: loader.productId }), + products.gallery.single({ id: loader.productId, viewed: true }), ); const product = query.data ?? loader.product; From ee087c5c4ebc0efa4909cf4cf2bab368100b46d3 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Tue, 12 May 2026 11:56:40 +0300 Subject: [PATCH 16/19] Preparations for Account page panels --- package-lock.json | 8 ++-- package.json | 2 +- .../layout/header/account/authenticated.tsx | 4 +- .../header/mobile/account/authenticated.tsx | 4 +- src/app/hooks/features/header/useLogout.ts | 14 +++++-- .../pages/private/{common.ts => index.ts} | 2 +- .../signin/reset-password/hooks/useForm.ts | 2 +- src/app/utils/date-time.ts | 13 +++++- src/routeTree.gen.ts | 42 +++++++++---------- src/routes/_guest/reset-password.tsx | 12 ------ src/routes/_shared/reset-password.tsx | 26 ++++++++++++ 11 files changed, 79 insertions(+), 50 deletions(-) rename src/app/hooks/locales/translations/pages/private/{common.ts => index.ts} (66%) delete mode 100644 src/routes/_guest/reset-password.tsx create mode 100644 src/routes/_shared/reset-password.tsx diff --git a/package-lock.json b/package-lock.json index 611acdf..87ab498 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@cloudflare/vite-plugin": "^1.17.1", - "@customcads/react-sdk": "^1.13.1-beta.6", + "@customcads/react-sdk": "^1.13.1-beta.21", "@icons-pack/react-simple-icons": "^13.8.0", "@microsoft/signalr": "^9.0.6", "@radix-ui/react-checkbox": "^1.3.3", @@ -741,9 +741,9 @@ } }, "node_modules/@customcads/react-sdk": { - "version": "1.13.1-beta.6", - "resolved": "https://registry.npmjs.org/@customcads/react-sdk/-/react-sdk-1.13.1-beta.6.tgz", - "integrity": "sha512-qyO2g/d1I5JJ/2iKFcmuFGcCl+n5i4CBkpmdpLi7shhSiMomUKhGReqaIp6zEqti7WFswWXdvq27q3IpfT/9CQ==", + "version": "1.13.1-beta.21", + "resolved": "https://registry.npmjs.org/@customcads/react-sdk/-/react-sdk-1.13.1-beta.21.tgz", + "integrity": "sha512-/HNgoDwHIU9eTA3Oen/ETmJNyByw//S+kGVLPhCsc2EUdR2cgAcp6KPgD+q+mAL7y3HQF3xQOJz3pU8B7guKzg==", "dependencies": { "@tanstack/react-query": "^5.90.5", "axios": "^1.12.2", diff --git a/package.json b/package.json index 9cc9d8f..d4111c7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@cloudflare/vite-plugin": "^1.17.1", - "@customcads/react-sdk": "^1.13.1-beta.6", + "@customcads/react-sdk": "^1.13.1-beta.21", "@icons-pack/react-simple-icons": "^13.8.0", "@microsoft/signalr": "^9.0.6", "@radix-ui/react-checkbox": "^1.3.3", diff --git a/src/app/components/layout/header/account/authenticated.tsx b/src/app/components/layout/header/account/authenticated.tsx index fc8a469..0859649 100644 --- a/src/app/components/layout/header/account/authenticated.tsx +++ b/src/app/components/layout/header/account/authenticated.tsx @@ -5,14 +5,14 @@ import { useLogout } from '@/app/hooks/features/header/useLogout'; const Authenticated = () => { const tHeader = useLayoutTranslations('header'); - const handleLogout = useLogout(); + const { logout } = useLogout(); return [ , , ]; diff --git a/src/app/components/layout/header/mobile/account/authenticated.tsx b/src/app/components/layout/header/mobile/account/authenticated.tsx index 3c45701..e010871 100644 --- a/src/app/components/layout/header/mobile/account/authenticated.tsx +++ b/src/app/components/layout/header/mobile/account/authenticated.tsx @@ -6,7 +6,7 @@ import { SidebarMenuButton } from '@/app/components/ui/sidebar'; const Authenticated = () => { const tHeader = useLayoutTranslations('header'); - const handleLogout = useLogout(); + const { logout } = useLogout(); return [ @@ -18,7 +18,7 @@ const Authenticated = () => { className='text-lg hover:text-header-foreground' /> , - + { - const { actions } = useCartStore(); const { mutateAsync: logout } = useMutation( ({ identity }) => identity.logout, ); + const { actions } = useCartStore(); const authStore = useAuthStore(); - const handleLogout = async () => { - await logout(); + + const clear = () => { authStore.logout(); actions.cart.clear(); localeStore.resetStore(); }; - return handleLogout; + return { + logout: async () => { + await logout(); + clear(); + }, + clearSession: clear, + }; }; diff --git a/src/app/hooks/locales/translations/pages/private/common.ts b/src/app/hooks/locales/translations/pages/private/index.ts similarity index 66% rename from src/app/hooks/locales/translations/pages/private/common.ts rename to src/app/hooks/locales/translations/pages/private/index.ts index 97fa19d..c08cde3 100644 --- a/src/app/hooks/locales/translations/pages/private/common.ts +++ b/src/app/hooks/locales/translations/pages/private/index.ts @@ -1,5 +1,5 @@ import { useTranslation } from '../../../useTranslation'; type Common = 'account'; -export const useCommonTranslations = (ns: N) => +export const usePrivateTranslations = (ns: N) => useTranslation(`pages.private.common.${ns}`).t; diff --git a/src/app/pages/guest/signin/reset-password/hooks/useForm.ts b/src/app/pages/guest/signin/reset-password/hooks/useForm.ts index 201d286..9c95e60 100644 --- a/src/app/pages/guest/signin/reset-password/hooks/useForm.ts +++ b/src/app/pages/guest/signin/reset-password/hooks/useForm.ts @@ -7,7 +7,7 @@ import { useFormTranslations } from '@/app/hooks/locales/translations/components import { useForceLocaleRefresh } from '@/app/hooks/locales/useForceLocaleRefresh'; import { schema } from '@/app/validators/reset-password'; -const Route = getRouteApi('/_guest/reset-password'); +const Route = getRouteApi('/_shared/reset-password'); export const useForm = () => { const { email, token } = Route.useSearch(); diff --git a/src/app/utils/date-time.ts b/src/app/utils/date-time.ts index 2a25f7a..ea408a3 100644 --- a/src/app/utils/date-time.ts +++ b/src/app/utils/date-time.ts @@ -21,16 +21,18 @@ export const format = ({ second: dateOnly ? undefined : '2-digit', }); +type Interval = 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second'; type FormatRelativeOptions = { date: string; + limit?: Interval; }; -export const formatRelative = ({ date }: FormatRelativeOptions) => { +export const formatRelative = ({ date, limit }: FormatRelativeOptions) => { const now = new Date(); const seconds = Math.floor( (now.getTime() - new Date(date).getTime()) / 1000, ); - const intervals = [ + const intervals: { label: Interval | 'year'; seconds: number }[] = [ { label: 'year', seconds: 60 * 60 * 24 * 365 }, { label: 'month', seconds: 60 * 60 * 24 * 30 }, { label: 'week', seconds: 60 * 60 * 24 * 7 }, @@ -40,6 +42,13 @@ export const formatRelative = ({ date }: FormatRelativeOptions) => { { label: 'second', seconds: 1 }, ]; + if (limit) { + const limitInterval = intervals.find((i) => i.label === limit); + if (limitInterval && seconds < limitInterval.seconds) { + return `<1 ${limit} ago`; + } + } + for (const interval of intervals) { const count = Math.floor(seconds / interval.seconds); if (count === 1) { diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 7083127..f4843e7 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -12,9 +12,9 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as PrivateRouteImport } from './routes/_private' import { Route as GuestRouteImport } from './routes/_guest' import { Route as PublicIndexRouteImport } from './routes/_public/index' +import { Route as SharedResetPasswordRouteImport } from './routes/_shared/reset-password' import { Route as PublicCartRouteImport } from './routes/_public/cart' import { Route as PrivateAccountRouteImport } from './routes/_private/account' -import { Route as GuestResetPasswordRouteImport } from './routes/_guest/reset-password' import { Route as GuestRegisterRouteImport } from './routes/_guest/register' import { Route as GuestLoginRouteImport } from './routes/_guest/login' import { Route as GuestConfirmEmailRouteImport } from './routes/_guest/confirm-email' @@ -35,6 +35,11 @@ const PublicIndexRoute = PublicIndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const SharedResetPasswordRoute = SharedResetPasswordRouteImport.update({ + id: '/_shared/reset-password', + path: '/reset-password', + getParentRoute: () => rootRouteImport, +} as any) const PublicCartRoute = PublicCartRouteImport.update({ id: '/_public/cart', path: '/cart', @@ -45,11 +50,6 @@ const PrivateAccountRoute = PrivateAccountRouteImport.update({ path: '/account', getParentRoute: () => PrivateRoute, } as any) -const GuestResetPasswordRoute = GuestResetPasswordRouteImport.update({ - id: '/reset-password', - path: '/reset-password', - getParentRoute: () => GuestRoute, -} as any) const GuestRegisterRoute = GuestRegisterRouteImport.update({ id: '/register', path: '/register', @@ -85,9 +85,9 @@ export interface FileRoutesByFullPath { '/confirm-email': typeof GuestConfirmEmailRoute '/login': typeof GuestLoginRoute '/register': typeof GuestRegisterRoute - '/reset-password': typeof GuestResetPasswordRoute '/account': typeof PrivateAccountRoute '/cart': typeof PublicCartRoute + '/reset-password': typeof SharedResetPasswordRoute '/': typeof PublicIndexRoute '/editor/$id': typeof PublicEditorIdRoute '/gallery/$id': typeof PublicGalleryIdRoute @@ -97,9 +97,9 @@ export interface FileRoutesByTo { '/confirm-email': typeof GuestConfirmEmailRoute '/login': typeof GuestLoginRoute '/register': typeof GuestRegisterRoute - '/reset-password': typeof GuestResetPasswordRoute '/account': typeof PrivateAccountRoute '/cart': typeof PublicCartRoute + '/reset-password': typeof SharedResetPasswordRoute '/': typeof PublicIndexRoute '/editor/$id': typeof PublicEditorIdRoute '/gallery/$id': typeof PublicGalleryIdRoute @@ -112,9 +112,9 @@ export interface FileRoutesById { '/_guest/confirm-email': typeof GuestConfirmEmailRoute '/_guest/login': typeof GuestLoginRoute '/_guest/register': typeof GuestRegisterRoute - '/_guest/reset-password': typeof GuestResetPasswordRoute '/_private/account': typeof PrivateAccountRoute '/_public/cart': typeof PublicCartRoute + '/_shared/reset-password': typeof SharedResetPasswordRoute '/_public/': typeof PublicIndexRoute '/_public/editor/$id': typeof PublicEditorIdRoute '/_public/gallery/$id': typeof PublicGalleryIdRoute @@ -126,9 +126,9 @@ export interface FileRouteTypes { | '/confirm-email' | '/login' | '/register' - | '/reset-password' | '/account' | '/cart' + | '/reset-password' | '/' | '/editor/$id' | '/gallery/$id' @@ -138,9 +138,9 @@ export interface FileRouteTypes { | '/confirm-email' | '/login' | '/register' - | '/reset-password' | '/account' | '/cart' + | '/reset-password' | '/' | '/editor/$id' | '/gallery/$id' @@ -152,9 +152,9 @@ export interface FileRouteTypes { | '/_guest/confirm-email' | '/_guest/login' | '/_guest/register' - | '/_guest/reset-password' | '/_private/account' | '/_public/cart' + | '/_shared/reset-password' | '/_public/' | '/_public/editor/$id' | '/_public/gallery/$id' @@ -165,6 +165,7 @@ export interface RootRouteChildren { GuestRoute: typeof GuestRouteWithChildren PrivateRoute: typeof PrivateRouteWithChildren PublicCartRoute: typeof PublicCartRoute + SharedResetPasswordRoute: typeof SharedResetPasswordRoute PublicIndexRoute: typeof PublicIndexRoute PublicEditorIdRoute: typeof PublicEditorIdRoute PublicGalleryIdRoute: typeof PublicGalleryIdRoute @@ -194,6 +195,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PublicIndexRouteImport parentRoute: typeof rootRouteImport } + '/_shared/reset-password': { + id: '/_shared/reset-password' + path: '/reset-password' + fullPath: '/reset-password' + preLoaderRoute: typeof SharedResetPasswordRouteImport + parentRoute: typeof rootRouteImport + } '/_public/cart': { id: '/_public/cart' path: '/cart' @@ -208,13 +216,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PrivateAccountRouteImport parentRoute: typeof PrivateRoute } - '/_guest/reset-password': { - id: '/_guest/reset-password' - path: '/reset-password' - fullPath: '/reset-password' - preLoaderRoute: typeof GuestResetPasswordRouteImport - parentRoute: typeof GuestRoute - } '/_guest/register': { id: '/_guest/register' path: '/register' @@ -264,14 +265,12 @@ interface GuestRouteChildren { GuestConfirmEmailRoute: typeof GuestConfirmEmailRoute GuestLoginRoute: typeof GuestLoginRoute GuestRegisterRoute: typeof GuestRegisterRoute - GuestResetPasswordRoute: typeof GuestResetPasswordRoute } const GuestRouteChildren: GuestRouteChildren = { GuestConfirmEmailRoute: GuestConfirmEmailRoute, GuestLoginRoute: GuestLoginRoute, GuestRegisterRoute: GuestRegisterRoute, - GuestResetPasswordRoute: GuestResetPasswordRoute, } const GuestRouteWithChildren = GuestRoute._addFileChildren(GuestRouteChildren) @@ -291,6 +290,7 @@ const rootRouteChildren: RootRouteChildren = { GuestRoute: GuestRouteWithChildren, PrivateRoute: PrivateRouteWithChildren, PublicCartRoute: PublicCartRoute, + SharedResetPasswordRoute: SharedResetPasswordRoute, PublicIndexRoute: PublicIndexRoute, PublicEditorIdRoute: PublicEditorIdRoute, PublicGalleryIdRoute: PublicGalleryIdRoute, diff --git a/src/routes/_guest/reset-password.tsx b/src/routes/_guest/reset-password.tsx deleted file mode 100644 index d7b6cd1..0000000 --- a/src/routes/_guest/reset-password.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import z from 'zod'; -import { createFileRoute } from '@tanstack/react-router'; -import ResetPassword from '@/app/pages/guest/signin/reset-password'; - -export const Route = createFileRoute('/_guest/reset-password')({ - component: ResetPassword, - validateSearch: z.object({ - email: z.email(), - token: z.string(), - }), - head: () => ({ meta: [{ title: 'CustomCADs | Reset Password' }] }), -}); diff --git a/src/routes/_shared/reset-password.tsx b/src/routes/_shared/reset-password.tsx new file mode 100644 index 0000000..9344648 --- /dev/null +++ b/src/routes/_shared/reset-password.tsx @@ -0,0 +1,26 @@ +import z from 'zod'; +import { createFileRoute, redirect } from '@tanstack/react-router'; +import { identityApi } from '@customcads/react-sdk'; +import * as auth from '@/lib/utils/auth'; +import { get } from '@/lib/isomorphic/persistence'; +import ResetPassword from '@/app/pages/guest/signin/reset-password'; + +export const Route = createFileRoute('/_shared/reset-password')({ + validateSearch: z.object({ + email: z.email(), + token: z.string(), + }), + component: ResetPassword, + beforeLoad: async ({ search }) => { + try { + const { email } = search; + const { data: account } = await identityApi.myAccount(); + if (email === account.email) return; + } catch {} // user's probably unauthenticated + + const role = get('role'); + const is = auth.is({ authn: !!role, authz: role ?? null }); + if (!is.guest) throw redirect({ to: '/' }); + }, + head: () => ({ meta: [{ title: 'CustomCADs | Reset Password' }] }), +}); From 87e33c5fdd85e27283ccd0066bdf57733c7e6618 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Tue, 12 May 2026 12:00:11 +0300 Subject: [PATCH 17/19] Alert & Clipboard components --- package-lock.json | 47 +++++ package.json | 1 + src/app/components/clipboard.tsx | 55 ++++++ src/app/components/ui/alert-dialog.tsx | 170 ++++++++++++++++++ .../hooks/locales/translations/components.ts | 2 +- src/app/locales/bg-BG/components/layout/ui.ts | 6 + src/app/locales/en-GB/components/layout/ui.ts | 6 + src/app/locales/types/components/index.ts | 1 + src/app/locales/types/components/layout.ts | 5 + .../pages/private/common/account/index.tsx | 4 +- 10 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 src/app/components/clipboard.tsx create mode 100644 src/app/components/ui/alert-dialog.tsx create mode 100644 src/app/locales/bg-BG/components/layout/ui.ts create mode 100644 src/app/locales/en-GB/components/layout/ui.ts diff --git a/package-lock.json b/package-lock.json index 87ab498..a347f78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@customcads/react-sdk": "^1.13.1-beta.21", "@icons-pack/react-simple-icons": "^13.8.0", "@microsoft/signalr": "^9.0.6", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", @@ -2127,6 +2128,52 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", diff --git a/package.json b/package.json index d4111c7..5f9cff9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@customcads/react-sdk": "^1.13.1-beta.21", "@icons-pack/react-simple-icons": "^13.8.0", "@microsoft/signalr": "^9.0.6", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", diff --git a/src/app/components/clipboard.tsx b/src/app/components/clipboard.tsx new file mode 100644 index 0000000..2ce68cb --- /dev/null +++ b/src/app/components/clipboard.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react'; +import { ClipboardPen } from 'lucide-react'; +import { cn } from '@/lib/utils/tailwindcss'; +import { useLayoutTranslations } from '@/app/hooks/locales/translations/components'; +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; +import { Button } from './ui/button'; + +type Props = { value: string }; +const Clipboard = ({ value }: Props) => { + const tUi = useLayoutTranslations('ui'); + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(value); + + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 2000); + }; + + return ( + + + + + {tUi('clipboard-message')} + + + {tUi('clipboard-label')} + + ); +}; + +export default Clipboard; diff --git a/src/app/components/ui/alert-dialog.tsx b/src/app/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..acde087 --- /dev/null +++ b/src/app/components/ui/alert-dialog.tsx @@ -0,0 +1,170 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; +import { cn } from '@/lib/utils/tailwindcss'; +import { Button } from '@/app/components/ui/button'; + +export const AlertDialog = ({ + ...props +}: React.ComponentProps) => ( + +); + +export const AlertDialogTrigger = ({ + ...props +}: React.ComponentProps) => ( + +); + +export const AlertDialogPortal = ({ + ...props +}: React.ComponentProps) => ( + +); + +export const AlertDialogOverlay = ({ + className, + ...props +}: React.ComponentProps) => ( + +); + +export const AlertDialogContent = ({ + className, + size = 'default', + ...props +}: React.ComponentProps & { + size?: 'default' | 'sm'; +}) => ( + + + + +); + +export const AlertDialogHeader = ({ + className, + ...props +}: React.ComponentProps<'div'>) => ( +
+); + +export const AlertDialogFooter = ({ + className, + ...props +}: React.ComponentProps<'div'>) => ( +
+); + +export const AlertDialogTitle = ({ + className, + ...props +}: React.ComponentProps) => ( + +); + +export const AlertDialogDescription = ({ + className, + ...props +}: React.ComponentProps) => ( + +); + +export const AlertDialogMedia = ({ + className, + ...props +}: React.ComponentProps<'div'>) => ( +
+); + +export const AlertDialogAction = ({ + className, + variant = 'default', + size = 'default', + ...props +}: React.ComponentProps & + Pick, 'variant' | 'size'>) => ( + +); + +export const AlertDialogCancel = ({ + className, + variant = 'outline', + size = 'default', + ...props +}: React.ComponentProps & + Pick, 'variant' | 'size'>) => ( + +); + +export { + AlertDialog as Root, + AlertDialogAction as Action, + AlertDialogCancel as Cancel, + AlertDialogContent as Content, + AlertDialogDescription as Description, + AlertDialogFooter as Footer, + AlertDialogHeader as Header, + AlertDialogMedia as Media, + AlertDialogOverlay as Overlay, + AlertDialogPortal as Portal, + AlertDialogTitle as Title, + AlertDialogTrigger as Trigger, +}; diff --git a/src/app/hooks/locales/translations/components.ts b/src/app/hooks/locales/translations/components.ts index 29f2b57..2659e4d 100644 --- a/src/app/hooks/locales/translations/components.ts +++ b/src/app/hooks/locales/translations/components.ts @@ -1,6 +1,6 @@ import { useTranslation } from '../useTranslation'; -type Layout = 'footer' | 'header'; +type Layout = 'footer' | 'header' | 'ui'; export const useLayoutTranslations = (ns: N) => useTranslation(`components.layout.${ns}`).t; diff --git a/src/app/locales/bg-BG/components/layout/ui.ts b/src/app/locales/bg-BG/components/layout/ui.ts new file mode 100644 index 0000000..6583085 --- /dev/null +++ b/src/app/locales/bg-BG/components/layout/ui.ts @@ -0,0 +1,6 @@ +import { Ui } from '@/app/locales/types/components/layout'; + +export default { + 'clipboard-label': 'Копирай в Клипборда', + 'clipboard-message': 'Копирано', +} satisfies Ui; diff --git a/src/app/locales/en-GB/components/layout/ui.ts b/src/app/locales/en-GB/components/layout/ui.ts new file mode 100644 index 0000000..cd9be10 --- /dev/null +++ b/src/app/locales/en-GB/components/layout/ui.ts @@ -0,0 +1,6 @@ +import { Ui } from '@/app/locales/types/components/layout'; + +export default { + 'clipboard-label': 'Copy to Clipboard', + 'clipboard-message': 'Copied', +} satisfies Ui; diff --git a/src/app/locales/types/components/index.ts b/src/app/locales/types/components/index.ts index 185efdb..e2b5d64 100644 --- a/src/app/locales/types/components/index.ts +++ b/src/app/locales/types/components/index.ts @@ -4,6 +4,7 @@ import * as form from './form'; export type Components = { 'components.layout.header': layout.Header; 'components.layout.footer': layout.Footer; + 'components.layout.ui': layout.Ui; 'components.form.labels': form.Labels; 'components.form.placeholders': form.Placeholders; 'components.form.errors': form.Errors; diff --git a/src/app/locales/types/components/layout.ts b/src/app/locales/types/components/layout.ts index 75fdef1..e610b66 100644 --- a/src/app/locales/types/components/layout.ts +++ b/src/app/locales/types/components/layout.ts @@ -5,6 +5,11 @@ export type Footer = { 'customer-support': string; }; +export type Ui = { + 'clipboard-label': string; + 'clipboard-message': string; +}; + export type Header = { useful: string; 'services-info': string; diff --git a/src/app/pages/private/common/account/index.tsx b/src/app/pages/private/common/account/index.tsx index af28e08..dd1d294 100644 --- a/src/app/pages/private/common/account/index.tsx +++ b/src/app/pages/private/common/account/index.tsx @@ -1,7 +1,7 @@ import { getRouteApi } from '@tanstack/react-router'; import { useQuery } from '@customcads/react-sdk'; import { cn } from '@/lib/utils/tailwindcss'; -import { useCommonTranslations } from '@/app/hooks/locales/translations/pages/private/common'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; import Tabs from '@/app/components/tabs'; import * as page from '@/app/utils/page'; import { View, Edit, Data } from './panels/index'; @@ -19,7 +19,7 @@ const MyAccount = () => { const query = useQuery(({ identity }) => identity.myAccount); const account = query.data ?? loader.account; - const tAccount = useCommonTranslations('account'); + const tAccount = usePrivateTranslations('account'); const labels: Record = { view: tAccount('view'), From 91384e1e3c5c79a6b55e4f6b21843eb7789a2dd7 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Wed, 13 May 2026 12:55:01 +0300 Subject: [PATCH 18/19] Implemented Account page header/footer and panels --- src/app/components/tabs.tsx | 28 +++++-- .../translations/pages/private/index.ts | 6 +- .../pages/private/common/account.access.ts | 22 ++++++ .../pages/private/common/account.profile.ts | 14 ++++ .../pages/private/common/account.shell.ts | 15 ++++ .../bg-BG/pages/private/common/account.ts | 5 +- .../pages/private/common/account.access.ts | 22 ++++++ .../pages/private/common/account.profile.ts | 15 ++++ .../pages/private/common/account.shell.ts | 15 ++++ .../en-GB/pages/private/common/account.ts | 5 +- src/app/locales/types/pages/private/common.ts | 47 +++++++++++- src/app/locales/types/pages/private/index.ts | 3 + .../private/common/account/footer/delete.tsx | 54 ++++++++++++++ .../private/common/account/footer/index.tsx | 26 +++++++ .../private/common/account/footer/item.tsx | 11 +++ .../common/account/header/download.tsx | 40 ++++++++++ .../private/common/account/header/index.tsx | 24 ++++++ .../private/common/account/header/item.tsx | 11 +++ .../pages/private/common/account/index.tsx | 42 ++++++----- .../panels/access/fingerprints/index.tsx | 24 ++++++ .../panels/access/fingerprints/item.tsx | 21 ++++++ .../panels/access/fingerprints/password.tsx | 25 +++++++ .../panels/access/fingerprints/remove.tsx | 74 +++++++++++++++++++ .../common/account/panels/access/index.tsx | 10 +++ .../private/common/account/panels/data.tsx | 10 --- .../private/common/account/panels/edit.tsx | 10 --- .../private/common/account/panels/index.ts | 5 +- .../common/account/panels/profile/index.tsx | 22 ++++++ .../account/panels/profile/names/actions.tsx | 25 +++++++ .../panels/profile/names/hooks/useFields.tsx | 45 +++++++++++ .../panels/profile/names/hooks/useForm.ts | 45 +++++++++++ .../account/panels/profile/names/index.tsx | 64 ++++++++++++++++ .../account/panels/profile/names/toggle.tsx | 15 ++++ .../account/panels/profile/products/index.tsx | 24 ++++++ .../account/panels/profile/products/item.tsx | 41 ++++++++++ .../panels/profile/products/remove.tsx | 50 +++++++++++++ .../account/panels/profile/products/track.tsx | 42 +++++++++++ .../common/account/panels/scroller.tsx | 34 +++++++++ .../private/common/account/panels/view.tsx | 10 --- src/app/validators/profile.ts | 9 +++ src/index.css | 33 +++++++++ 41 files changed, 976 insertions(+), 67 deletions(-) create mode 100644 src/app/locales/bg-BG/pages/private/common/account.access.ts create mode 100644 src/app/locales/bg-BG/pages/private/common/account.profile.ts create mode 100644 src/app/locales/bg-BG/pages/private/common/account.shell.ts create mode 100644 src/app/locales/en-GB/pages/private/common/account.access.ts create mode 100644 src/app/locales/en-GB/pages/private/common/account.profile.ts create mode 100644 src/app/locales/en-GB/pages/private/common/account.shell.ts create mode 100644 src/app/pages/private/common/account/footer/delete.tsx create mode 100644 src/app/pages/private/common/account/footer/index.tsx create mode 100644 src/app/pages/private/common/account/footer/item.tsx create mode 100644 src/app/pages/private/common/account/header/download.tsx create mode 100644 src/app/pages/private/common/account/header/index.tsx create mode 100644 src/app/pages/private/common/account/header/item.tsx create mode 100644 src/app/pages/private/common/account/panels/access/fingerprints/index.tsx create mode 100644 src/app/pages/private/common/account/panels/access/fingerprints/item.tsx create mode 100644 src/app/pages/private/common/account/panels/access/fingerprints/password.tsx create mode 100644 src/app/pages/private/common/account/panels/access/fingerprints/remove.tsx create mode 100644 src/app/pages/private/common/account/panels/access/index.tsx delete mode 100644 src/app/pages/private/common/account/panels/data.tsx delete mode 100644 src/app/pages/private/common/account/panels/edit.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/index.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/names/actions.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/names/hooks/useFields.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/names/hooks/useForm.ts create mode 100644 src/app/pages/private/common/account/panels/profile/names/index.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/names/toggle.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/products/index.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/products/item.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/products/remove.tsx create mode 100644 src/app/pages/private/common/account/panels/profile/products/track.tsx create mode 100644 src/app/pages/private/common/account/panels/scroller.tsx delete mode 100644 src/app/pages/private/common/account/panels/view.tsx create mode 100644 src/app/validators/profile.ts diff --git a/src/app/components/tabs.tsx b/src/app/components/tabs.tsx index dc2f28d..12a8c54 100644 --- a/src/app/components/tabs.tsx +++ b/src/app/components/tabs.tsx @@ -1,27 +1,37 @@ import React, { useEffect, useState } from 'react'; import * as ui from '@/app/components/ui/tabs'; +import { Card } from './ui/card'; type Props = React.ComponentProps & { tabs: readonly { id: Tab; - label: string; + header: React.ReactNode; + ui: { + label: string; + panel: React.ReactNode; + }; + footer: React.ReactNode; className: string; - panel: React.ReactNode; }[]; defaultTab?: Tab; onTabChange?: (tab: Tab) => void; + tabVariant?: React.ComponentProps['variant']; }; // eslint-disable-next-line func-style function Tabs({ tabs, defaultTab, onTabChange, + tabVariant, ...props }: Props) { const [tab, setTab] = useState(defaultTab ?? tabs[0].id); useEffect(() => { - onTabChange?.(tab); + const timer = setTimeout(() => { + onTabChange?.(tab); + }, 1000); + return () => clearTimeout(timer); }, [tab]); return ( @@ -30,16 +40,20 @@ function Tabs({ onValueChange={(tab) => setTab(tab as Tab)} {...props} > - - {tabs.map(({ id, label, className }) => ( + + {tabs.map(({ id, ui: { label }, className }) => ( {label} ))} - {tabs.map(({ id, panel }) => ( + {tabs.map(({ id, header, ui: { panel }, footer }) => ( - {panel} + + {header} + {panel} + {footer} + ))} diff --git a/src/app/hooks/locales/translations/pages/private/index.ts b/src/app/hooks/locales/translations/pages/private/index.ts index c08cde3..258c67f 100644 --- a/src/app/hooks/locales/translations/pages/private/index.ts +++ b/src/app/hooks/locales/translations/pages/private/index.ts @@ -1,5 +1,9 @@ import { useTranslation } from '../../../useTranslation'; -type Common = 'account'; +type Common = + | 'account' + | 'account.shell' + | 'account.profile' + | 'account.access'; export const usePrivateTranslations = (ns: N) => useTranslation(`pages.private.common.${ns}`).t; diff --git a/src/app/locales/bg-BG/pages/private/common/account.access.ts b/src/app/locales/bg-BG/pages/private/common/account.access.ts new file mode 100644 index 0000000..831afc9 --- /dev/null +++ b/src/app/locales/bg-BG/pages/private/common/account.access.ts @@ -0,0 +1,22 @@ +import { MyAccountAccess } from '@/app/locales/types/pages/private/common'; + +export default { + logins: 'Устройства', + logins_one: 'Устройство (само едно)', + logins_other: 'Устройства ({{count}})', + 'warn-btn': 'Излез', + 'warn-title': 'Това е текущото Ви устройство', + 'warn-description': 'Не можете сами да се изритате.', + logout: 'Излез', + understood: 'Разбрах', + 'remove-btn': 'Изритай', + 'remove-title': 'Напълно ли сте сигурен?', + 'remove-description': + 'Това устройство ще забрави сесията си за под 5 минути.', + cancel: 'Отмени', + continue: 'Изритай', + 'reset-password-idle': 'Смяна на Паролата', + 'reset-password-pending': 'Изпращане на имейл...', + 'reset-password-success': 'Имейл Изпратен!', + 'reset-password-error': 'Не успяхме да изпратим имейл!', +} satisfies MyAccountAccess & { logins_one: string; logins_other: string }; diff --git a/src/app/locales/bg-BG/pages/private/common/account.profile.ts b/src/app/locales/bg-BG/pages/private/common/account.profile.ts new file mode 100644 index 0000000..3222c19 --- /dev/null +++ b/src/app/locales/bg-BG/pages/private/common/account.profile.ts @@ -0,0 +1,14 @@ +import { MyAccountProfile } from '@/app/locales/types/pages/private/common'; + +export default { + names: 'Твоите Имена', + edit: 'Редактирай', + cancel: 'Отмени', + save: 'Запази', + continue: 'Продължи', + 'viewed-products': 'Прегледани Продукти', + 'remove-btn': 'Премахни', + 'remove-title': 'Напълно ли сте сигурни?', + 'remove-description': 'Ще изгубите този Продукт от историята си завинаги.', + 'track-products': 'Проследяване на прегледани Продукти?', +} satisfies MyAccountProfile; diff --git a/src/app/locales/bg-BG/pages/private/common/account.shell.ts b/src/app/locales/bg-BG/pages/private/common/account.shell.ts new file mode 100644 index 0000000..8af19c6 --- /dev/null +++ b/src/app/locales/bg-BG/pages/private/common/account.shell.ts @@ -0,0 +1,15 @@ +import { MyAccountShell } from '@/app/locales/types/pages/private/common'; + +export default { + role: 'Роля:', + download: 'Изтегли Данните си', + email: 'Имейл:', + joined: 'Кога си се присъединил?', + 'delete-btn': 'Изтрйи този Акаунт', + 'delete-title': 'Напълно ли сте сигурни?', + 'delete-description': + 'Това действие и следователната загуба на данни са необратими.', + cancel: 'Отемни', + continue: 'Потвърждавам Премахването', + id: 'Потребителско ID', +} satisfies MyAccountShell; diff --git a/src/app/locales/bg-BG/pages/private/common/account.ts b/src/app/locales/bg-BG/pages/private/common/account.ts index 6ce8523..63e64c1 100644 --- a/src/app/locales/bg-BG/pages/private/common/account.ts +++ b/src/app/locales/bg-BG/pages/private/common/account.ts @@ -2,7 +2,6 @@ import { MyAccount } from '@/app/locales/types/pages/private/common'; export default { title: 'Акаунт | {{username}}', - view: 'Разгледай Профил', - edit: 'Редактирай Акаунт', - data: 'Твоите Данни', + profile: 'Профил', + access: 'Достъп', } satisfies MyAccount; diff --git a/src/app/locales/en-GB/pages/private/common/account.access.ts b/src/app/locales/en-GB/pages/private/common/account.access.ts new file mode 100644 index 0000000..1bc4c3c --- /dev/null +++ b/src/app/locales/en-GB/pages/private/common/account.access.ts @@ -0,0 +1,22 @@ +import { MyAccountAccess } from '@/app/locales/types/pages/private/common'; + +export default { + logins: 'Logged Devices', + logins_one: 'Logged Device (only one)', + logins_other: 'Logged Devices ({{count}})', + 'warn-btn': 'Logout', + 'warn-title': 'This is your current Device', + 'warn-description': "You can't kick yourself out.", + logout: 'Log Out', + understood: 'Understood', + 'remove-btn': 'Kick out', + 'remove-title': 'Are you absolutely sure?', + 'remove-description': + 'This device will have its session erased in under 5 minutes.', + cancel: 'Cancel', + continue: 'Kick', + 'reset-password-idle': 'Reset Password', + 'reset-password-pending': 'Sending email...', + 'reset-password-success': 'Email Sent!', + 'reset-password-error': "Couldn't send email!", +} satisfies MyAccountAccess & { logins_one: string; logins_other: string }; diff --git a/src/app/locales/en-GB/pages/private/common/account.profile.ts b/src/app/locales/en-GB/pages/private/common/account.profile.ts new file mode 100644 index 0000000..c3a336f --- /dev/null +++ b/src/app/locales/en-GB/pages/private/common/account.profile.ts @@ -0,0 +1,15 @@ +import { MyAccountProfile } from '@/app/locales/types/pages/private/common'; + +export default { + names: 'Your Names', + edit: 'Edit', + cancel: 'Cancel', + save: 'Save', + continue: 'Continue', + 'viewed-products': 'Viewed Products', + 'remove-btn': 'Remove', + 'remove-title': 'Are you absolutely sure?', + 'remove-description': + "You'll permanently lose this Product from your history.", + 'track-products': 'Track Products you view?', +} satisfies MyAccountProfile; diff --git a/src/app/locales/en-GB/pages/private/common/account.shell.ts b/src/app/locales/en-GB/pages/private/common/account.shell.ts new file mode 100644 index 0000000..1ab28a5 --- /dev/null +++ b/src/app/locales/en-GB/pages/private/common/account.shell.ts @@ -0,0 +1,15 @@ +import { MyAccountShell } from '@/app/locales/types/pages/private/common'; + +export default { + role: 'Role:', + download: 'Download your Data', + email: 'Email:', + joined: 'When did you join?', + 'delete-btn': 'Delete this Account', + 'delete-title': 'Are you absolutely sure?', + 'delete-description': + 'This action and the resuling loss of data are irrevirsible.', + cancel: 'Cancel', + continue: 'Confirm Deletion', + id: 'User ID', +} satisfies MyAccountShell; diff --git a/src/app/locales/en-GB/pages/private/common/account.ts b/src/app/locales/en-GB/pages/private/common/account.ts index 5161c66..d488779 100644 --- a/src/app/locales/en-GB/pages/private/common/account.ts +++ b/src/app/locales/en-GB/pages/private/common/account.ts @@ -2,7 +2,6 @@ import { MyAccount } from '@/app/locales/types/pages/private/common'; export default { title: 'Account | {{username}}', - view: 'View Profile', - edit: 'Edit Account', - data: 'Your Data', + profile: 'Profile', + access: 'Access', } satisfies MyAccount; diff --git a/src/app/locales/types/pages/private/common.ts b/src/app/locales/types/pages/private/common.ts index 6268512..65f942e 100644 --- a/src/app/locales/types/pages/private/common.ts +++ b/src/app/locales/types/pages/private/common.ts @@ -1,6 +1,49 @@ export type MyAccount = { title: string; - view: string; + profile: string; + access: string; +}; + +export type MyAccountShell = { + role: string; + email: string; + cancel: string; + continue: string; + download: string; + 'delete-btn': string; + 'delete-title': string; + 'delete-description': string; + joined: string; + id: string; +}; + +export type MyAccountProfile = { + names: string; edit: string; - data: string; + save: string; + 'viewed-products': string; + 'track-products': string; + 'remove-btn': string; + 'remove-title': string; + 'remove-description': string; + cancel: string; + continue: string; +}; + +export type MyAccountAccess = { + logins: string; + 'warn-btn': string; + 'warn-title': string; + 'warn-description': string; + logout: string; + understood: string; + 'remove-btn': string; + 'remove-title': string; + 'remove-description': string; + cancel: string; + continue: string; + 'reset-password-pending': string; + 'reset-password-success': string; + 'reset-password-error': string; + 'reset-password-idle': string; }; diff --git a/src/app/locales/types/pages/private/index.ts b/src/app/locales/types/pages/private/index.ts index 635e138..80d13d8 100644 --- a/src/app/locales/types/pages/private/index.ts +++ b/src/app/locales/types/pages/private/index.ts @@ -2,4 +2,7 @@ import * as common from './common'; export type Private = { 'pages.private.common.account': common.MyAccount; + 'pages.private.common.account.shell': common.MyAccountShell; + 'pages.private.common.account.profile': common.MyAccountProfile; + 'pages.private.common.account.access': common.MyAccountAccess; }; diff --git a/src/app/pages/private/common/account/footer/delete.tsx b/src/app/pages/private/common/account/footer/delete.tsx new file mode 100644 index 0000000..45dcf95 --- /dev/null +++ b/src/app/pages/private/common/account/footer/delete.tsx @@ -0,0 +1,54 @@ +import { useNavigate } from '@tanstack/react-router'; +import { useMutation } from '@customcads/react-sdk'; +import { UserX } from 'lucide-react'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { useLogout } from '@/app/hooks/features/header/useLogout'; +import * as dialog from '@/app/components/ui/alert-dialog'; +import { Button } from '@/app/components/ui/button'; + +const Delete = () => { + const { mutateAsync: deleteMyAccount } = useMutation( + ({ identity }) => identity.deleteMyAccount, + ); + const { clearSession } = useLogout(); + + const navigate = useNavigate(); + const handleDelete = async () => { + await deleteMyAccount(); + clearSession(); + navigate({ to: '.' }); + }; + + const tShell = usePrivateTranslations('account.shell'); + + return ( +
+ + + + + + + {tShell('delete-title')} + + {tShell('delete-description')} + + + + {tShell('cancel')} + + {tShell('continue')} + + + + +
+ ); +}; + +export default Delete; diff --git a/src/app/pages/private/common/account/footer/index.tsx b/src/app/pages/private/common/account/footer/index.tsx new file mode 100644 index 0000000..e01bb23 --- /dev/null +++ b/src/app/pages/private/common/account/footer/index.tsx @@ -0,0 +1,26 @@ +import { type MyAccountResponse } from '@customcads/react-sdk'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { CardFooter } from '@/app/components/ui/card'; +import Clipboard from '@/app/components/clipboard'; +import { format } from '@/app/utils/date-time'; +import Delete from './delete'; +import Item from './item'; + +type Props = Pick; +const Footer = ({ id, createdAt }: Props) => { + const tShell = usePrivateTranslations('account.shell'); + + return ( + + {format({ date: createdAt })} +
+ +
+ + + +
+ ); +}; + +export default Footer; diff --git a/src/app/pages/private/common/account/footer/item.tsx b/src/app/pages/private/common/account/footer/item.tsx new file mode 100644 index 0000000..d7c604a --- /dev/null +++ b/src/app/pages/private/common/account/footer/item.tsx @@ -0,0 +1,11 @@ +import { Children } from '@/types/react'; + +type Props = Children & { label: string }; +const Item = ({ label, children }: Props) => ( +
+ {label} + {children} +
+); + +export default Item; diff --git a/src/app/pages/private/common/account/header/download.tsx b/src/app/pages/private/common/account/header/download.tsx new file mode 100644 index 0000000..93e1300 --- /dev/null +++ b/src/app/pages/private/common/account/header/download.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react'; +import { SquareUser } from 'lucide-react'; +import { identityApi } from '@customcads/react-sdk'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { Button } from '@/app/components/ui/button'; + +const Download = () => { + const [url, setUrl] = useState(null); + + useEffect(() => { + identityApi.downloadInfo().then(({ config }) => { + if (config.url) { + setUrl(config.baseURL + config.url); + } + }); + + return () => { + if (url) URL.revokeObjectURL(url); + }; + }, [url]); + + const tShell = usePrivateTranslations('account.shell'); + + return ( + + ); +}; + +export default Download; diff --git a/src/app/pages/private/common/account/header/index.tsx b/src/app/pages/private/common/account/header/index.tsx new file mode 100644 index 0000000..7409e88 --- /dev/null +++ b/src/app/pages/private/common/account/header/index.tsx @@ -0,0 +1,24 @@ +import { type MyAccountResponse } from '@customcads/react-sdk'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { useCommonTranslations } from '@/app/hooks/locales/translations/common'; +import { CardHeader } from '@/app/components/ui/card'; +import Download from './download'; +import Item from './item'; + +type Props = Pick; +const Header = ({ role, email }: Props) => { + const tShell = usePrivateTranslations('account.shell'); + const tRoles = useCommonTranslations('roles'); + + return ( + + {tRoles(role as never)} +
+ +
+ {email} +
+ ); +}; + +export default Header; diff --git a/src/app/pages/private/common/account/header/item.tsx b/src/app/pages/private/common/account/header/item.tsx new file mode 100644 index 0000000..b26f669 --- /dev/null +++ b/src/app/pages/private/common/account/header/item.tsx @@ -0,0 +1,11 @@ +import { Children } from '@/types/react'; + +type Props = Children & { label: string }; +const Item = ({ label, children }: Props) => ( +
+ {label} +

{children}

+
+); + +export default Item; diff --git a/src/app/pages/private/common/account/index.tsx b/src/app/pages/private/common/account/index.tsx index dd1d294..f964250 100644 --- a/src/app/pages/private/common/account/index.tsx +++ b/src/app/pages/private/common/account/index.tsx @@ -4,9 +4,11 @@ import { cn } from '@/lib/utils/tailwindcss'; import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; import Tabs from '@/app/components/tabs'; import * as page from '@/app/utils/page'; -import { View, Edit, Data } from './panels/index'; +import Header from './header'; +import { Profile, Access } from './panels'; +import Footer from './footer'; -export const tabs = ['view', 'edit', 'data'] as const; +export const tabs = ['profile', 'access'] as const; export type Tab = (typeof tabs)[number]; const Route = getRouteApi('/_private/account'); @@ -21,33 +23,37 @@ const MyAccount = () => { const account = query.data ?? loader.account; const tAccount = usePrivateTranslations('account'); - const labels: Record = { - view: tAccount('view'), - edit: tAccount('edit'), - data: tAccount('data'), - }; - const panels: Record = { - view: , - edit: , - data: , + const tabsUI: Record = { + profile: { + label: tAccount('profile'), + panel: , + }, + access: { + label: tAccount('access'), + panel: , + }, }; return ( -
+

{tAccount('title', { username: account.username })}

({ id: tab, - label: labels[tab], - className: 'inline text-xl md:px-20 min-h-20 md:min-h-50', - panel: panels[tab], + ui: tabsUI[tab], + className: 'text-xl', + header:
, + footer:
, }))} defaultTab={search.tab} - onTabChange={(tab) => navigate({ search: { tab } })} - orientation='vertical' - className='w-full flex flex-col md:flex-row gap-y-4' + onTabChange={(tab) => + tab !== search.tab && navigate({ search: { tab } }) + } + orientation='horizontal' + tabVariant='line' + className='w-full md:min-h-[83vh]' />
); diff --git a/src/app/pages/private/common/account/panels/access/fingerprints/index.tsx b/src/app/pages/private/common/account/panels/access/fingerprints/index.tsx new file mode 100644 index 0000000..ae1960c --- /dev/null +++ b/src/app/pages/private/common/account/panels/access/fingerprints/index.tsx @@ -0,0 +1,24 @@ +import { Fingerprint } from '@customcads/react-sdk'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import Scroller from '../../scroller'; +import Item from './item'; +import Password from './password'; + +type Props = { email: string; fingerprints: Fingerprint[] }; +const Fingerprints = ({ email, fingerprints }: Props) => { + const tAccess = usePrivateTranslations('account.access'); + + return ( + ( + + ))} + className='mb-4 lg:w-175 xl:w-250' + > + + + ); +}; + +export default Fingerprints; diff --git a/src/app/pages/private/common/account/panels/access/fingerprints/item.tsx b/src/app/pages/private/common/account/panels/access/fingerprints/item.tsx new file mode 100644 index 0000000..6fe2714 --- /dev/null +++ b/src/app/pages/private/common/account/panels/access/fingerprints/item.tsx @@ -0,0 +1,21 @@ +import { type Fingerprint } from '@customcads/react-sdk'; +import * as dateTime from '@/app/utils/date-time'; +import Remove from './remove'; + +type Props = { fingerprint: Fingerprint }; +const Item = ({ fingerprint }: Props) => ( +
+

+ {fingerprint.device} + + {dateTime.formatRelative({ + date: fingerprint.issuedAt, + limit: 'hour', + })} + +

+ +
+); + +export default Item; diff --git a/src/app/pages/private/common/account/panels/access/fingerprints/password.tsx b/src/app/pages/private/common/account/panels/access/fingerprints/password.tsx new file mode 100644 index 0000000..da2c447 --- /dev/null +++ b/src/app/pages/private/common/account/panels/access/fingerprints/password.tsx @@ -0,0 +1,25 @@ +import { useMutation } from '@customcads/react-sdk'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { Button } from '@/app/components/ui/button'; + +type Props = { email: string }; +const Password = ({ email }: Props) => { + const { mutateAsync: sendEmail, status } = useMutation( + ({ identity }) => identity.forgotPassword, + ); + const tAccess = usePrivateTranslations('account.access'); + + return ( +
+ +
+ ); +}; + +export default Password; diff --git a/src/app/pages/private/common/account/panels/access/fingerprints/remove.tsx b/src/app/pages/private/common/account/panels/access/fingerprints/remove.tsx new file mode 100644 index 0000000..2400400 --- /dev/null +++ b/src/app/pages/private/common/account/panels/access/fingerprints/remove.tsx @@ -0,0 +1,74 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { queryCall, useMutation } from '@customcads/react-sdk'; +import { ShieldUser, X } from 'lucide-react'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { useLogout } from '@/app/hooks/features/header/useLogout'; +import { Button } from '@/app/components/ui/button'; +import * as ui from '@/app/components/ui/alert-dialog'; + +type Props = { id: string; allow: boolean }; +const Remove = ({ id, allow }: Props) => { + const queryClient = useQueryClient(); + const mutation = useMutation(({ identity }) => identity.deleteFingerprint); + + const handleDelete = async () => { + await mutation.mutateAsync({ refreshTokenId: id }); + await queryCall( + ({ identity }) => identity.myAccount, + (opts) => queryClient.invalidateQueries(opts), + ); + }; + const { logout } = useLogout(); + + const tAccess = usePrivateTranslations('account.access'); + const alertMode = allow ? 'remove' : 'warn'; + + return ( + + + {allow ? ( + + ) : ( + + )} + + + + {tAccess(`${alertMode}-title`)} + + {tAccess(`${alertMode}-description`)} + + + + {allow ? ( + <> + {tAccess('cancel')} + + {tAccess('continue')} + + + ) : ( + <> + {tAccess('understood')} + + {tAccess('logout')} + + + )} + + + + ); +}; + +export default Remove; diff --git a/src/app/pages/private/common/account/panels/access/index.tsx b/src/app/pages/private/common/account/panels/access/index.tsx new file mode 100644 index 0000000..4306a63 --- /dev/null +++ b/src/app/pages/private/common/account/panels/access/index.tsx @@ -0,0 +1,10 @@ +import { MyAccountResponse } from '@customcads/react-sdk'; +import { CardContent } from '@/app/components/ui/card'; +import Fingerprints from './fingerprints'; + +type Props = { account: MyAccountResponse }; +export const Access = ({ account }: Props) => ( + + + +); diff --git a/src/app/pages/private/common/account/panels/data.tsx b/src/app/pages/private/common/account/panels/data.tsx deleted file mode 100644 index 976d8da..0000000 --- a/src/app/pages/private/common/account/panels/data.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { - Card, - CardContent, - CardFooter, - CardHeader, -} from '@/app/components/ui/card'; - -export const Data = () => { - return ; -}; diff --git a/src/app/pages/private/common/account/panels/edit.tsx b/src/app/pages/private/common/account/panels/edit.tsx deleted file mode 100644 index 4d96526..0000000 --- a/src/app/pages/private/common/account/panels/edit.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { - Card, - CardContent, - CardFooter, - CardHeader, -} from '@/app/components/ui/card'; - -export const Edit = () => { - return ; -}; diff --git a/src/app/pages/private/common/account/panels/index.ts b/src/app/pages/private/common/account/panels/index.ts index 66885ff..78ee05d 100644 --- a/src/app/pages/private/common/account/panels/index.ts +++ b/src/app/pages/private/common/account/panels/index.ts @@ -1,3 +1,2 @@ -export * from './view'; -export * from './edit'; -export * from './data'; +export * from './profile'; +export * from './access'; diff --git a/src/app/pages/private/common/account/panels/profile/index.tsx b/src/app/pages/private/common/account/panels/profile/index.tsx new file mode 100644 index 0000000..0bce427 --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/index.tsx @@ -0,0 +1,22 @@ +import { type MyAccountResponse } from '@customcads/react-sdk'; +import { CardContent } from '@/app/components/ui/card'; +import { Separator } from '@/app/components/ui/separator'; +import Names from './names'; +import Products from './products'; + +type Props = { account: MyAccountResponse }; +export const Profile = ({ account }: Props) => ( + + + + + + +); diff --git a/src/app/pages/private/common/account/panels/profile/names/actions.tsx b/src/app/pages/private/common/account/panels/profile/names/actions.tsx new file mode 100644 index 0000000..3099e26 --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/names/actions.tsx @@ -0,0 +1,25 @@ +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { Error } from '@/app/components/form/error'; +import { Button } from '@/app/components/ui/button'; + +type Props = { + error: string; + onCancel: (e: React.MouseEvent) => void; +}; +const Actions = ({ error, onCancel }: Props) => { + const tProfile = usePrivateTranslations('account.profile'); + + return ( +
+ {error && } +
+ + +
+
+ ); +}; + +export default Actions; diff --git a/src/app/pages/private/common/account/panels/profile/names/hooks/useFields.tsx b/src/app/pages/private/common/account/panels/profile/names/hooks/useFields.tsx new file mode 100644 index 0000000..9e0fda2 --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/names/hooks/useFields.tsx @@ -0,0 +1,45 @@ +import FormField from '@/app/components/fields/field'; +import { Data } from '@/app/validators/profile'; +import { useForm } from './useForm'; + +export const useFields = (defaultData: Data, canEdit: boolean) => { + const { form, isSubmitted, ...rest } = useForm(defaultData); + + const fields = { + Username: () => ( + + {(api) => ( + + )} + + ), + FirstName: () => ( + + {(api) => ( + + )} + + ), + LastName: () => ( + + {(api) => ( + + )} + + ), + }; + + return { fields, ...rest, reset: form.reset }; +}; diff --git a/src/app/pages/private/common/account/panels/profile/names/hooks/useForm.ts b/src/app/pages/private/common/account/panels/profile/names/hooks/useForm.ts new file mode 100644 index 0000000..4c8ad10 --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/names/hooks/useForm.ts @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { useForm as useTanStackForm } from '@tanstack/react-form'; +import { useQueryClient } from '@tanstack/react-query'; +import { queryCall, useMutation } from '@customcads/react-sdk'; +import * as form from '@/lib/utils/form'; +import { useFormTranslations } from '@/app/hooks/locales/translations/components'; +import { useForceLocaleRefresh } from '@/app/hooks/locales/useForceLocaleRefresh'; +import * as profile from '@/app/validators/profile'; + +export const useForm = (defaultValues: profile.Data) => { + const [isSubmitted, setIsSubmitted] = useState(false); + const queryClient = useQueryClient(); + + const { mutateAsync: changeNames, ...mutation } = useMutation( + ({ identity }) => identity.changeNames, + ); + + const tErrors = useFormTranslations('errors'); + const tLabels = useFormTranslations('labels'); + + const formApi = useTanStackForm({ + defaultValues, + onSubmit: async ({ value }) => { + await changeNames(value); + + await queryCall( + ({ identity }) => identity.myAccount, + (opts) => queryClient.invalidateQueries(opts), + ); + }, + validators: { onChange: profile.schema({ tErrors, tLabels }) }, + }); + useForceLocaleRefresh(() => formApi.validate('change')); + + return { + form: formApi, + error: form.extractError(mutation.error), + handleSubmit: (e: React.FormEvent) => { + setIsSubmitted(true); + return form.handleSubmit(e, formApi.handleSubmit); + }, + isSubmitted, + isSuccess: mutation.isSuccess, + }; +}; diff --git a/src/app/pages/private/common/account/panels/profile/names/index.tsx b/src/app/pages/private/common/account/panels/profile/names/index.tsx new file mode 100644 index 0000000..4bc4318 --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/names/index.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react'; +import { cn } from '@/lib/utils/tailwindcss'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { useFields } from './hooks/useFields'; +import Actions from './actions'; +import ToggleEdit from './toggle'; + +type Props = { username: string; firstName?: string; lastName?: string }; +const Names = ({ username, firstName, lastName }: Props) => { + const [mode, setMode] = useState<'view' | 'edit'>('view'); + const form = useFields({ username, firstName, lastName }, mode === 'edit'); + + const handle = { + submit: async (e: React.FormEvent) => { + await form.handleSubmit(e); + setMode('view'); + }, + cancel: (e: React.MouseEvent) => { + e.preventDefault(); + form.reset(); + setMode('view'); + }, + }; + const tProfile = usePrivateTranslations('account.profile'); + + return ( +
+
+

+ {tProfile('names')} +

+
+
+
+ +
+
+ +
+
+ +
+
+ {mode === 'view' ? ( + setMode('edit')}> + + {tProfile('edit')} + + + ) : ( + + )} +
+
+
+ ); +}; + +export default Names; diff --git a/src/app/pages/private/common/account/panels/profile/names/toggle.tsx b/src/app/pages/private/common/account/panels/profile/names/toggle.tsx new file mode 100644 index 0000000..01cc89a --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/names/toggle.tsx @@ -0,0 +1,15 @@ +import { Edit } from 'lucide-react'; +import { Children } from '@/types/react'; +import { Button } from '@/app/components/ui/button'; + +type Props = Children & { onClick: VoidFunction }; +const ToggleEdit = ({ children, onClick }: Props) => ( +
+ +
+); + +export default ToggleEdit; diff --git a/src/app/pages/private/common/account/panels/profile/products/index.tsx b/src/app/pages/private/common/account/panels/profile/products/index.tsx new file mode 100644 index 0000000..a1833e0 --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/products/index.tsx @@ -0,0 +1,24 @@ +import { type ViewedProduct } from '@customcads/react-sdk'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import Scroller from '../../scroller'; +import Track from './track'; +import Item from './item'; + +type Props = { track: boolean; products: Array }; +const Products = ({ track, products }: Props) => { + const tProfile = usePrivateTranslations('account.profile'); + + return ( + ( + + ))} + className='w-65 md:w-md' + > + + + ); +}; + +export default Products; diff --git a/src/app/pages/private/common/account/panels/profile/products/item.tsx b/src/app/pages/private/common/account/panels/profile/products/item.tsx new file mode 100644 index 0000000..45ffece --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/products/item.tsx @@ -0,0 +1,41 @@ +import { Link } from '@tanstack/react-router'; +import { useQuery } from '@customcads/react-sdk'; +import * as uuid from '@/lib/utils/uuid'; +import * as dateTime from '@/app/utils/date-time'; +import Remove from './remove'; + +type Props = { id: string; viewedAt: string }; +const Item = ({ id, viewedAt }: Props) => { + const { data: product } = useQuery(({ products }) => + products.gallery.single({ id }), + ); + + return ( +
+ +

+ {product?.name ?? ( + + {uuid.extractSegment(id, 'first')}... + + )} +

+ +
+ + {dateTime.formatRelative({ + date: viewedAt, + limit: 'minute', + })} + +
+ +
+ ); +}; + +export default Item; diff --git a/src/app/pages/private/common/account/panels/profile/products/remove.tsx b/src/app/pages/private/common/account/panels/profile/products/remove.tsx new file mode 100644 index 0000000..078184e --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/products/remove.tsx @@ -0,0 +1,50 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { queryCall, useMutation } from '@customcads/react-sdk'; +import { X } from 'lucide-react'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { Button } from '@/app/components/ui/button'; +import * as ui from '@/app/components/ui/alert-dialog'; + +type Props = { id: string }; +const Remove = ({ id }: Props) => { + const { mutateAsync: deleteViewedProduct } = useMutation( + ({ identity }) => identity.deleteViewedProduct, + ); + const queryClient = useQueryClient(); + + const handleDelete = async () => { + await deleteViewedProduct({ productId: id }); + await queryCall( + ({ identity }) => identity.myAccount, + (opts) => queryClient.invalidateQueries(opts), + ); + }; + const tProfile = usePrivateTranslations('account.profile'); + + return ( + + + + + + + {tProfile('remove-title')} + + {tProfile('remove-description')} + + + + {tProfile('cancel')} + + {tProfile('continue')} + + + + + ); +}; + +export default Remove; diff --git a/src/app/pages/private/common/account/panels/profile/products/track.tsx b/src/app/pages/private/common/account/panels/profile/products/track.tsx new file mode 100644 index 0000000..f675bb8 --- /dev/null +++ b/src/app/pages/private/common/account/panels/profile/products/track.tsx @@ -0,0 +1,42 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { queryCall, useMutation } from '@customcads/react-sdk'; +import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; +import { Checkbox } from '@/app/components/ui/checkbox'; + +type Props = { track: boolean }; +const Track = ({ track }: Props) => { + const mutation = useMutation( + ({ identity }) => identity.toggleTrackViewedProducts, + ); + const queryClient = useQueryClient(); + + const toggleTrackViewedProducts = async () => { + await mutation.mutateAsync(); + await queryCall( + ({ identity }) => identity.myAccount, + (opts) => queryClient.invalidateQueries(opts), + ); + }; + const tProfile = usePrivateTranslations('account.profile'); + + return ( + <> + +

+ +

+ + ); +}; + +export default Track; diff --git a/src/app/pages/private/common/account/panels/scroller.tsx b/src/app/pages/private/common/account/panels/scroller.tsx new file mode 100644 index 0000000..4cfcf55 --- /dev/null +++ b/src/app/pages/private/common/account/panels/scroller.tsx @@ -0,0 +1,34 @@ +import { Children } from '@/types/react'; +import { cn } from '@/lib/utils/tailwindcss'; +import { ScrollArea, ScrollBar } from '@/app/components/ui/scroll-area'; + +type Props = Children & { + title: string; + items?: React.ReactNode; + className?: string; +}; +const Scroller = ({ title, items, className, children }: Props) => ( +
+

+ {title} +

+
+ + + + {items} + +
+ {children} +
+
+
+); + +export default Scroller; diff --git a/src/app/pages/private/common/account/panels/view.tsx b/src/app/pages/private/common/account/panels/view.tsx deleted file mode 100644 index 07b3027..0000000 --- a/src/app/pages/private/common/account/panels/view.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { - Card, - CardContent, - CardFooter, - CardHeader, -} from '@/app/components/ui/card'; - -export const View = () => { - return ; -}; diff --git a/src/app/validators/profile.ts b/src/app/validators/profile.ts new file mode 100644 index 0000000..f86b51a --- /dev/null +++ b/src/app/validators/profile.ts @@ -0,0 +1,9 @@ +import z from 'zod'; +import { Translators } from '@/app/types/schema'; +import * as schemas from './schemas'; + +type Props = Translators; +export const schema = ({ tErrors, tLabels }: Props) => + z.object({ ...schemas.name({ tErrors, tLabels }) }); + +export type Data = z.infer>; diff --git a/src/index.css b/src/index.css index f103445..b8d2335 100644 --- a/src/index.css +++ b/src/index.css @@ -223,4 +223,37 @@ code { animation: fade-in var(--tw-duration, 1s) ease-in-out var(--tw-animation-delay, 0s) forwards; } + + /* Chrome, Edge, Safari */ + ::-webkit-scrollbar { + width: 8px; /* Ширина на вертикалния скрол */ + height: 8px; /* Височина на хоризонталния скрол */ + } + + ::-webkit-scrollbar-track { + background: transparent; /* Скрива пътечката */ + } + + ::-webkit-scrollbar-thumb { + background: rgba(100, 116, 139, 0.5); /* Цвят на плъзгача (slate-500) */ + border-radius: 10px; /* Заобляне */ + } + + ::-webkit-scrollbar-thumb:hover { + background: #3b82f6; /* Цвят при посочване (blue-500) */ + } + + /* Firefox */ + * { + scrollbar-width: thin; + scrollbar-color: rgba(100, 116, 139, 0.5) transparent; + } + + .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome, Safari, and Opera */ + } + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } } From 5a0ccd6729486b04c2dd6610a327807295a5b5f6 Mon Sep 17 00:00:00 2001 From: Ivan Angelov Date: Wed, 13 May 2026 15:58:32 +0300 Subject: [PATCH 19/19] Fixed Build errors --- package-lock.json | 108 ++++++++++-------- package.json | 10 +- src/app/locales/bg-BG/common/devtools.ts | 9 -- src/app/locales/bg-BG/common/empty.ts | 4 +- src/app/locales/bg-BG/common/errors.ts | 4 +- src/app/locales/bg-BG/common/loading.ts | 4 +- src/app/locales/bg-BG/common/locales.ts | 4 +- src/app/locales/bg-BG/common/metrics.ts | 4 +- src/app/locales/bg-BG/common/roles.ts | 4 +- src/app/locales/en-GB/common/devtools.ts | 9 -- src/app/locales/en-GB/common/empty.ts | 4 +- src/app/locales/en-GB/common/errors.ts | 4 +- src/app/locales/en-GB/common/loading.ts | 4 +- src/app/locales/en-GB/common/locales.ts | 4 +- src/app/locales/en-GB/common/metrics.ts | 4 +- src/app/locales/en-GB/common/roles.ts | 4 +- .../panels/access/fingerprints/remove.tsx | 6 +- .../panels/profile/names/hooks/useForm.ts | 6 +- .../panels/profile/products/remove.tsx | 6 +- .../account/panels/profile/products/track.tsx | 6 +- src/routes/_private/account.tsx | 6 +- src/routes/_public/editor.$id.tsx | 10 +- src/routes/_public/gallery/index.tsx | 10 +- 23 files changed, 116 insertions(+), 118 deletions(-) delete mode 100644 src/app/locales/bg-BG/common/devtools.ts delete mode 100644 src/app/locales/en-GB/common/devtools.ts diff --git a/package-lock.json b/package-lock.json index a347f78..41aa224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@cloudflare/vite-plugin": "^1.17.1", - "@customcads/react-sdk": "^1.13.1-beta.21", + "@customcads/react-sdk": "^1.13.4-beta.2", "@icons-pack/react-simple-icons": "^13.8.0", "@microsoft/signalr": "^9.0.6", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -49,8 +49,8 @@ "i18next": "^25.6.0", "js-cookie": "^3.0.5", "lucide-react": "^0.544.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", "react-i18next": "^16.1.5", "react-phone-number-input": "^3.4.12", "tailwind-merge": "^3.0.2", @@ -71,8 +71,8 @@ "@tanstack/react-router-devtools": "^1.141.2", "@types/js-cookie": "^3.0.6", "@types/node": "^20.19.9", - "@types/react": "^19.1.9", - "@types/react-dom": "^19.1.7", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/three": "^0.172.0", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", @@ -742,15 +742,28 @@ } }, "node_modules/@customcads/react-sdk": { - "version": "1.13.1-beta.21", - "resolved": "https://registry.npmjs.org/@customcads/react-sdk/-/react-sdk-1.13.1-beta.21.tgz", - "integrity": "sha512-/HNgoDwHIU9eTA3Oen/ETmJNyByw//S+kGVLPhCsc2EUdR2cgAcp6KPgD+q+mAL7y3HQF3xQOJz3pU8B7guKzg==", + "version": "1.13.4-beta.2", + "resolved": "https://registry.npmjs.org/@customcads/react-sdk/-/react-sdk-1.13.4-beta.2.tgz", + "integrity": "sha512-rRqs6pU1Q54mYbrZPXA9yPN0NSP6C1G91A062EAourYDlFrvaI5CXYAB0DLV544meAaIJzTPjZ4Y84ZZRMMt0g==", "dependencies": { - "@tanstack/react-query": "^5.90.5", - "axios": "^1.12.2", - "prettier": "^3.6.2", - "react": "^19.2.0", - "uuid": "^13.0.0" + "@tanstack/react-query": "^5.100.10", + "axios": "^1.16.0", + "prettier": "^3.8.3", + "react": "^19.2.6", + "uuid": "^14.0.0" + } + }, + "node_modules/@customcads/react-sdk/node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" } }, "node_modules/@emnapi/runtime": { @@ -4360,9 +4373,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.90.18", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.18.tgz", - "integrity": "sha512-rbGx6bHgPNVzutP7BEr+53UPKohpckqlMAad+To9UxTbeaQ+kC/1SDRj+QzkwbQ7qhLT/1IKp34yS6thda6fzA==", + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", + "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", "license": "MIT", "peer": true, "funding": { @@ -4448,13 +4461,13 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.18", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.18.tgz", - "integrity": "sha512-KqNZX0C5IFz4639zR1ilnQ288tQdJrMNLtzmlzyJ14xauBkhtLEy3mPU/V4KiHsr41eL1ILZbDP36TB12lYfCQ==", + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", + "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", "license": "MIT", "peer": true, "dependencies": { - "@tanstack/query-core": "5.90.18" + "@tanstack/query-core": "5.100.10" }, "funding": { "type": "github", @@ -5199,9 +5212,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", - "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", "peer": true, @@ -5852,14 +5865,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "node_modules/babel-dead-code-elimination": { @@ -7216,9 +7229,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -8715,9 +8728,9 @@ } }, "node_modules/prettier": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz", - "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "license": "MIT", "peer": true, "bin": { @@ -8755,10 +8768,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/psl": { "version": "1.15.0", @@ -8788,9 +8804,9 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", "peer": true, "engines": { @@ -8798,16 +8814,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.2.6" } }, "node_modules/react-i18next": { diff --git a/package.json b/package.json index 5f9cff9..3855953 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@cloudflare/vite-plugin": "^1.17.1", - "@customcads/react-sdk": "^1.13.1-beta.21", + "@customcads/react-sdk": "^1.13.4-beta.2", "@icons-pack/react-simple-icons": "^13.8.0", "@microsoft/signalr": "^9.0.6", "@radix-ui/react-alert-dialog": "^1.1.15", @@ -58,8 +58,8 @@ "i18next": "^25.6.0", "js-cookie": "^3.0.5", "lucide-react": "^0.544.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", "react-i18next": "^16.1.5", "react-phone-number-input": "^3.4.12", "tailwind-merge": "^3.0.2", @@ -80,8 +80,8 @@ "@tanstack/react-router-devtools": "^1.141.2", "@types/js-cookie": "^3.0.6", "@types/node": "^20.19.9", - "@types/react": "^19.1.9", - "@types/react-dom": "^19.1.7", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/three": "^0.172.0", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", diff --git a/src/app/locales/bg-BG/common/devtools.ts b/src/app/locales/bg-BG/common/devtools.ts deleted file mode 100644 index c2c4cec..0000000 --- a/src/app/locales/bg-BG/common/devtools.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Devtools } from '../../types/common'; - -export default { - theme: 'Тема:', - language: 'Език:', - 'language-placeholder': 'Потърси език...', - currency: 'Валута:', - 'currency-placeholder': 'Потърси валута...', -} satisfies Devtools; diff --git a/src/app/locales/bg-BG/common/empty.ts b/src/app/locales/bg-BG/common/empty.ts index b7709ee..ee6ecda 100644 --- a/src/app/locales/bg-BG/common/empty.ts +++ b/src/app/locales/bg-BG/common/empty.ts @@ -1,5 +1,5 @@ -import { Empty } from '../../types/common'; +import { Common } from '../../types/common'; export default { products: 'Няма намерени Продукти.', -} satisfies Empty; +} satisfies Common['common.empty']; diff --git a/src/app/locales/bg-BG/common/errors.ts b/src/app/locales/bg-BG/common/errors.ts index be41e4d..cfd1fa3 100644 --- a/src/app/locales/bg-BG/common/errors.ts +++ b/src/app/locales/bg-BG/common/errors.ts @@ -1,4 +1,4 @@ -import { Errors } from '@/app/locales/types/common'; +import { Common } from '@/app/locales/types/common'; export default { login_link: 'Влез в акаунт.', @@ -19,4 +19,4 @@ export default { default_title: 'Нещо се обърка!', default_message: 'Възникна неочаквана грешка. Опитайте по-късно.', default_tip: 'Ако проблемът продължи, свържете се с нас.', -} satisfies Errors; +} satisfies Common['common.errors']; diff --git a/src/app/locales/bg-BG/common/loading.ts b/src/app/locales/bg-BG/common/loading.ts index 752d1ef..42bf12c 100644 --- a/src/app/locales/bg-BG/common/loading.ts +++ b/src/app/locales/bg-BG/common/loading.ts @@ -1,5 +1,5 @@ -import { Loading } from '../../types/common'; +import { Common } from '../../types/common'; export default { gallery: 'Галерията зарежда...', -} satisfies Loading; +} satisfies Common['common.loading']; diff --git a/src/app/locales/bg-BG/common/locales.ts b/src/app/locales/bg-BG/common/locales.ts index 374d7da..e7b96fb 100644 --- a/src/app/locales/bg-BG/common/locales.ts +++ b/src/app/locales/bg-BG/common/locales.ts @@ -1,4 +1,4 @@ -import { Locales } from '@/app/locales/types/common'; +import { Common } from '@/app/locales/types/common'; export default { '': 'Европейски', @@ -32,4 +32,4 @@ export default { 'en-SG': 'Английски (Сингапур)', 'th-TH': 'Тайландски', 'en-ZA': 'Английски (Южна Африка)', -} satisfies Locales; +} satisfies Common['common.locales']; diff --git a/src/app/locales/bg-BG/common/metrics.ts b/src/app/locales/bg-BG/common/metrics.ts index 1149ef2..bc08c81 100644 --- a/src/app/locales/bg-BG/common/metrics.ts +++ b/src/app/locales/bg-BG/common/metrics.ts @@ -1,8 +1,8 @@ -import { Metrics } from '@/app/locales/types/common'; +import { Common } from '@/app/locales/types/common'; export default { width: 'Широчина', length: 'Дължина', height: 'Височина', volume: 'Обем', -} satisfies Metrics; +} satisfies Common['common.metrics']; diff --git a/src/app/locales/bg-BG/common/roles.ts b/src/app/locales/bg-BG/common/roles.ts index 8778372..2c85ef6 100644 --- a/src/app/locales/bg-BG/common/roles.ts +++ b/src/app/locales/bg-BG/common/roles.ts @@ -1,4 +1,4 @@ -import { Roles } from '@/app/locales/types/common'; +import { Common } from '@/app/locales/types/common'; export default { Customer: 'Купувач', @@ -6,4 +6,4 @@ export default { Creator: 'Създател', Designer: 'Дизайнер', Administrator: 'Администратор', -} satisfies Roles; +} satisfies Common['common.roles']; diff --git a/src/app/locales/en-GB/common/devtools.ts b/src/app/locales/en-GB/common/devtools.ts deleted file mode 100644 index b652dd1..0000000 --- a/src/app/locales/en-GB/common/devtools.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Devtools } from '../../types/common'; - -export default { - theme: 'Theme:', - language: 'Language:', - 'language-placeholder': 'Search language...', - currency: 'Currency:', - 'currency-placeholder': 'Search currency...', -} satisfies Devtools; diff --git a/src/app/locales/en-GB/common/empty.ts b/src/app/locales/en-GB/common/empty.ts index a59d8d4..9af3854 100644 --- a/src/app/locales/en-GB/common/empty.ts +++ b/src/app/locales/en-GB/common/empty.ts @@ -1,5 +1,5 @@ -import { Empty } from '../../types/common'; +import { Common } from '../../types/common'; export default { products: 'No Products found.', -} satisfies Empty; +} satisfies Common['common.empty']; diff --git a/src/app/locales/en-GB/common/errors.ts b/src/app/locales/en-GB/common/errors.ts index 8533516..70d7189 100644 --- a/src/app/locales/en-GB/common/errors.ts +++ b/src/app/locales/en-GB/common/errors.ts @@ -1,4 +1,4 @@ -import { Errors } from '@/app/locales/types/common'; +import { Common } from '@/app/locales/types/common'; export default { login_link: 'Sign In', @@ -21,4 +21,4 @@ export default { default_title: 'Something Went Wrong', default_message: 'An unexpected error occurred. Please try again later.', default_tip: 'If the issue persists, contact our support team.', -} satisfies Errors; +} satisfies Common['common.errors']; diff --git a/src/app/locales/en-GB/common/loading.ts b/src/app/locales/en-GB/common/loading.ts index ecb9d41..d12e631 100644 --- a/src/app/locales/en-GB/common/loading.ts +++ b/src/app/locales/en-GB/common/loading.ts @@ -1,5 +1,5 @@ -import { Loading } from '../../types/common'; +import { Common } from '../../types/common'; export default { gallery: 'Gallery loading...', -} satisfies Loading; +} satisfies Common['common.loading']; diff --git a/src/app/locales/en-GB/common/locales.ts b/src/app/locales/en-GB/common/locales.ts index 2d8af5f..a15e350 100644 --- a/src/app/locales/en-GB/common/locales.ts +++ b/src/app/locales/en-GB/common/locales.ts @@ -1,4 +1,4 @@ -import { Locales } from '@/app/locales/types/common'; +import { Common } from '@/app/locales/types/common'; export default { '': 'European', @@ -32,4 +32,4 @@ export default { 'en-SG': 'English (Singapore)', 'th-TH': 'Thai', 'en-ZA': 'English (South Africa)', -} satisfies Locales; +} satisfies Common['common.locales']; diff --git a/src/app/locales/en-GB/common/metrics.ts b/src/app/locales/en-GB/common/metrics.ts index c8093e1..ff6dab4 100644 --- a/src/app/locales/en-GB/common/metrics.ts +++ b/src/app/locales/en-GB/common/metrics.ts @@ -1,8 +1,8 @@ -import { Metrics } from '@/app/locales/types/common'; +import { Common } from '@/app/locales/types/common'; export default { width: 'Width', length: 'Length', height: 'Height', volume: 'Volume', -} satisfies Metrics; +} satisfies Common['common.metrics']; diff --git a/src/app/locales/en-GB/common/roles.ts b/src/app/locales/en-GB/common/roles.ts index bbbe57d..4cf7591 100644 --- a/src/app/locales/en-GB/common/roles.ts +++ b/src/app/locales/en-GB/common/roles.ts @@ -1,4 +1,4 @@ -import { Roles } from '@/app/locales/types/common'; +import { Common } from '@/app/locales/types/common'; export default { Customer: 'Customer', @@ -6,4 +6,4 @@ export default { Creator: 'Creator', Designer: 'Designer', Administrator: 'Administrator', -} satisfies Roles; +} satisfies Common['common.roles']; diff --git a/src/app/pages/private/common/account/panels/access/fingerprints/remove.tsx b/src/app/pages/private/common/account/panels/access/fingerprints/remove.tsx index 2400400..1318a01 100644 --- a/src/app/pages/private/common/account/panels/access/fingerprints/remove.tsx +++ b/src/app/pages/private/common/account/panels/access/fingerprints/remove.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query'; -import { queryCall, useMutation } from '@customcads/react-sdk'; +import { query, useMutation } from '@customcads/react-sdk'; import { ShieldUser, X } from 'lucide-react'; import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; import { useLogout } from '@/app/hooks/features/header/useLogout'; @@ -13,9 +13,9 @@ const Remove = ({ id, allow }: Props) => { const handleDelete = async () => { await mutation.mutateAsync({ refreshTokenId: id }); - await queryCall( + await query.invalidateQueries( ({ identity }) => identity.myAccount, - (opts) => queryClient.invalidateQueries(opts), + queryClient, ); }; const { logout } = useLogout(); diff --git a/src/app/pages/private/common/account/panels/profile/names/hooks/useForm.ts b/src/app/pages/private/common/account/panels/profile/names/hooks/useForm.ts index 4c8ad10..6b3520b 100644 --- a/src/app/pages/private/common/account/panels/profile/names/hooks/useForm.ts +++ b/src/app/pages/private/common/account/panels/profile/names/hooks/useForm.ts @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useForm as useTanStackForm } from '@tanstack/react-form'; import { useQueryClient } from '@tanstack/react-query'; -import { queryCall, useMutation } from '@customcads/react-sdk'; +import { query, useMutation } from '@customcads/react-sdk'; import * as form from '@/lib/utils/form'; import { useFormTranslations } from '@/app/hooks/locales/translations/components'; import { useForceLocaleRefresh } from '@/app/hooks/locales/useForceLocaleRefresh'; @@ -23,9 +23,9 @@ export const useForm = (defaultValues: profile.Data) => { onSubmit: async ({ value }) => { await changeNames(value); - await queryCall( + await query.invalidateQueries( ({ identity }) => identity.myAccount, - (opts) => queryClient.invalidateQueries(opts), + queryClient, ); }, validators: { onChange: profile.schema({ tErrors, tLabels }) }, diff --git a/src/app/pages/private/common/account/panels/profile/products/remove.tsx b/src/app/pages/private/common/account/panels/profile/products/remove.tsx index 078184e..4d8e510 100644 --- a/src/app/pages/private/common/account/panels/profile/products/remove.tsx +++ b/src/app/pages/private/common/account/panels/profile/products/remove.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query'; -import { queryCall, useMutation } from '@customcads/react-sdk'; +import { query, useMutation } from '@customcads/react-sdk'; import { X } from 'lucide-react'; import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; import { Button } from '@/app/components/ui/button'; @@ -14,9 +14,9 @@ const Remove = ({ id }: Props) => { const handleDelete = async () => { await deleteViewedProduct({ productId: id }); - await queryCall( + await query.invalidateQueries( ({ identity }) => identity.myAccount, - (opts) => queryClient.invalidateQueries(opts), + queryClient, ); }; const tProfile = usePrivateTranslations('account.profile'); diff --git a/src/app/pages/private/common/account/panels/profile/products/track.tsx b/src/app/pages/private/common/account/panels/profile/products/track.tsx index f675bb8..e73d357 100644 --- a/src/app/pages/private/common/account/panels/profile/products/track.tsx +++ b/src/app/pages/private/common/account/panels/profile/products/track.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query'; -import { queryCall, useMutation } from '@customcads/react-sdk'; +import { query, useMutation } from '@customcads/react-sdk'; import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/private'; import { Checkbox } from '@/app/components/ui/checkbox'; @@ -12,9 +12,9 @@ const Track = ({ track }: Props) => { const toggleTrackViewedProducts = async () => { await mutation.mutateAsync(); - await queryCall( + await query.invalidateQueries( ({ identity }) => identity.myAccount, - (opts) => queryClient.invalidateQueries(opts), + queryClient, ); }; const tProfile = usePrivateTranslations('account.profile'); diff --git a/src/routes/_private/account.tsx b/src/routes/_private/account.tsx index d9758ce..c62b96c 100644 --- a/src/routes/_private/account.tsx +++ b/src/routes/_private/account.tsx @@ -1,6 +1,6 @@ import z from 'zod'; import { createFileRoute } from '@tanstack/react-router'; -import { queryCall } from '@customcads/react-sdk'; +import { query } from '@customcads/react-sdk'; import MyAccount, { tabs } from '@/app/pages/private/common/account'; export const Route = createFileRoute('/_private/account')({ @@ -9,9 +9,9 @@ export const Route = createFileRoute('/_private/account')({ tab: z.literal(tabs).optional(), }), loader: async ({ context: { queryClient } }) => { - const { data: account } = await queryCall( + const { data: account } = await query.fetchQuery( ({ identity }) => identity.myAccount, - (opts) => queryClient.fetchQuery(opts), + queryClient, ); return { account }; }, diff --git a/src/routes/_public/editor.$id.tsx b/src/routes/_public/editor.$id.tsx index 940fbee..e7345ea 100644 --- a/src/routes/_public/editor.$id.tsx +++ b/src/routes/_public/editor.$id.tsx @@ -1,17 +1,17 @@ import { createFileRoute } from '@tanstack/react-router'; -import { queryCall } from '@customcads/react-sdk'; +import { query } from '@customcads/react-sdk'; import Editor from '@/app/pages/public/editor'; export const Route = createFileRoute('/_public/editor/$id')({ component: Editor, loader: async ({ params, context: { queryClient } }) => { - const { data: product } = await queryCall( + const { data: product } = await query.fetchQuery( ({ products }) => products.gallery.single({ id: params.id }), - (opts) => queryClient.fetchQuery(opts), + queryClient, ); - const { data: cad } = await queryCall( + const { data: cad } = await query.fetchQuery( ({ cads }) => cads.single({ id: product.cadId }), - (opts) => queryClient.fetchQuery(opts), + queryClient, ); return { product, cad }; diff --git a/src/routes/_public/gallery/index.tsx b/src/routes/_public/gallery/index.tsx index e12ebbb..6511862 100644 --- a/src/routes/_public/gallery/index.tsx +++ b/src/routes/_public/gallery/index.tsx @@ -1,7 +1,7 @@ import z from 'zod'; import { createFileRoute } from '@tanstack/react-router'; import { - queryCall, + query, type GalleryAllProductsRequest, type SingleCategoryRequest, } from '@customcads/react-sdk'; @@ -41,16 +41,16 @@ export const Route = createFileRoute('/_public/gallery/')({ name: deps.categoryName, }; - const { data: category } = await queryCall( + const { data: category } = await query.fetchQuery( ({ categories }) => categories.single(categoryRequestParams), - (opts) => queryClient.fetchQuery(opts), + queryClient, ); requestParams.categoryId = category.id; } - const { data: result } = await queryCall( + const { data: result } = await query.fetchQuery( ({ products }) => products.gallery.all(requestParams), - (opts) => queryClient.fetchQuery(opts), + queryClient, ); return { requestParams, result };