};
+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..4d8e510
--- /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 { 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';
+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 query.invalidateQueries(
+ ({ identity }) => identity.myAccount,
+ queryClient,
+ );
+ };
+ 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..e73d357
--- /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 { query, 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 query.invalidateQueries(
+ ({ identity }) => identity.myAccount,
+ queryClient,
+ );
+ };
+ 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/public/cart/aside.tsx b/src/app/pages/public/cart/aside.tsx
index 37eb9e9..93807e9 100644
--- a/src/app/pages/public/cart/aside.tsx
+++ b/src/app/pages/public/cart/aside.tsx
@@ -1,7 +1,7 @@
// import { Link } from '@tanstack/react-router';
// import { useGalleryTranslations } from '@/app/hooks/locales/translations/pages/public';
import { useCartStore } from '@/app/hooks/stores/useCartStore';
-import { useMoney } from '@/app/hooks/locales/useMoney';
+import { useMoneyFormatter } from '@/app/hooks/locales/useMoneyFormatter';
// import { Button } from '@/app/components/ui/button';
import Money from './money';
@@ -19,10 +19,11 @@ const Aside = ({ prices, costs }: Props) => {
costs: calculate(costs),
};
+ const formatMoney = useMoneyFormatter();
const money = {
- product: useMoney(sum.prices),
- print: useMoney(sum.costs),
- total: useMoney(sum.prices + sum.costs),
+ product: formatMoney(sum.prices),
+ print: formatMoney(sum.costs),
+ total: formatMoney(sum.prices + sum.costs),
};
// TODO: Update when Payment is implemented
diff --git a/src/app/pages/public/cart/item/hooks/useItemMoney.ts b/src/app/pages/public/cart/item/hooks/useItemMoney.ts
index e2be8af..5783688 100644
--- a/src/app/pages/public/cart/item/hooks/useItemMoney.ts
+++ b/src/app/pages/public/cart/item/hooks/useItemMoney.ts
@@ -1,8 +1,12 @@
-import { useMoney } from '@/app/hooks/locales/useMoney';
+import { useMoneyFormatter } from '@/app/hooks/locales/useMoneyFormatter';
type Props = { product: number; print: number };
-export const useItemMoney = ({ product, print }: Props) => ({
- product: useMoney(product),
- print: useMoney(print),
- total: useMoney(product + print),
-});
+export const useItemMoney = ({ product, print }: Props) => {
+ const formatMoney = useMoneyFormatter();
+
+ return {
+ product: formatMoney(product),
+ print: formatMoney(print),
+ total: formatMoney(product + print),
+ };
+};
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/editor/info/print.tsx b/src/app/pages/public/editor/info/print.tsx
index dc86328..3901ee2 100644
--- a/src/app/pages/public/editor/info/print.tsx
+++ b/src/app/pages/public/editor/info/print.tsx
@@ -1,7 +1,7 @@
import type { Mass } from '@/types/units';
import * as units from '@/lib/utils/units';
import { useEditorStore } from '@/app/hooks/stores/useEditorStore';
-import { useMoney } from '@/app/hooks/locales/useMoney';
+import { useMoneyFormatter } from '@/app/hooks/locales/useMoneyFormatter';
import { useGalleryTranslations } from '@/app/hooks/locales/translations/pages/public';
type Props = { id: string; mass: Mass };
@@ -10,11 +10,13 @@ const PrintInfo = ({ id, mass }: Props) => {
weight: useEditorStore(id, (state) => state.weight),
cost: useEditorStore(id, (state) => state.cost),
};
- const tEditor = useGalleryTranslations('editor');
- const cost = useMoney(state.cost);
+ const tEditor = useGalleryTranslations('editor');
const weight = units.weight(state.weight, mass);
+ const formatMoney = useMoneyFormatter();
+ const cost = formatMoney(state.cost);
+
return (
diff --git a/src/app/pages/public/editor/looks/dropdown.tsx b/src/app/pages/public/editor/looks/dropdown.tsx
index f8a00a2..5ec53ee 100644
--- a/src/app/pages/public/editor/looks/dropdown.tsx
+++ b/src/app/pages/public/editor/looks/dropdown.tsx
@@ -1,6 +1,7 @@
import { type MaterialResponse } from '@customcads/react-sdk';
import { Children } from '@/types/react';
import { cn } from '@/lib/utils/tailwindcss';
+import { useMoneyFormatter } from '@/app/hooks/locales/useMoneyFormatter';
import {
DropdownMenu,
DropdownMenuContent,
@@ -18,28 +19,35 @@ const MaterialsDropdown = ({
materials,
current,
onClick,
-}: Props) => (
-
- {children}
-
-
- {materials.map((x) => (
- - onClick?.(x)}
- className={cn(
- 'cursor-pointer',
- current === x.id
- ? 'opacity-50'
- : 'hover:opacity-70',
- )}
- >
- {materials_utils.format({ material: x, cost: true })}
-
- ))}
-
-
-
-);
+}: Props) => {
+ const formatMoney = useMoneyFormatter();
+
+ return (
+
+ {children}
+
+
+ {materials.map((x) => (
+ - onClick?.(x)}
+ className={cn(
+ 'cursor-pointer',
+ current === x.id
+ ? 'opacity-50'
+ : 'hover:opacity-70',
+ )}
+ >
+ {materials_utils.format({
+ material: x,
+ cost: formatMoney(x.cost, 0),
+ })}
+
+ ))}
+
+
+
+ );
+};
export default MaterialsDropdown;
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/card.tsx b/src/app/pages/public/product/card.tsx
index 68dd8a4..8909328 100644
--- a/src/app/pages/public/product/card.tsx
+++ b/src/app/pages/public/product/card.tsx
@@ -1,14 +1,15 @@
import { GallerySingleProductResponse } from '@customcads/react-sdk';
import { useGalleryTranslations } from '@/app/hooks/locales/translations/pages/public';
-import { useMoney } from '@/app/hooks/locales/useMoney';
+import { useMoneyFormatter } from '@/app/hooks/locales/useMoneyFormatter';
import Cad from '@/app/components/cad';
import Detail from './detail';
type Props = { product: GallerySingleProductResponse };
const Card = ({ product }: Props) => {
- const tProduct = useGalleryTranslations('product');
+ const moneyFormat = useMoneyFormatter();
+ const price = moneyFormat(product.price);
- const price = useMoney(product.price);
+ const tProduct = useGalleryTranslations('product');
const { name, description, category, cadId } = product;
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;
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/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/app/utils/materials.ts b/src/app/utils/materials.ts
index b686b11..3101119 100644
--- a/src/app/utils/materials.ts
+++ b/src/app/utils/materials.ts
@@ -1,10 +1,11 @@
import { type MaterialResponse } from '@customcads/react-sdk';
-type FormatProps = { material: MaterialResponse; cost?: boolean };
-export const format = ({ material, cost }: FormatProps) => {
+type Props = { material: MaterialResponse; cost?: string };
+export const format = ({ material, cost }: Props) => {
+ const money = cost ?? `${material.cost}€`;
const n = material.name;
const d = `: ${material.density}g/cm³`;
- const c = ` (${material.cost}€/kg)`;
+ const c = ` (${money}/kg)`;
return cost ? n + c + d : n + d;
};
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/app/validators/forgot-password.ts b/src/app/validators/forgot-password.ts
new file mode 100644
index 0000000..c72f087
--- /dev/null
+++ b/src/app/validators/forgot-password.ts
@@ -0,0 +1,7 @@
+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.email({ tErrors, tLabels }) });
diff --git a/src/app/validators/login.ts b/src/app/validators/login.ts
index fcae387..85a9ad1 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 * as schemas 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: schemas.name({ tErrors, tLabels }).username,
+ password: schemas.password({ tErrors, tLabels }).password,
});
-};
+
+export type Data = z.infer>;
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/app/validators/register.ts b/src/app/validators/register.ts
new file mode 100644
index 0000000..a7d658f
--- /dev/null
+++ b/src/app/validators/register.ts
@@ -0,0 +1,19 @@
+import z from 'zod';
+import * as form from '@/lib/utils/form';
+import { Translators } from '@/app/types/schema';
+import * as schemas from './schemas';
+
+type Props = Translators;
+export const schema = ({ tErrors, tLabels }: Props) => {
+ return form.zodHelpers.passwordEquality(
+ z.object({
+ role: z.literal(['Customer', 'Contributor']),
+ ...schemas.name({ tErrors, tLabels }),
+ ...schemas.password({ tErrors, tLabels }),
+ ...schemas.email({ tErrors, tLabels }),
+ }),
+ tErrors,
+ );
+};
+
+export type Data = z.infer>;
diff --git a/src/app/validators/reset-password.ts b/src/app/validators/reset-password.ts
new file mode 100644
index 0000000..984bfca
--- /dev/null
+++ b/src/app/validators/reset-password.ts
@@ -0,0 +1,7 @@
+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.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..b6e06cb
--- /dev/null
+++ b/src/app/validators/schemas/name.ts
@@ -0,0 +1,45 @@
+import z from 'zod';
+import { Translators } from '@/app/types/schema';
+import { USERS as VALIDATIONS } from '@/app/constants/validations';
+import { zodHelpers } from '@/lib/utils/form';
+
+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: zodHelpers.emptyOrLength(
+ args.firstName,
+ tErrors('length', args.firstName),
+ ),
+ lastName: zodHelpers.emptyOrLength(
+ args.lastName,
+ 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..26d482d
--- /dev/null
+++ b/src/app/validators/schemas/password.ts
@@ -0,0 +1,40 @@
+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 = {
+ password: {
+ field: tLabels('password'),
+ min: VALIDATIONS.password.min,
+ max: VALIDATIONS.password.max,
+ },
+ confirmPassword: {
+ field: tLabels('confirmPassword'),
+ min: VALIDATIONS.password.min,
+ max: VALIDATIONS.password.max,
+ },
+ };
+
+ return {
+ 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),
+ }),
+ 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),
+ }),
+ };
+};
diff --git a/src/index.css b/src/index.css
index e1606a1..b8d2335 100644
--- a/src/index.css
+++ b/src/index.css
@@ -36,9 +36,11 @@ code {
--accent-foreground: oklch(21.033% 0.00588 285.832 / 0.714);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
+ --success: oklch(0.6 0.15 142);
+ --success-foreground: oklch(0.985 0 0);
--border: oklch(32.897% 0.00004 271.152);
--input: oklch(32.897% 0.00004 271.152);
- --input-accent: oklch(91.973% 0.00413 285.946 / 0.366);
+ --input-accent: oklch(65.823% 0.00739 286.052 / 0.366);
--ring: oklch(0.871 0.006 286.286);
--shadow: oklch(20.505% 0.00198 286.056 / 0.765);
--unread: oklch(77.464% 0.1392 228.017);
@@ -93,8 +95,10 @@ code {
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
+ --success: oklch(0.4 0.15 142);
+ --success-foreground: oklch(0.985 0 0);
--border: oklch(71.959% 0.01875 285.863);
- --input: oklch(0.274 0.006 286.033);
+ --input: oklch(0.474 0.006 286.033);
--input-accent: oklch(91.973% 0.00413 285.946 / 0.366);
--ring: oklch(0.442 0.017 285.786);
--shadow: oklch(29.598% 0.01262 285.574);
@@ -150,6 +154,8 @@ code {
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
+ --color-success: var(--success);
+ --color-success-foreground: var(--success-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-input-accent: var(--input-accent);
@@ -217,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 */
+ }
}
diff --git a/src/lib/isomorphic/locale.ts b/src/lib/isomorphic/locale.ts
index a28c3cf..8150e50 100644
--- a/src/lib/isomorphic/locale.ts
+++ b/src/lib/isomorphic/locale.ts
@@ -1,21 +1,27 @@
import { createIsomorphicFn } from '@tanstack/react-start';
-import { AllowedLanguage } from '@/types/locale';
+import { ALLOWED_LANGUAGES, AllowedLanguage } from '@/types/locale';
import { LanguageStoreState } from '@/app/stores/locale';
import { LOCALE } from '@/app/constants/stores';
import { get } from './persistence';
export const getUserDefaultLanguage = createIsomorphicFn()
- .client(() => {
+ .client<[], AllowedLanguage>(() => {
const languages = navigator.languages || [navigator.language];
- return languages[0] as AllowedLanguage;
+
+ const matches: (AllowedLanguage | undefined)[] = [
+ ALLOWED_LANGUAGES.find((x) => languages.includes(x)),
+ ALLOWED_LANGUAGES.map((x) => ({
+ original: x,
+ split: x.split('-')[0],
+ })).find((x) => languages.includes(x.split))?.original,
+ ];
+
+ return matches.find((x) => !!x) ?? 'en-GB';
})
- .server(() => 'en-GB' as AllowedLanguage);
+ .server(() => 'en-GB');
export const getUserTimeZone = createIsomorphicFn()
- .client(() => {
- const { timeZone } = Intl.DateTimeFormat().resolvedOptions();
- return timeZone;
- })
+ .client(() => Intl.DateTimeFormat().resolvedOptions().timeZone)
.server(() => 'UTC');
export const getLanguageCookie = () =>
diff --git a/src/lib/isomorphic/persistence/get.ts b/src/lib/isomorphic/persistence/get.ts
index e95394a..6d55485 100644
--- a/src/lib/isomorphic/persistence/get.ts
+++ b/src/lib/isomorphic/persistence/get.ts
@@ -17,3 +17,7 @@ const parseCookie = (cookie: string | null): TState | null => {
export const exists = (key: string) => getCookie(key) !== undefined;
export const get = (key: string) =>
parseCookie(getCookie(key) ?? null);
+
+export const getAll = createIsomorphicFn()
+ .client(() => document.cookie)
+ .server(() => server.getRequestHeader('cookie') ?? '');
diff --git a/src/lib/utils/__tests__/form.test.ts b/src/lib/utils/__tests__/form.test.ts
index 020719e..66e0a14 100644
--- a/src/lib/utils/__tests__/form.test.ts
+++ b/src/lib/utils/__tests__/form.test.ts
@@ -1,39 +1,7 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
import * as src from '../form';
describe('Form utility tests', () => {
- describe('Equality Helper', () => {
- let helper: ReturnType;
-
- beforeEach(() => {
- helper = src.equalityHelper();
- });
-
- it('sync method should return true', () => {
- // Arrange
- // Act
- const result = helper.sync('test');
-
- // Assert
- expect(result).toBe(true);
- });
-
- it.each([{ input: 'test' }, { input: 'different' }])(
- 'check method should return correctly when input is $input',
- ({ input }) => {
- // Arrange
- const expected = 'test';
- helper.sync(expected);
-
- // Act
- const result = helper.check(input);
-
- // Assert
- expect(result).toBe(input === expected);
- },
- );
- });
-
describe('File Helper', () => {
it('returns false for empty file', () => {
// Arrange
diff --git a/src/lib/utils/form.ts b/src/lib/utils/form.ts
index 3f05d3c..4963240 100644
--- a/src/lib/utils/form.ts
+++ b/src/lib/utils/form.ts
@@ -1,16 +1,33 @@
-export const equalityHelper = () => {
- let holder = '';
+import { Translators } from '@/app/types/schema';
+import z from 'zod';
- const sync = (x: string) => {
- holder = x;
- return true;
- };
-
- const check = (x: string) => {
- return holder === x;
- };
+export const zodHelpers = {
+ emptyOrLength: (args: { min: number; max: number }, error: string) =>
+ z
+ .string()
+ .optional()
+ .refine(
+ (x) => {
+ if (!x) return true;
- return { sync, check };
+ return x.length >= args.min && x.length <= args.max;
+ },
+ { error },
+ ),
+ passwordEquality: <
+ TShape extends {
+ password: z.ZodString;
+ confirmPassword: z.ZodString;
+ },
+ >(
+ object: z.ZodObject,
+ tErrors: Translators['tErrors'],
+ ) =>
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ object.refine((data: any) => data.password === data.confirmPassword, {
+ error: tErrors('equal-passwords'),
+ path: ['confirmPassword'],
+ }),
};
export const fileHelper = (file: File) => file.size > 0;
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 269787d..f4843e7 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -9,14 +9,23 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
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 GuestRegisterRouteImport } from './routes/_guest/register'
import { Route as GuestLoginRouteImport } from './routes/_guest/login'
+import { Route as GuestConfirmEmailRouteImport } from './routes/_guest/confirm-email'
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'
+const PrivateRoute = PrivateRouteImport.update({
+ id: '/_private',
+ getParentRoute: () => rootRouteImport,
+} as any)
const GuestRoute = GuestRouteImport.update({
id: '/_guest',
getParentRoute: () => rootRouteImport,
@@ -26,16 +35,36 @@ 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',
getParentRoute: () => rootRouteImport,
} as any)
+const PrivateAccountRoute = PrivateAccountRouteImport.update({
+ id: '/account',
+ path: '/account',
+ getParentRoute: () => PrivateRoute,
+} as any)
+const GuestRegisterRoute = GuestRegisterRouteImport.update({
+ id: '/register',
+ path: '/register',
+ getParentRoute: () => GuestRoute,
+} as any)
const GuestLoginRoute = GuestLoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => GuestRoute,
} as any)
+const GuestConfirmEmailRoute = GuestConfirmEmailRouteImport.update({
+ id: '/confirm-email',
+ path: '/confirm-email',
+ getParentRoute: () => GuestRoute,
+} as any)
const PublicGalleryIndexRoute = PublicGalleryIndexRouteImport.update({
id: '/_public/gallery/',
path: '/gallery/',
@@ -53,16 +82,24 @@ const PublicEditorIdRoute = PublicEditorIdRouteImport.update({
} as any)
export interface FileRoutesByFullPath {
+ '/confirm-email': typeof GuestConfirmEmailRoute
'/login': typeof GuestLoginRoute
+ '/register': typeof GuestRegisterRoute
+ '/account': typeof PrivateAccountRoute
'/cart': typeof PublicCartRoute
+ '/reset-password': typeof SharedResetPasswordRoute
'/': typeof PublicIndexRoute
'/editor/$id': typeof PublicEditorIdRoute
'/gallery/$id': typeof PublicGalleryIdRoute
'/gallery': typeof PublicGalleryIndexRoute
}
export interface FileRoutesByTo {
+ '/confirm-email': typeof GuestConfirmEmailRoute
'/login': typeof GuestLoginRoute
+ '/register': typeof GuestRegisterRoute
+ '/account': typeof PrivateAccountRoute
'/cart': typeof PublicCartRoute
+ '/reset-password': typeof SharedResetPasswordRoute
'/': typeof PublicIndexRoute
'/editor/$id': typeof PublicEditorIdRoute
'/gallery/$id': typeof PublicGalleryIdRoute
@@ -71,8 +108,13 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_guest': typeof GuestRouteWithChildren
+ '/_private': typeof PrivateRouteWithChildren
+ '/_guest/confirm-email': typeof GuestConfirmEmailRoute
'/_guest/login': typeof GuestLoginRoute
+ '/_guest/register': typeof GuestRegisterRoute
+ '/_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
@@ -81,19 +123,38 @@ export interface FileRoutesById {
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
+ | '/confirm-email'
| '/login'
+ | '/register'
+ | '/account'
| '/cart'
+ | '/reset-password'
| '/'
| '/editor/$id'
| '/gallery/$id'
| '/gallery'
fileRoutesByTo: FileRoutesByTo
- to: '/login' | '/cart' | '/' | '/editor/$id' | '/gallery/$id' | '/gallery'
+ to:
+ | '/confirm-email'
+ | '/login'
+ | '/register'
+ | '/account'
+ | '/cart'
+ | '/reset-password'
+ | '/'
+ | '/editor/$id'
+ | '/gallery/$id'
+ | '/gallery'
id:
| '__root__'
| '/_guest'
+ | '/_private'
+ | '/_guest/confirm-email'
| '/_guest/login'
+ | '/_guest/register'
+ | '/_private/account'
| '/_public/cart'
+ | '/_shared/reset-password'
| '/_public/'
| '/_public/editor/$id'
| '/_public/gallery/$id'
@@ -102,7 +163,9 @@ export interface FileRouteTypes {
}
export interface RootRouteChildren {
GuestRoute: typeof GuestRouteWithChildren
+ PrivateRoute: typeof PrivateRouteWithChildren
PublicCartRoute: typeof PublicCartRoute
+ SharedResetPasswordRoute: typeof SharedResetPasswordRoute
PublicIndexRoute: typeof PublicIndexRoute
PublicEditorIdRoute: typeof PublicEditorIdRoute
PublicGalleryIdRoute: typeof PublicGalleryIdRoute
@@ -111,6 +174,13 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
+ '/_private': {
+ id: '/_private'
+ path: ''
+ fullPath: ''
+ preLoaderRoute: typeof PrivateRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/_guest': {
id: '/_guest'
path: ''
@@ -125,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'
@@ -132,6 +209,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PublicCartRouteImport
parentRoute: typeof rootRouteImport
}
+ '/_private/account': {
+ id: '/_private/account'
+ path: '/account'
+ fullPath: '/account'
+ preLoaderRoute: typeof PrivateAccountRouteImport
+ parentRoute: typeof PrivateRoute
+ }
+ '/_guest/register': {
+ id: '/_guest/register'
+ path: '/register'
+ fullPath: '/register'
+ preLoaderRoute: typeof GuestRegisterRouteImport
+ parentRoute: typeof GuestRoute
+ }
'/_guest/login': {
id: '/_guest/login'
path: '/login'
@@ -139,6 +230,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof GuestLoginRouteImport
parentRoute: typeof GuestRoute
}
+ '/_guest/confirm-email': {
+ id: '/_guest/confirm-email'
+ path: '/confirm-email'
+ fullPath: '/confirm-email'
+ preLoaderRoute: typeof GuestConfirmEmailRouteImport
+ parentRoute: typeof GuestRoute
+ }
'/_public/gallery/': {
id: '/_public/gallery/'
path: '/gallery'
@@ -164,18 +262,35 @@ declare module '@tanstack/react-router' {
}
interface GuestRouteChildren {
+ GuestConfirmEmailRoute: typeof GuestConfirmEmailRoute
GuestLoginRoute: typeof GuestLoginRoute
+ GuestRegisterRoute: typeof GuestRegisterRoute
}
const GuestRouteChildren: GuestRouteChildren = {
+ GuestConfirmEmailRoute: GuestConfirmEmailRoute,
GuestLoginRoute: GuestLoginRoute,
+ GuestRegisterRoute: GuestRegisterRoute,
}
const GuestRouteWithChildren = GuestRoute._addFileChildren(GuestRouteChildren)
+interface PrivateRouteChildren {
+ PrivateAccountRoute: typeof PrivateAccountRoute
+}
+
+const PrivateRouteChildren: PrivateRouteChildren = {
+ PrivateAccountRoute: PrivateAccountRoute,
+}
+
+const PrivateRouteWithChildren =
+ PrivateRoute._addFileChildren(PrivateRouteChildren)
+
const rootRouteChildren: RootRouteChildren = {
GuestRoute: GuestRouteWithChildren,
+ PrivateRoute: PrivateRouteWithChildren,
PublicCartRoute: PublicCartRoute,
+ SharedResetPasswordRoute: SharedResetPasswordRoute,
PublicIndexRoute: PublicIndexRoute,
PublicEditorIdRoute: PublicEditorIdRoute,
PublicGalleryIdRoute: PublicGalleryIdRoute,
diff --git a/src/router.tsx b/src/router.tsx
index 045d5cd..c8ea7c5 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -2,15 +2,14 @@ import { createRouter } from '@tanstack/react-router';
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query';
import { routeTree } from '@/routeTree.gen';
import * as TanstackQuery from '@/app/integrations/tanstack-query';
-import { setupApi } from '@/app/integrations/customcads-react';
-import * as auth from '@/app/stores/auth';
+import { setupApi } from '@/app/integrations/customcads-axios';
import '@/app/locales/i18n';
export type RouterContext = ReturnType;
export const getRouter = () => {
const queryContext = TanstackQuery.getContext();
- setupApi(auth.store());
+ setupApi();
const router = createRouter({
routeTree,
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 3d3eff1..916d21e 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,5 +1,6 @@
import { isAxiosError } from 'axios';
import {
+ AnyRouteMatch,
HeadContent,
Scripts,
createRootRouteWithContext,
@@ -12,24 +13,34 @@ import '@/app/config/env';
import Layout from '@/app/components/layout';
import ErrorPage from '@/app/components/error';
import { TanStackDevtools } from '@/app/integrations/tanstack-devtools';
-import cssUrl from '@/index.css?url';
+import '@/index.css';
+
+let cssUrl: string | undefined;
+if (!import.meta.env.DEV) {
+ await import('@/index.css?url').then((i) => (cssUrl = i.default));
+}
export const Route = createRootRouteWithContext()({
- head: () => ({
- meta: [
- {
- charSet: 'utf-8',
- },
- {
- name: 'viewport',
- content: 'width=device-width, initial-scale=1',
- },
- {
- title: 'CustomCADs',
- },
- ],
- links: [{ rel: 'stylesheet', href: cssUrl }],
- }),
+ head: () => {
+ const links: AnyRouteMatch['links'] = [];
+ if (cssUrl) links.push({ rel: 'stylesheet', href: cssUrl });
+
+ return {
+ meta: [
+ {
+ charSet: 'utf-8',
+ },
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ {
+ title: 'CustomCADs',
+ },
+ ],
+ links,
+ };
+ },
shellComponent: ({ children }) => (
diff --git a/src/routes/_guest/confirm-email.tsx b/src/routes/_guest/confirm-email.tsx
new file mode 100644
index 0000000..5b93f2c
--- /dev/null
+++ b/src/routes/_guest/confirm-email.tsx
@@ -0,0 +1,12 @@
+import z from 'zod';
+import { createFileRoute } from '@tanstack/react-router';
+import ConfirmEmail from '@/app/pages/guest/signup/confirm-email';
+
+export const Route = createFileRoute('/_guest/confirm-email')({
+ component: ConfirmEmail,
+ validateSearch: z.object({
+ username: z.string(),
+ token: z.string(),
+ }),
+ head: () => ({ meta: [{ title: 'CustomCADs | Confirm Email' }] }),
+});
diff --git a/src/routes/_guest/register.tsx b/src/routes/_guest/register.tsx
new file mode 100644
index 0000000..c452681
--- /dev/null
+++ b/src/routes/_guest/register.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/react-router';
+import Register from '@/app/pages/guest/signup/register';
+
+export const Route = createFileRoute('/_guest/register')({
+ component: Register,
+ head: () => ({ meta: [{ title: 'CustomCADs | Register' }] }),
+});
diff --git a/src/routes/_private.tsx b/src/routes/_private.tsx
new file mode 100644
index 0000000..cd82b42
--- /dev/null
+++ b/src/routes/_private.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute, redirect } from '@tanstack/react-router';
+import * as auth from '@/lib/utils/auth';
+import { get } from '@/lib/isomorphic/persistence';
+
+export const Route = createFileRoute('/_private')({
+ beforeLoad: () => {
+ const role = get('role');
+ const is = auth.is({ authn: !!role, authz: role ?? null });
+ if (is.guest) throw redirect({ to: '/login' });
+ },
+});
diff --git a/src/routes/_private/account.tsx b/src/routes/_private/account.tsx
new file mode 100644
index 0000000..c62b96c
--- /dev/null
+++ b/src/routes/_private/account.tsx
@@ -0,0 +1,19 @@
+import z from 'zod';
+import { createFileRoute } from '@tanstack/react-router';
+import { query } from '@customcads/react-sdk';
+import MyAccount, { tabs } from '@/app/pages/private/common/account';
+
+export const Route = createFileRoute('/_private/account')({
+ component: MyAccount,
+ validateSearch: z.object({
+ tab: z.literal(tabs).optional(),
+ }),
+ loader: async ({ context: { queryClient } }) => {
+ const { data: account } = await query.fetchQuery(
+ ({ identity }) => identity.myAccount,
+ queryClient,
+ );
+ return { account };
+ },
+ head: () => ({ meta: [{ title: 'CustomCADs | 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 };
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' }] }),
+});
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;
+};
diff --git a/vite.config.ts b/vite.config.ts
index 0856b9e..2dbc025 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,11 +5,11 @@ import * as viteHelper from './vite.helper';
export default vite.defineConfig(({ mode }) => ({
plugins: [
+ vitePlugins.tanstackStart({ mode }),
vitePlugins.react(),
vitePlugins.tsConfigPaths(),
vitePlugins.tailwindcss(),
vitePlugins.cloudflare({ enable: mode !== 'test' }),
- vitePlugins.tanstackStart({ mode }),
],
build: { assetsInlineLimit: 0 },
test: { globals: true, environment: 'jsdom' },
diff --git a/vite.plugins.ts b/vite.plugins.ts
index eba44ee..61b14b5 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: '/register',
+ 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 },