diff --git a/package-lock.json b/package-lock.json
index a8b781f..0ab6948 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-i18next": "^17.0.8",
+ "sonner": "^2.0.8",
"tailwind-merge": "^3.6.0",
"three": "^0.184.0",
"tw-animate-css": "^1.3.6",
@@ -9629,6 +9630,22 @@
"seroval-plugins": "~1.5.0"
}
},
+ "node_modules/sonner": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.8.tgz",
+ "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
diff --git a/package.json b/package.json
index a866691..6e5efdc 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-i18next": "^17.0.8",
+ "sonner": "^2.0.8",
"tailwind-merge": "^3.6.0",
"three": "^0.184.0",
"tw-animate-css": "^1.3.6",
diff --git a/src/app/components/cad/editor/index.tsx b/src/app/components/cad/editor/index.tsx
index ecb8e90..2620733 100644
--- a/src/app/components/cad/editor/index.tsx
+++ b/src/app/components/cad/editor/index.tsx
@@ -2,14 +2,14 @@ import { useQuery } from '@customcads/react-sdk';
import { AppError } from '@/types/errors';
import { getCadType } from '@/lib/cad';
import { useEditorStore } from '@/app/hooks/stores/useEditorStore';
-import { useCadBlobUrl } from '@/app/hooks/features/cads/useCadBlobUrl';
+import { useFetchCad } from '@/app/hooks/features/cads/useFetchCad';
import { useTextures } from '@/app/hooks/features/materials/useTextures';
import Loader from '@/app/components/loading';
import EditorThreeJS from './threejs';
type Props = { cadId: string };
const EditorCad = ({ cadId }: Props) => {
- const { blobUrl: cadBlobUrl, progress } = useCadBlobUrl(cadId, 'Product');
+ const { blobUrl, progress } = useFetchCad(cadId, 'Product');
const { data: cad } = useQuery(({ cads }) => cads.single({ id: cadId }));
const materialId = useEditorStore(cadId, (state) => state.materialId);
@@ -23,7 +23,7 @@ const EditorCad = ({ cadId }: Props) => {
tip: 'Reload this page or clear your browser cache.',
});
- if (!cad || !cadBlobUrl || !texture) {
+ if (!cad || !blobUrl || !texture) {
return ;
}
@@ -33,7 +33,7 @@ const EditorCad = ({ cadId }: Props) => {
texture={texture}
cad={{
id: cadId,
- blobUrl: cadBlobUrl,
+ blobUrl: blobUrl,
type: getCadType(cad.contentType as never),
coords: {
cam: cad.camCoordinates,
diff --git a/src/app/components/cad/gallery/index.tsx b/src/app/components/cad/gallery/index.tsx
index 9324bb9..b359f99 100644
--- a/src/app/components/cad/gallery/index.tsx
+++ b/src/app/components/cad/gallery/index.tsx
@@ -1,12 +1,12 @@
import { useQuery } from '@customcads/react-sdk';
import { getCadType } from '@/lib/cad';
-import { useCadBlobUrl } from '@/app/hooks/features/cads/useCadBlobUrl';
+import { useFetchCad } from '@/app/hooks/features/cads/useFetchCad';
import Loader from '@/app/components/loading';
import GalleryThreeJS from './threejs';
const GalleryCad = ({ cadId }: { cadId: string }) => {
const { data: cad } = useQuery(({ cads }) => cads.single({ id: cadId }));
- const { blobUrl, progress } = useCadBlobUrl(cadId, 'Product');
+ const { blobUrl, progress } = useFetchCad(cadId, 'Product');
return (
diff --git a/src/app/components/image.tsx b/src/app/components/image.tsx
new file mode 100644
index 0000000..9a92f42
--- /dev/null
+++ b/src/app/components/image.tsx
@@ -0,0 +1,24 @@
+import { DownloadRequest, useQuery } from '@customcads/react-sdk';
+
+type Props = {
+ request: DownloadRequest;
+ enabled?: boolean;
+} & React.ComponentProps<'img'>;
+const PresignedImage = ({ request, enabled, ...props }: Props) => {
+ const { data: image, refetch } = useQuery(({ images }) => {
+ const { queryKey, queryFn } = images.download(request);
+ return {
+ queryKey,
+ queryFn,
+ enabled,
+ staleTime: 1000 * 20,
+ gcTime: 1000 * 60,
+ };
+ });
+
+ return (
+

refetch()} {...props} />
+ );
+};
+
+export default PresignedImage;
diff --git a/src/app/components/layout/header/notifications/index.tsx b/src/app/components/layout/header/notifications/index.tsx
index 32626bc..e544aee 100644
--- a/src/app/components/layout/header/notifications/index.tsx
+++ b/src/app/components/layout/header/notifications/index.tsx
@@ -1,13 +1,12 @@
import { Bell } from 'lucide-react';
import { useInfiniteQuery } from '@customcads/react-sdk';
-import { useNotificationRealTime } from '@/app/hooks/features/notifications/useNotificationRealTime';
+import { useLiveNotifications } from '@/app/hooks/features/notifications';
import { useAuthStore } from '@/app/hooks/stores/useAuthStore';
import { popover } from '@/app/components/ui';
import CustomIcon from '@/app/components/icon';
import Scroll from './scroll';
const ALL_PARAMS = { page: 1, limit: 10 };
-const bell =
;
const NotificationsTab = () => {
const { is } = useAuthStore();
@@ -15,15 +14,17 @@ const NotificationsTab = () => {
({ notifications }) => notifications.all(ALL_PARAMS),
!is.guest,
);
- useNotificationRealTime({ allParams: ALL_PARAMS });
+ useLiveNotifications(ALL_PARAMS);
if (is.guest) return;
- if (!query.data) return bell;
+ if (!query.data) return
;
const { pages } = query.data;
return (
- {bell}
+
+
+
{
{children}
+
diff --git a/src/app/components/search/categories/index.tsx b/src/app/components/search/categories/index.tsx
index 79050f1..c628e95 100644
--- a/src/app/components/search/categories/index.tsx
+++ b/src/app/components/search/categories/index.tsx
@@ -35,10 +35,9 @@ const Categories = ({ getCategory, updateCategory }: Props) => {
}
};
- const iconClass = 'cursor-pointer w-5 h-5 md:w-5 md:h-5';
return (
-
+
({
@@ -47,12 +46,15 @@ const Categories = ({ getCategory, updateCategory }: Props) => {
}))}
onSelect={handleSelect}
>
-
+
{category}
{category !== initial && (
- handleSelect()} className={iconClass} />
+ handleSelect()}
+ className='cursor-pointer w-5 h-5 md:w-5 md:h-5'
+ />
)}
);
diff --git a/src/app/components/search/pagination/index.tsx b/src/app/components/search/pagination/index.tsx
index 4e8f290..8d47b40 100644
--- a/src/app/components/search/pagination/index.tsx
+++ b/src/app/components/search/pagination/index.tsx
@@ -18,7 +18,7 @@ const Pagination = ({ total, defaultPagination, navigate }: Props) => {
return (
-
+
{
onChange={handleChange.page}
/>
-
+
{
return (
onChange(Number(val))}>
-
+
{limit}
diff --git a/src/app/components/search/searchbar/index.tsx b/src/app/components/search/searchbar/index.tsx
index c885a0f..b557781 100644
--- a/src/app/components/search/searchbar/index.tsx
+++ b/src/app/components/search/searchbar/index.tsx
@@ -1,43 +1,46 @@
import { useState } from 'react';
-import { Search, X } from 'lucide-react';
-import { Input } from '@/app/components/ui';
+import { Search } from 'lucide-react';
+import { Children } from '@/types/react';
+import { inputGroup } from '@/app/components/ui';
type Props = {
+ preview?: (search: string) => Children['children'];
placeholder: string;
getSearch: () => string | undefined;
updateSearch: (searchTerm: string | undefined) => void;
};
-
-const Searchbar = ({ placeholder, getSearch, updateSearch }: Props) => {
+const Searchbar = ({
+ preview,
+ placeholder,
+ getSearch,
+ updateSearch,
+}: Props) => {
const [search, setSearch] = useState(getSearch());
return (
-
- {search && (
- {
- setSearch(undefined);
- updateSearch(undefined);
- }}
- className='cursor-pointer'
- />
- )}
-
+ setSearch(target.value)}
- onBlur={() => updateSearch(search)}
onKeyDown={({ key }) => key === 'Enter' && updateSearch(search)}
- className='bg-secondary border-2 rounded-xl min-h-11 md:min-h-14 md:min-w-75 md:px-6 text-ellipsis text-xs md:text-lg'
+ className='min-w-50 sm:min-w-60 md:min-w-75 lg:min-w-100 xl:min-w-120 text-ellipsis text-xs md:text-lg'
autoComplete='off'
/>
- updateSearch(search)}
- className='cursor-pointer'
- />
-
+ {preview && search && search !== getSearch() && (
+
+ {preview(search)}
+
+ )}
+
+ updateSearch(search)}
+ className='cursor-pointer size-4 lg:size-5'
+ />
+
+
);
};
diff --git a/src/app/components/search/sortings/index.tsx b/src/app/components/search/sortings/index.tsx
index 181fc68..55e11d7 100644
--- a/src/app/components/search/sortings/index.tsx
+++ b/src/app/components/search/sortings/index.tsx
@@ -42,11 +42,14 @@ const Sortings = ({ getSorting, updateSorting, sortings }: Props) => {
}
};
+ if (!sortings) return;
+
const handleSelect = (name?: string) => {
setSorting(() => name ?? initial);
updateSorting({ type: name, direction });
};
- if (!sortings) return;
+ const format = (sorting: string) =>
+ sorting.replace(/([a-z])([A-Z])/g, '$1 $2');
const DirectionArrow = direction === 'ascending' ? ArrowUp : ArrowDown;
@@ -55,18 +58,18 @@ const Sortings = ({ getSorting, updateSorting, sortings }: Props) => {
({
- value: x,
- label: x,
- }))}
+ options={sortings.map((x) => ({ value: x, label: format(x) }))}
onSelect={handleSelect}
>
-
- {sorting}
+
+ {format(sorting)}
{sorting !== initial && (
-
+
)}
);
diff --git a/src/app/components/ui/badge.tsx b/src/app/components/ui/badge.tsx
new file mode 100644
index 0000000..0359abd
--- /dev/null
+++ b/src/app/components/ui/badge.tsx
@@ -0,0 +1,47 @@
+import { Slot } from 'radix-ui';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+
+const badgeVariants = cva(
+ 'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
+ {
+ variants: {
+ variant: {
+ default:
+ 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
+ secondary:
+ 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
+ destructive:
+ 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
+ outline:
+ 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
+ ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+const Badge = ({
+ className,
+ variant = 'default',
+ asChild = false,
+ ...props
+}: React.ComponentProps<'span'> &
+ VariantProps & { asChild?: boolean }) => {
+ const Comp = asChild ? Slot.Root : 'span';
+
+ return (
+
+ );
+};
+
+export { Badge, badgeVariants };
diff --git a/src/app/components/ui/button.tsx b/src/app/components/ui/button.tsx
index 3c00c11..1fc3917 100644
--- a/src/app/components/ui/button.tsx
+++ b/src/app/components/ui/button.tsx
@@ -8,13 +8,13 @@ const buttonVariants = cva(
variants: {
variant: {
default:
- 'bg-primary text-primary-foreground hover:bg-primary/90',
+ 'bg-primary text-primary-foreground hover:bg-primary/90 select-none',
destructive:
- 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 select-none',
outline:
- 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-border dark:hover:bg-input/50',
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-border dark:hover:bg-input/50 select-none',
secondary:
- 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80 select-none',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
diff --git a/src/app/components/ui/carousel.tsx b/src/app/components/ui/carousel.tsx
index d2a952c..3aa31b4 100644
--- a/src/app/components/ui/carousel.tsx
+++ b/src/app/components/ui/carousel.tsx
@@ -192,7 +192,7 @@ const CarouselPrevious = ({
variant={variant}
size={size}
className={cn(
- 'absolute size-8 rounded-full',
+ 'absolute size-8 rounded-full cursor-pointer',
orientation === 'horizontal'
? 'top-1/2 -left-12 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
@@ -222,7 +222,7 @@ const CarouselNext = ({
variant={variant}
size={size}
className={cn(
- 'absolute size-8 rounded-full',
+ 'absolute size-8 rounded-full cursor-pointer',
orientation === 'horizontal'
? 'top-1/2 -right-12 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
diff --git a/src/app/components/ui/command.tsx b/src/app/components/ui/command.tsx
index 78ec6eb..a2f8363 100644
--- a/src/app/components/ui/command.tsx
+++ b/src/app/components/ui/command.tsx
@@ -122,7 +122,7 @@ const CommandItem = ({
) => {
+ return (
+
+ );
+};
+
+const EmptyHeader = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+const emptyMediaVariants = cva(
+ 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
+ {
+ variants: {
+ variant: {
+ default: 'bg-transparent',
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+const EmptyMedia = ({
+ className,
+ variant = 'default',
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) => {
+ return (
+
+ );
+};
+
+const EmptyTitle = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+const EmptyDescription = ({
+ className,
+ ...props
+}: React.ComponentProps<'p'>) => {
+ return (
+ a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
+ className,
+ )}
+ {...props}
+ />
+ );
+};
+
+const EmptyContent = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+export {
+ Empty as Root,
+ EmptyHeader as Header,
+ EmptyTitle as Title,
+ EmptyDescription as Description,
+ EmptyContent as Content,
+ EmptyMedia as Media,
+};
diff --git a/src/app/components/ui/index.ts b/src/app/components/ui/index.ts
index 1c91e97..4b7fb02 100644
--- a/src/app/components/ui/index.ts
+++ b/src/app/components/ui/index.ts
@@ -1,5 +1,6 @@
export * as alert from './alert';
export * as alertDialog from './alert-dialog';
+export { Badge } from './badge';
export { Button } from './button';
export * as card from './card';
export * as carousel from './carousel';
@@ -8,7 +9,10 @@ export * as collapsible from './collapsible';
export * as command from './command';
export * as dialog from './dialog';
export * as dropdownMenu from './dropdown-menu';
+export * as empty from './empty';
export { Input } from './input';
+export * as inputGroup from './input-group';
+export * as item from './item';
export { Label } from './label';
export * as navMenu from './navmenu';
export * as pagination from './pagination';
@@ -25,4 +29,5 @@ export { Slider } from './slider';
export { Spinner } from './spinner';
export { Switch } from './switch';
export * as tabs from './tabs';
+export { Textarea } from './textarea';
export * as tooltip from './tooltip';
diff --git a/src/app/components/ui/input-group.tsx b/src/app/components/ui/input-group.tsx
new file mode 100644
index 0000000..c38fa97
--- /dev/null
+++ b/src/app/components/ui/input-group.tsx
@@ -0,0 +1,158 @@
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils/index';
+import { Button } from '@/app/components/ui/button';
+import { Input } from '@/app/components/ui/input';
+import { Textarea } from '@/app/components/ui/textarea';
+
+const InputGroup = ({ className, ...props }: React.ComponentProps<'div'>) => (
+
textarea]:h-auto',
+
+ // Variants based on alignment.
+ 'has-[>[data-align=inline-start]]:[&>input]:pl-2',
+ 'has-[>[data-align=inline-end]]:[&>input]:pr-2',
+ 'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
+ 'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
+
+ // Focus state.
+ 'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50',
+
+ // Error state.
+ 'has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
+
+ className,
+ )}
+ {...props}
+ />
+);
+
+const inputGroupAddonVariants = cva(
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ align: {
+ 'inline-start':
+ 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
+ 'inline-end':
+ 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
+ 'block-start':
+ 'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3',
+ 'block-end':
+ 'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3',
+ },
+ },
+ defaultVariants: {
+ align: 'inline-start',
+ },
+ },
+);
+
+const InputGroupAddon = ({
+ className,
+ align = 'inline-start',
+ ...props
+}: React.ComponentProps<'div'> &
+ VariantProps
) => (
+ {
+ if ((e.target as HTMLElement).closest('button')) {
+ return;
+ }
+ e.currentTarget.parentElement?.querySelector('input')?.focus();
+ }}
+ {...props}
+ />
+);
+
+const inputGroupButtonVariants = cva(
+ 'flex items-center gap-2 text-sm shadow-none',
+ {
+ variants: {
+ size: {
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
+ sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
+ 'icon-xs':
+ 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
+ 'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
+ },
+ },
+ defaultVariants: {
+ size: 'xs',
+ },
+ },
+);
+
+const InputGroupButton = ({
+ className,
+ type = 'button',
+ variant = 'ghost',
+ size = 'xs',
+ ...props
+}: Omit
, 'size'> &
+ VariantProps) => (
+
+);
+
+const InputGroupText = ({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) => (
+
+);
+
+const InputGroupInput = ({
+ className,
+ ...props
+}: React.ComponentProps<'input'>) => (
+
+);
+
+const InputGroupTextarea = ({
+ className,
+ ...props
+}: React.ComponentProps<'textarea'>) => (
+
+);
+
+export {
+ InputGroup as Root,
+ InputGroupAddon as Addon,
+ InputGroupButton as Button,
+ InputGroupText as Text,
+ InputGroupInput as Input,
+ InputGroupTextarea as Textarea,
+};
diff --git a/src/app/components/ui/item.tsx b/src/app/components/ui/item.tsx
new file mode 100644
index 0000000..c3962df
--- /dev/null
+++ b/src/app/components/ui/item.tsx
@@ -0,0 +1,193 @@
+import { Slot } from 'radix-ui';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+import { Separator } from '@/app/components/ui/separator';
+
+const ItemGroup = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+const ItemSeparator = ({
+ className,
+ ...props
+}: React.ComponentProps) => {
+ return (
+
+ );
+};
+
+const itemVariants = cva(
+ 'group/item flex flex-wrap items-center rounded-md border border-transparent text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-accent/50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-transparent',
+ outline: 'border-border',
+ muted: 'bg-muted/50',
+ },
+ size: {
+ default: 'gap-4 p-4',
+ sm: 'gap-2.5 px-4 py-3',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+const Item = ({
+ className,
+ variant = 'default',
+ size = 'default',
+ asChild = false,
+ ...props
+}: React.ComponentProps<'div'> &
+ VariantProps & { asChild?: boolean }) => {
+ const Comp = asChild ? Slot.Root : 'div';
+ return (
+
+ );
+};
+
+const itemMediaVariants = cva(
+ 'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none',
+ {
+ variants: {
+ variant: {
+ default: 'bg-transparent',
+ icon: "size-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
+ image: 'size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+const ItemMedia = ({
+ className,
+ variant = 'default',
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) => {
+ return (
+
+ );
+};
+
+const ItemContent = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+const ItemTitle = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+const ItemDescription = ({
+ className,
+ ...props
+}: React.ComponentProps<'p'>) => {
+ return (
+ a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
+ className,
+ )}
+ {...props}
+ />
+ );
+};
+
+const ItemActions = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+const ItemHeader = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+const ItemFooter = ({ className, ...props }: React.ComponentProps<'div'>) => {
+ return (
+
+ );
+};
+
+export {
+ Item as Root,
+ ItemMedia as Media,
+ ItemContent as Content,
+ ItemActions as Actions,
+ ItemGroup as Group,
+ ItemSeparator as Separator,
+ ItemTitle as Title,
+ ItemDescription as Description,
+ ItemHeader as Header,
+ ItemFooter as Footer,
+};
diff --git a/src/app/components/ui/select.tsx b/src/app/components/ui/select.tsx
index 2a6d987..7446eeb 100644
--- a/src/app/components/ui/select.tsx
+++ b/src/app/components/ui/select.tsx
@@ -33,6 +33,7 @@ const SelectTrigger = ({
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
+ data-placeholder={null}
{...props}
>
{children}
diff --git a/src/app/components/ui/sonner.tsx b/src/app/components/ui/sonner.tsx
new file mode 100644
index 0000000..e4a692a
--- /dev/null
+++ b/src/app/components/ui/sonner.tsx
@@ -0,0 +1,38 @@
+import {
+ CircleCheckIcon,
+ InfoIcon,
+ Loader2Icon,
+ OctagonXIcon,
+ TriangleAlertIcon,
+} from 'lucide-react';
+import { Toaster as Sonner, type ToasterProps as Props } from 'sonner';
+import { useThemeStore } from '@/app/hooks/stores/useThemeStore';
+
+const Toaster = (props: Props) => {
+ const { theme } = useThemeStore();
+
+ return (
+ ,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
+ style={
+ {
+ '--normal-bg': 'var(--popover)',
+ '--normal-text': 'var(--popover-foreground)',
+ '--normal-border': 'var(--border)',
+ '--border-radius': 'var(--radius)',
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ );
+};
+
+export { Toaster };
diff --git a/src/app/components/ui/textarea.tsx b/src/app/components/ui/textarea.tsx
new file mode 100644
index 0000000..a3a851d
--- /dev/null
+++ b/src/app/components/ui/textarea.tsx
@@ -0,0 +1,17 @@
+import { cn } from '@/lib/utils/index';
+
+const Textarea = ({
+ className,
+ ...props
+}: React.ComponentProps<'textarea'>) => (
+
+);
+
+export { Textarea };
diff --git a/src/app/hooks/features/cads/queryOptions.ts b/src/app/hooks/features/cads/queryOptions.ts
new file mode 100644
index 0000000..5ecb590
--- /dev/null
+++ b/src/app/hooks/features/cads/queryOptions.ts
@@ -0,0 +1,36 @@
+import { DownloadResponse } from '@customcads/react-sdk';
+import { fetchFile } from '@/lib/utils';
+
+type Props = {
+ keys: unknown[];
+ enabled?: boolean;
+ getDownloadUrl: () => Promise;
+ onProgress: (progress: number) => void;
+};
+export const fetchCad = ({
+ keys,
+ enabled,
+ getDownloadUrl,
+ onProgress,
+}: Props) => ({
+ queryKey: ['fetch-cad', ...keys, getDownloadUrl, onProgress],
+ queryFn: async () => {
+ const { length, response } = await fetchFile(await getDownloadUrl());
+
+ const reader = response.body?.getReader()!;
+ const parts: BlobPart[] = [];
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ parts.push(value);
+ onProgress(value.length / length);
+ }
+
+ const blob = new Blob(parts);
+ return URL.createObjectURL(blob);
+ },
+ enabled,
+ staleTime: 1000 * 60 * 5,
+ gcTime: 1000 * 60 * 30,
+});
diff --git a/src/app/hooks/features/cads/useBlobUrlRevoker.ts b/src/app/hooks/features/cads/useBlobUrlRevoker.ts
new file mode 100644
index 0000000..238b0f2
--- /dev/null
+++ b/src/app/hooks/features/cads/useBlobUrlRevoker.ts
@@ -0,0 +1,16 @@
+import { useRef, useEffect } from 'react';
+
+export const useBlobUrlRevoker = (blobUrl: string | undefined) => {
+ const prevBlobUrlRef = useRef(null);
+
+ const revokeUrl = () => {
+ if (prevBlobUrlRef.current) {
+ URL.revokeObjectURL(prevBlobUrlRef.current);
+ }
+ };
+
+ useEffect(() => {
+ if (prevBlobUrlRef.current !== blobUrl) revokeUrl();
+ prevBlobUrlRef.current = blobUrl ?? null;
+ }, [blobUrl]);
+};
diff --git a/src/app/hooks/features/cads/useCadBlobUrl.ts b/src/app/hooks/features/cads/useCadBlobUrl.ts
deleted file mode 100644
index 1306c8b..0000000
--- a/src/app/hooks/features/cads/useCadBlobUrl.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { useEffect, useRef, useState } from 'react';
-import { DownloadRequest, useMutation } from '@customcads/react-sdk';
-import { fetchFile } from '@/lib/utils';
-
-export const useCadBlobUrl = (
- cadId: DownloadRequest['id'] | undefined,
- relationType: DownloadRequest['relationType'],
-) => {
- const { mutateAsync: downloadUrl } = useMutation(
- ({ cads }) => cads.download,
- );
- const [blobUrl, setBlobUrl] = useState(null);
- const [progress, setProgress] = useState(0);
- const isFetchingRef = useRef(false);
-
- const fetch = async (id: string) => {
- isFetchingRef.current = true;
-
- const { length, response } = await fetchFile(
- await downloadUrl({ id, relationType }),
- );
-
- const reader = response.body?.getReader()!;
- const parts: BlobPart[] = [];
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- parts.push(value);
- setProgress((prev) => prev + value.length / length);
- }
-
- const blob = new Blob(parts);
- setBlobUrl(URL.createObjectURL(blob));
-
- isFetchingRef.current = false;
- };
-
- const revokeUrl = (url: string | null) => {
- if (url) URL.revokeObjectURL(url);
- };
-
- useEffect(() => {
- if (cadId && isFetchingRef.current === false) {
- revokeUrl(blobUrl);
- fetch(cadId);
- }
-
- return () => {
- setBlobUrl((prevBlobUrl) => {
- revokeUrl(prevBlobUrl);
- return null;
- });
- };
- }, [cadId, relationType]);
-
- return { blobUrl, progress };
-};
diff --git a/src/app/hooks/features/cads/useFetchCad.ts b/src/app/hooks/features/cads/useFetchCad.ts
new file mode 100644
index 0000000..a63afe8
--- /dev/null
+++ b/src/app/hooks/features/cads/useFetchCad.ts
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+import { useMutation, type DownloadRequest } from '@customcads/react-sdk';
+import { useQuery } from '@tanstack/react-query';
+import { useBlobUrlRevoker } from './useBlobUrlRevoker';
+import * as queryOptions from './queryOptions';
+
+export const useFetchCad = (
+ cadId: DownloadRequest['id'] | undefined,
+ relationType: DownloadRequest['relationType'],
+) => {
+ const [progress, setProgress] = useState(0);
+ const { mutateAsync: downloadUrl } = useMutation(
+ ({ cads }) => cads.download,
+ );
+
+ const options = queryOptions.fetchCad({
+ keys: [cadId, relationType],
+ enabled: !!cadId,
+ getDownloadUrl: () => downloadUrl({ id: cadId!, relationType }),
+ onProgress: (progress) => setProgress((prev) => prev + progress),
+ });
+
+ const { data: blobUrl } = useQuery(options);
+ useBlobUrlRevoker(blobUrl);
+
+ return { blobUrl, progress };
+};
diff --git a/src/app/hooks/features/carts/useCartItemEditor.ts b/src/app/hooks/features/carts/useCartItemEditor.ts
index 225a89d..c3e0620 100644
--- a/src/app/hooks/features/carts/useCartItemEditor.ts
+++ b/src/app/hooks/features/carts/useCartItemEditor.ts
@@ -5,29 +5,26 @@ import { useCartStore } from '@/app/hooks/stores/useCartStore';
import { useCustomizationCreator } from '@/app/hooks/features/customizations/useCustomizationCreator';
import { useCartUpdates } from './useCartUpdates';
-export const useCartItemEditor = (productId: string) => {
+export const useCartItemEditor = (productId: string, volume: number) => {
const { items } = useCartStore();
const itemsLoaded = !!items;
const item = items?.find((i) => i.productId === productId);
const updates = useCartUpdates();
const addItemIfMissing = (customizationId: string) => {
- if (!item) {
- updates.cart.add({
- productId,
- quantity: 1,
- forDelivery: true,
- customizationId: customizationId,
- });
- return;
- }
+ if (item)
+ return { otherwise: (cb: (item: CartItem) => void) => cb(item) };
- return {
- otherwise: (callback: (item: CartItem) => void) => callback(item),
- };
+ updates.cart.add({
+ productId,
+ quantity: 1,
+ forDelivery: true,
+ customizationId: customizationId,
+ });
};
const { customization, edit: editCustomization } = useCustomizationCreator(
+ volume,
item,
itemsLoaded,
);
diff --git a/src/app/hooks/features/customizations/useCustomizationCreator.ts b/src/app/hooks/features/customizations/useCustomizationCreator.ts
index 800447d..74cdb96 100644
--- a/src/app/hooks/features/customizations/useCustomizationCreator.ts
+++ b/src/app/hooks/features/customizations/useCustomizationCreator.ts
@@ -6,6 +6,7 @@ import { useIdempotencyKeys } from '@/app/hooks/features/idempotency-keys/useIde
import { INFILL } from '@/app/constants/threejs';
export const useCustomizationCreator = (
+ volume: number,
item?: CartItem,
enableCreation?: boolean,
) => {
@@ -16,8 +17,9 @@ export const useCustomizationCreator = (
edit: useMutation(({ customizations }) => customizations.edit),
};
- const itemCustomizationId = item?.forDelivery ? item.customizationId : null;
- const customizationId = mutations.create.data?.id ?? itemCustomizationId;
+ const customizationId =
+ mutations.create.data?.id ??
+ (item?.forDelivery ? item.customizationId : null);
const { data: customization, error: error } = useQuery(
({ customizations }) => customizations.single({ id: customizationId! }),
@@ -35,7 +37,7 @@ export const useCustomizationCreator = (
color: '#ffffff',
infill: INFILL.min,
scale: 100 / 100,
- volume: 0,
+ volume,
});
}
};
diff --git a/src/app/hooks/features/idempotency-keys/useIdempotencyKeys.ts b/src/app/hooks/features/idempotency-keys/useIdempotencyKeys.ts
index c350e27..4c03429 100644
--- a/src/app/hooks/features/idempotency-keys/useIdempotencyKeys.ts
+++ b/src/app/hooks/features/idempotency-keys/useIdempotencyKeys.ts
@@ -1,10 +1,10 @@
import { useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
-export const useIdempotencyKeys = (
- names: ExactNames,
+export const useIdempotencyKeys = (
+ allNames: AllNames,
) => {
- type Name = ExactNames[number];
+ type Name = AllNames[number];
type SomeNames = readonly Name[];
const generate = (names: SomeNames) =>
@@ -14,13 +14,13 @@ export const useIdempotencyKeys = (
>;
const keysRef = useRef>(null);
- keysRef.current ??= generate(names);
+ keysRef.current ??= generate(allNames);
return {
idempotencyKeys: keysRef.current,
refreshKeys: (namesToRefresh?: SomeNames) => {
if (!namesToRefresh) {
- keysRef.current = generate(names);
+ keysRef.current = generate(allNames);
return;
}
diff --git a/src/app/hooks/features/notifications/index.ts b/src/app/hooks/features/notifications/index.ts
new file mode 100644
index 0000000..5c540e0
--- /dev/null
+++ b/src/app/hooks/features/notifications/index.ts
@@ -0,0 +1,5 @@
+export * from './useLiveNotifications';
+export * from './useNotificationQueryData';
+export * from './useNotificationStatus';
+export * from './useNotificationSync';
+export * from './useNotificationVirtualization';
diff --git a/src/app/hooks/features/notifications/useLiveNotifications.ts b/src/app/hooks/features/notifications/useLiveNotifications.ts
new file mode 100644
index 0000000..1b85ea2
--- /dev/null
+++ b/src/app/hooks/features/notifications/useLiveNotifications.ts
@@ -0,0 +1,31 @@
+import { toast } from 'sonner';
+import { AllNotificationsRequest, useMutation } from '@customcads/react-sdk';
+import { useNotificationsHub } from '@/app/hooks/hubs/useNotificationHub';
+import { useNotificationQueryData } from './useNotificationQueryData';
+
+export const useLiveNotifications = (allParams: AllNotificationsRequest) => {
+ const queries = useNotificationQueryData({
+ params: { all: allParams },
+ });
+ const { mutateAsync: read } = useMutation(
+ ({ notifications }) => notifications.read,
+ );
+
+ const raiseToast = (id: string, description: string) => {
+ const onRead = () => {
+ read({ id });
+ queries.invalidate();
+ };
+
+ toast(description, {
+ action: { label: 'Read', onClick: onRead },
+ });
+ };
+
+ useNotificationsHub('ReceiveNew', async (notification) => {
+ await queries.all.add(notification);
+ await queries.stats.increment();
+ await queries.invalidate();
+ raiseToast(notification.id, notification.description);
+ });
+};
diff --git a/src/app/hooks/features/notifications/useNotificationRealTime.ts b/src/app/hooks/features/notifications/useNotificationRealTime.ts
deleted file mode 100644
index 7aee290..0000000
--- a/src/app/hooks/features/notifications/useNotificationRealTime.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { type AllNotificationsRequest } from '@customcads/react-sdk';
-import { useNotificationsHub } from '@/app/hooks/hubs/useNotificationHub';
-import { useNotificationQueryData } from './useNotificationQueryData';
-
-type Props = {
- allParams: AllNotificationsRequest;
-};
-export const useNotificationRealTime = ({ allParams }: Props) => {
- const queries = useNotificationQueryData({
- params: { all: allParams },
- });
-
- useNotificationsHub('ReceiveNew', async (notification) => {
- await queries.all.add(notification);
- await queries.stats.increment();
- await queries.invalidate();
- });
-};
diff --git a/src/app/hooks/hubs/useNotificationHub.ts b/src/app/hooks/hubs/useNotificationHub.ts
index 8a13a6d..03d8bd1 100644
--- a/src/app/hooks/hubs/useNotificationHub.ts
+++ b/src/app/hooks/hubs/useNotificationHub.ts
@@ -17,10 +17,10 @@ export const useNotificationsHub = (
useHub({
hub: {
- connection: hubs.connect('Notifications'),
- methods: [{ name: methodName, onReceived: onSingleReceived }],
+ ...hubs.connect('Notifications'),
+ methodsToAdd: [{ name: methodName, onReceived: onSingleReceived }],
},
- condition: authn,
+ condition: !!authn && !!account,
deps: [account?.id],
});
};
diff --git a/src/app/hooks/locales/translations/common.ts b/src/app/hooks/locales/translations/common.ts
index 0243dd0..5b7f5eb 100644
--- a/src/app/hooks/locales/translations/common.ts
+++ b/src/app/hooks/locales/translations/common.ts
@@ -1,5 +1,9 @@
import { useTranslation } from '../useTranslation';
-type Common = 'locales' | 'roles' | 'loading' | 'empty' | 'errors' | 'metrics';
+type Common = 'locales' | 'roles' | 'loading' | 'errors' | 'metrics';
export const useCommonTranslations = (ns: N) =>
useTranslation(`common.${ns}`).t;
+
+type Empty = 'gallery' | 'cart';
+export const useEmptyTranslations = (ns: N) =>
+ useTranslation(`common.empty.${ns}`).t;
diff --git a/src/app/locales/bg-BG/common/empty.ts b/src/app/locales/bg-BG/common/empty.ts
deleted file mode 100644
index ee6ecda..0000000
--- a/src/app/locales/bg-BG/common/empty.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Common } from '../../types/common';
-
-export default {
- products: 'Няма намерени Продукти.',
-} satisfies Common['common.empty'];
diff --git a/src/app/locales/bg-BG/common/empty/gallery.ts b/src/app/locales/bg-BG/common/empty/gallery.ts
new file mode 100644
index 0000000..9b1cd8b
--- /dev/null
+++ b/src/app/locales/bg-BG/common/empty/gallery.ts
@@ -0,0 +1,7 @@
+import { Empty } from '@/app/locales/types/common/empty';
+
+export default {
+ title: 'Няма Продукти все още.',
+ description: 'Можеш да допринесеш като се регистрираш и качиш моделите си!',
+ link: 'Регистрирай се като Сътрудник',
+} satisfies Empty['common.empty.gallery'];
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 63e64c1..b69dcab 100644
--- a/src/app/locales/bg-BG/pages/private/common/account.ts
+++ b/src/app/locales/bg-BG/pages/private/common/account.ts
@@ -4,4 +4,5 @@ export default {
title: 'Акаунт | {{username}}',
profile: 'Профил',
access: 'Достъп',
+ info: 'Данни',
} satisfies MyAccount;
diff --git a/src/app/locales/bg-BG/pages/public/gallery/cart.ts b/src/app/locales/bg-BG/pages/public/gallery/cart.ts
index 2d59fb1..2afaeee 100644
--- a/src/app/locales/bg-BG/pages/public/gallery/cart.ts
+++ b/src/app/locales/bg-BG/pages/public/gallery/cart.ts
@@ -10,5 +10,4 @@ export default {
'print-cost': 'Разход за Принтиране',
'total-sum': 'Обща сума',
buy: 'Купи',
- 'no-items': 'Добави Продукти към тази Количка',
} satisfies Cart;
diff --git a/src/app/locales/en-GB/common/empty.ts b/src/app/locales/en-GB/common/empty.ts
deleted file mode 100644
index 9af3854..0000000
--- a/src/app/locales/en-GB/common/empty.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Common } from '../../types/common';
-
-export default {
- products: 'No Products found.',
-} satisfies Common['common.empty'];
diff --git a/src/app/locales/en-GB/common/empty/gallery.ts b/src/app/locales/en-GB/common/empty/gallery.ts
new file mode 100644
index 0000000..31931ac
--- /dev/null
+++ b/src/app/locales/en-GB/common/empty/gallery.ts
@@ -0,0 +1,8 @@
+import { Empty } from '@/app/locales/types/common/empty';
+
+export default {
+ title: 'No Products yet.',
+ description:
+ 'You can contribute contribute by signing up and uploading your models!',
+ link: 'Sign Up as a Contributor',
+} satisfies Empty['common.empty.gallery'];
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 d488779..9c06125 100644
--- a/src/app/locales/en-GB/pages/private/common/account.ts
+++ b/src/app/locales/en-GB/pages/private/common/account.ts
@@ -4,4 +4,5 @@ export default {
title: 'Account | {{username}}',
profile: 'Profile',
access: 'Access',
+ info: 'Info',
} satisfies MyAccount;
diff --git a/src/app/locales/en-GB/pages/public/gallery/cart.ts b/src/app/locales/en-GB/pages/public/gallery/cart.ts
index df1adf5..d30eb27 100644
--- a/src/app/locales/en-GB/pages/public/gallery/cart.ts
+++ b/src/app/locales/en-GB/pages/public/gallery/cart.ts
@@ -10,5 +10,4 @@ export default {
'print-cost': 'Print cost',
'total-sum': 'Total sum',
buy: 'Buy',
- 'no-items': 'Add Products to this Cart',
} satisfies Cart;
diff --git a/src/app/locales/i18n.ts b/src/app/locales/i18n.ts
index d754131..c619c0e 100644
--- a/src/app/locales/i18n.ts
+++ b/src/app/locales/i18n.ts
@@ -7,8 +7,8 @@ import {
} from '@/lib/isomorphic/locale';
import { loadTranslations } from './load-translations';
-const initialize = () => {
- i18n.use(initReactI18next).init({
+const initialize = async () => {
+ await i18n.use(initReactI18next).init({
supportedLngs: ALLOWED_LANGUAGES,
lng: getLanguageCookie(),
fallbackLng: getDefaultLanguageCookie() ?? ('en-GB' satisfies Language),
diff --git a/src/app/locales/types/common.ts b/src/app/locales/types/common.ts
index 26adbd0..06dd2fb 100644
--- a/src/app/locales/types/common.ts
+++ b/src/app/locales/types/common.ts
@@ -1,13 +1,13 @@
import { Language } from '@/types/locale';
+import { Empty } from './common/empty';
export type Common = {
'common.locales': Locales;
'common.roles': Roles;
'common.metrics': Metrics;
'common.loading': Loading;
- 'common.empty': Empty;
'common.errors': Errors;
-};
+} & Empty;
type Locales = Record;
@@ -23,10 +23,6 @@ type Loading = {
gallery: string;
};
-type Empty = {
- products: string;
-};
-
type Metrics = {
width: string;
height: string;
diff --git a/src/app/locales/types/common/empty.ts b/src/app/locales/types/common/empty.ts
new file mode 100644
index 0000000..f2d9b91
--- /dev/null
+++ b/src/app/locales/types/common/empty.ts
@@ -0,0 +1,5 @@
+export type Empty = {
+ 'common.empty.gallery': Template;
+ 'common.empty.cart': Template;
+};
+type Template = { title: string; description: string; link: string };
diff --git a/src/app/locales/types/pages/private/common.ts b/src/app/locales/types/pages/private/common.ts
index 65f942e..b00829e 100644
--- a/src/app/locales/types/pages/private/common.ts
+++ b/src/app/locales/types/pages/private/common.ts
@@ -2,6 +2,7 @@ export type MyAccount = {
title: string;
profile: string;
access: string;
+ info: string;
};
export type MyAccountShell = {
diff --git a/src/app/locales/types/pages/public/gallery.ts b/src/app/locales/types/pages/public/gallery.ts
index a3d2843..e2d9c50 100644
--- a/src/app/locales/types/pages/public/gallery.ts
+++ b/src/app/locales/types/pages/public/gallery.ts
@@ -25,7 +25,6 @@ export type Cart = {
'print-cost': string;
'total-sum': string;
buy: string;
- 'no-items': string;
};
export type Editor = {
diff --git a/src/app/pages/private/common/account/index.tsx b/src/app/pages/private/common/account/index.tsx
index e76cffd..c3613f0 100644
--- a/src/app/pages/private/common/account/index.tsx
+++ b/src/app/pages/private/common/account/index.tsx
@@ -5,10 +5,10 @@ import { usePrivateTranslations } from '@/app/hooks/locales/translations/pages/p
import Tabs from '@/app/components/tabs';
import * as page from '@/app/utils/page';
import Header from './header';
-import { Profile, Access } from './panels';
+import { Profile, Access, Info } from './panels';
import Footer from './footer';
-export const tabs = ['profile', 'access'] as const;
+export const tabs = ['profile', 'access', 'info'] as const;
export type Tab = (typeof tabs)[number];
const Route = getRouteApi('/_private/account');
@@ -31,6 +31,10 @@ const MyAccount = () => {
label: tAccount('access'),
panel: ,
},
+ info: {
+ label: tAccount('info'),
+ panel: ,
+ },
};
return (
diff --git a/src/app/pages/private/common/account/panels/index.ts b/src/app/pages/private/common/account/panels/index.ts
index 78ee05d..a241932 100644
--- a/src/app/pages/private/common/account/panels/index.ts
+++ b/src/app/pages/private/common/account/panels/index.ts
@@ -1,2 +1,3 @@
export * from './profile';
export * from './access';
+export * from './info';
diff --git a/src/app/pages/private/common/account/panels/info/index.tsx b/src/app/pages/private/common/account/panels/info/index.tsx
new file mode 100644
index 0000000..65bca85
--- /dev/null
+++ b/src/app/pages/private/common/account/panels/info/index.tsx
@@ -0,0 +1,13 @@
+import { type MyAccountResponse } from '@customcads/react-sdk';
+import { Content } from '@/app/components/ui/card';
+import Products from './products';
+
+type Props = { account: MyAccountResponse };
+export const Info = ({ account }: Props) => (
+
+
+
+);
diff --git a/src/app/pages/private/common/account/panels/profile/products/index.tsx b/src/app/pages/private/common/account/panels/info/products/index.tsx
similarity index 94%
rename from src/app/pages/private/common/account/panels/profile/products/index.tsx
rename to src/app/pages/private/common/account/panels/info/products/index.tsx
index a1833e0..c74a723 100644
--- a/src/app/pages/private/common/account/panels/profile/products/index.tsx
+++ b/src/app/pages/private/common/account/panels/info/products/index.tsx
@@ -14,7 +14,7 @@ const Products = ({ track, products }: Props) => {
items={products.map(({ id, viewedAt }) => (
))}
- className='w-65 md:w-md'
+ className='w-65 lg:w-175 xl:w-250'
>
diff --git a/src/app/pages/private/common/account/panels/profile/products/item.tsx b/src/app/pages/private/common/account/panels/info/products/item.tsx
similarity index 100%
rename from src/app/pages/private/common/account/panels/profile/products/item.tsx
rename to src/app/pages/private/common/account/panels/info/products/item.tsx
diff --git a/src/app/pages/private/common/account/panels/profile/products/remove.tsx b/src/app/pages/private/common/account/panels/info/products/remove.tsx
similarity index 100%
rename from src/app/pages/private/common/account/panels/profile/products/remove.tsx
rename to src/app/pages/private/common/account/panels/info/products/remove.tsx
diff --git a/src/app/pages/private/common/account/panels/profile/products/track.tsx b/src/app/pages/private/common/account/panels/info/products/track.tsx
similarity index 100%
rename from src/app/pages/private/common/account/panels/profile/products/track.tsx
rename to src/app/pages/private/common/account/panels/info/products/track.tsx
diff --git a/src/app/pages/private/common/account/panels/profile/index.tsx b/src/app/pages/private/common/account/panels/profile/index.tsx
index 7fa3e7c..0be64ee 100644
--- a/src/app/pages/private/common/account/panels/profile/index.tsx
+++ b/src/app/pages/private/common/account/panels/profile/index.tsx
@@ -1,22 +1,14 @@
import { type MyAccountResponse } from '@customcads/react-sdk';
-import { Separator } from '@/app/components/ui';
import { Content } from '@/app/components/ui/card';
import Names from './names';
-import Products from './products';
type Props = { account: MyAccountResponse };
export const Profile = ({ account }: Props) => (
-
+
-
-
-
);
diff --git a/src/app/pages/public/cart/aside.tsx b/src/app/pages/public/cart/aside.tsx
index 4e51d19..113d40a 100644
--- a/src/app/pages/public/cart/aside.tsx
+++ b/src/app/pages/public/cart/aside.tsx
@@ -1,16 +1,11 @@
-// import { Link } from '@tanstack/react-router';
-// import { useGalleryTranslations } from '@/app/hooks/locales/translations/pages/public';
import { useCartStore } from '@/app/hooks/stores/useCartStore';
import { useMoneyFormatter } from '@/app/hooks/locales/useMoneyFormatter';
-// import { Button } from '@/app/components/ui';
import Money from './money';
type MoneyRecord = Record;
type Props = { prices: MoneyRecord; costs: MoneyRecord };
const Aside = ({ prices, costs }: Props) => {
- // const tCart = useGalleryTranslations('cart');
const { items } = useCartStore();
-
const calculate = (money: Record) =>
Object.values(money).reduce((price, acc) => acc + price, 0);
@@ -26,38 +21,19 @@ const Aside = ({ prices, costs }: Props) => {
total: formatMoney(sum.prices + sum.costs),
};
- // TODO: Update when Payment is implemented
- const content = items?.length ? (
-
-
-
- {'Payment is currently still not supported.'}
-
- {/*
-
- */}
-
- ) : (
-
-
- {'Payment is currently still not supported.'}
-
- {/*
- {tCart('no-items')}
- */}
-
- );
+ if (!items?.length) return;
+ // TODO: Update when Payment is implemented
return (
-