create-pxlr 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -0
- package/package.json +51 -0
- package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
- package/templates/blog/frontend/app/blog/page.tsx +102 -0
- package/templates/blog/frontend/app/components/footer.tsx +21 -0
- package/templates/blog/frontend/app/components/header.tsx +45 -0
- package/templates/blog/frontend/app/globals.css +30 -0
- package/templates/blog/frontend/app/layout.tsx +38 -0
- package/templates/blog/frontend/app/lib/cms.ts +71 -0
- package/templates/blog/frontend/app/page.tsx +155 -0
- package/templates/blog/frontend/next.config.ts +16 -0
- package/templates/blog/frontend/package.json +24 -0
- package/templates/blog/frontend/postcss.config.mjs +7 -0
- package/templates/blog/frontend/tsconfig.json +23 -0
- package/templates/blog/pxlr-cms/README.md +188 -0
- package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
- package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
- package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
- package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
- package/templates/clean/pxlr-cms/README.md +188 -0
- package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
- package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
- package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/clean/pxlr-cms/packages/shared/tsconfig.json +18 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as ToastPrimitives from '@radix-ui/react-toast';
|
|
5
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
6
|
+
import { X } from 'lucide-react';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
const ToastProvider = ToastPrimitives.Provider;
|
|
10
|
+
|
|
11
|
+
const ToastViewport = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<ToastPrimitives.Viewport
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(
|
|
18
|
+
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
));
|
|
24
|
+
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
|
25
|
+
|
|
26
|
+
const toastVariants = cva(
|
|
27
|
+
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
default: 'border bg-background text-foreground',
|
|
32
|
+
destructive:
|
|
33
|
+
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: 'default',
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const Toast = React.forwardRef<
|
|
43
|
+
React.ElementRef<typeof ToastPrimitives.Root>,
|
|
44
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
|
45
|
+
VariantProps<typeof toastVariants>
|
|
46
|
+
>(({ className, variant, ...props }, ref) => {
|
|
47
|
+
return (
|
|
48
|
+
<ToastPrimitives.Root
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn(toastVariants({ variant }), className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
Toast.displayName = ToastPrimitives.Root.displayName;
|
|
56
|
+
|
|
57
|
+
const ToastAction = React.forwardRef<
|
|
58
|
+
React.ElementRef<typeof ToastPrimitives.Action>,
|
|
59
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
|
60
|
+
>(({ className, ...props }, ref) => (
|
|
61
|
+
<ToastPrimitives.Action
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cn(
|
|
64
|
+
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
));
|
|
70
|
+
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
|
71
|
+
|
|
72
|
+
const ToastClose = React.forwardRef<
|
|
73
|
+
React.ElementRef<typeof ToastPrimitives.Close>,
|
|
74
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
|
75
|
+
>(({ className, ...props }, ref) => (
|
|
76
|
+
<ToastPrimitives.Close
|
|
77
|
+
ref={ref}
|
|
78
|
+
className={cn(
|
|
79
|
+
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
|
80
|
+
className
|
|
81
|
+
)}
|
|
82
|
+
toast-close=""
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
<X className="h-4 w-4" />
|
|
86
|
+
</ToastPrimitives.Close>
|
|
87
|
+
));
|
|
88
|
+
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
|
89
|
+
|
|
90
|
+
const ToastTitle = React.forwardRef<
|
|
91
|
+
React.ElementRef<typeof ToastPrimitives.Title>,
|
|
92
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
|
93
|
+
>(({ className, ...props }, ref) => (
|
|
94
|
+
<ToastPrimitives.Title
|
|
95
|
+
ref={ref}
|
|
96
|
+
className={cn('text-sm font-semibold', className)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
));
|
|
100
|
+
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
|
101
|
+
|
|
102
|
+
const ToastDescription = React.forwardRef<
|
|
103
|
+
React.ElementRef<typeof ToastPrimitives.Description>,
|
|
104
|
+
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
|
105
|
+
>(({ className, ...props }, ref) => (
|
|
106
|
+
<ToastPrimitives.Description
|
|
107
|
+
ref={ref}
|
|
108
|
+
className={cn('text-sm opacity-90', className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
));
|
|
112
|
+
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
|
113
|
+
|
|
114
|
+
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
|
115
|
+
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
type ToastProps,
|
|
119
|
+
type ToastActionElement,
|
|
120
|
+
ToastProvider,
|
|
121
|
+
ToastViewport,
|
|
122
|
+
Toast,
|
|
123
|
+
ToastTitle,
|
|
124
|
+
ToastDescription,
|
|
125
|
+
ToastClose,
|
|
126
|
+
ToastAction,
|
|
127
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Toast,
|
|
5
|
+
ToastClose,
|
|
6
|
+
ToastDescription,
|
|
7
|
+
ToastProvider,
|
|
8
|
+
ToastTitle,
|
|
9
|
+
ToastViewport,
|
|
10
|
+
} from '@/components/ui/toast';
|
|
11
|
+
import { useToast } from '@/components/ui/use-toast';
|
|
12
|
+
|
|
13
|
+
export function Toaster() {
|
|
14
|
+
const { toasts } = useToast();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<ToastProvider>
|
|
18
|
+
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
19
|
+
return (
|
|
20
|
+
<Toast key={id} {...props}>
|
|
21
|
+
<div className="grid gap-1">
|
|
22
|
+
{title && <ToastTitle>{title}</ToastTitle>}
|
|
23
|
+
{description && (
|
|
24
|
+
<ToastDescription>{description}</ToastDescription>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
{action}
|
|
28
|
+
<ToastClose />
|
|
29
|
+
</Toast>
|
|
30
|
+
);
|
|
31
|
+
})}
|
|
32
|
+
<ToastViewport />
|
|
33
|
+
</ToastProvider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
|
|
5
|
+
|
|
6
|
+
const TOAST_LIMIT = 1;
|
|
7
|
+
const TOAST_REMOVE_DELAY = 1000000;
|
|
8
|
+
|
|
9
|
+
type ToasterToast = ToastProps & {
|
|
10
|
+
id: string;
|
|
11
|
+
title?: React.ReactNode;
|
|
12
|
+
description?: React.ReactNode;
|
|
13
|
+
action?: ToastActionElement;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const actionTypes = {
|
|
17
|
+
ADD_TOAST: 'ADD_TOAST',
|
|
18
|
+
UPDATE_TOAST: 'UPDATE_TOAST',
|
|
19
|
+
DISMISS_TOAST: 'DISMISS_TOAST',
|
|
20
|
+
REMOVE_TOAST: 'REMOVE_TOAST',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
let count = 0;
|
|
24
|
+
|
|
25
|
+
function genId() {
|
|
26
|
+
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
|
27
|
+
return count.toString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ActionType = typeof actionTypes;
|
|
31
|
+
|
|
32
|
+
type Action =
|
|
33
|
+
| {
|
|
34
|
+
type: ActionType['ADD_TOAST'];
|
|
35
|
+
toast: ToasterToast;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
type: ActionType['UPDATE_TOAST'];
|
|
39
|
+
toast: Partial<ToasterToast>;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
type: ActionType['DISMISS_TOAST'];
|
|
43
|
+
toastId?: ToasterToast['id'];
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
type: ActionType['REMOVE_TOAST'];
|
|
47
|
+
toastId?: ToasterToast['id'];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
interface State {
|
|
51
|
+
toasts: ToasterToast[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
55
|
+
|
|
56
|
+
const addToRemoveQueue = (toastId: string) => {
|
|
57
|
+
if (toastTimeouts.has(toastId)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const timeout = setTimeout(() => {
|
|
62
|
+
toastTimeouts.delete(toastId);
|
|
63
|
+
dispatch({
|
|
64
|
+
type: 'REMOVE_TOAST',
|
|
65
|
+
toastId: toastId,
|
|
66
|
+
});
|
|
67
|
+
}, TOAST_REMOVE_DELAY);
|
|
68
|
+
|
|
69
|
+
toastTimeouts.set(toastId, timeout);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const reducer = (state: State, action: Action): State => {
|
|
73
|
+
switch (action.type) {
|
|
74
|
+
case 'ADD_TOAST':
|
|
75
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
case 'UPDATE_TOAST':
|
|
81
|
+
return {
|
|
82
|
+
...state,
|
|
83
|
+
toasts: state.toasts.map((t) =>
|
|
84
|
+
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
case 'DISMISS_TOAST': {
|
|
89
|
+
const { toastId } = action;
|
|
90
|
+
|
|
91
|
+
if (toastId) {
|
|
92
|
+
addToRemoveQueue(toastId);
|
|
93
|
+
} else {
|
|
94
|
+
state.toasts.forEach((toast) => {
|
|
95
|
+
addToRemoveQueue(toast.id);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
...state,
|
|
101
|
+
toasts: state.toasts.map((t) =>
|
|
102
|
+
t.id === toastId || toastId === undefined
|
|
103
|
+
? {
|
|
104
|
+
...t,
|
|
105
|
+
open: false,
|
|
106
|
+
}
|
|
107
|
+
: t
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
case 'REMOVE_TOAST':
|
|
112
|
+
if (action.toastId === undefined) {
|
|
113
|
+
return {
|
|
114
|
+
...state,
|
|
115
|
+
toasts: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const listeners: Array<(state: State) => void> = [];
|
|
126
|
+
|
|
127
|
+
let memoryState: State = { toasts: [] };
|
|
128
|
+
|
|
129
|
+
function dispatch(action: Action) {
|
|
130
|
+
memoryState = reducer(memoryState, action);
|
|
131
|
+
listeners.forEach((listener) => {
|
|
132
|
+
listener(memoryState);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type Toast = Omit<ToasterToast, 'id'>;
|
|
137
|
+
|
|
138
|
+
function toast({ ...props }: Toast) {
|
|
139
|
+
const id = genId();
|
|
140
|
+
|
|
141
|
+
const update = (props: ToasterToast) =>
|
|
142
|
+
dispatch({
|
|
143
|
+
type: 'UPDATE_TOAST',
|
|
144
|
+
toast: { ...props, id },
|
|
145
|
+
});
|
|
146
|
+
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
|
|
147
|
+
|
|
148
|
+
dispatch({
|
|
149
|
+
type: 'ADD_TOAST',
|
|
150
|
+
toast: {
|
|
151
|
+
...props,
|
|
152
|
+
id,
|
|
153
|
+
open: true,
|
|
154
|
+
onOpenChange: (open) => {
|
|
155
|
+
if (!open) dismiss();
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
id: id,
|
|
162
|
+
dismiss,
|
|
163
|
+
update,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function useToast() {
|
|
168
|
+
const [state, setState] = React.useState<State>(memoryState);
|
|
169
|
+
|
|
170
|
+
React.useEffect(() => {
|
|
171
|
+
listeners.push(setState);
|
|
172
|
+
return () => {
|
|
173
|
+
const index = listeners.indexOf(setState);
|
|
174
|
+
if (index > -1) {
|
|
175
|
+
listeners.splice(index, 1);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}, [state]);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
...state,
|
|
182
|
+
toast,
|
|
183
|
+
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { useToast, toast };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
|
2
|
+
|
|
3
|
+
class ApiClient {
|
|
4
|
+
private baseUrl: string;
|
|
5
|
+
|
|
6
|
+
constructor(baseUrl: string) {
|
|
7
|
+
this.baseUrl = baseUrl;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
private getToken(): string | null {
|
|
11
|
+
if (typeof window === 'undefined') return null;
|
|
12
|
+
const stored = localStorage.getItem('pxlr-auth');
|
|
13
|
+
if (!stored) return null;
|
|
14
|
+
try {
|
|
15
|
+
const { state } = JSON.parse(stored);
|
|
16
|
+
return state?.token || null;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async request<T>(
|
|
23
|
+
method: string,
|
|
24
|
+
endpoint: string,
|
|
25
|
+
data?: any,
|
|
26
|
+
options: RequestInit = {}
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
const token = this.getToken();
|
|
29
|
+
const headers: HeadersInit = {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
...options.headers,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (token) {
|
|
35
|
+
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
39
|
+
method,
|
|
40
|
+
headers,
|
|
41
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
42
|
+
...options,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const result = await response.json();
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(result.message || 'Request failed');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async get<T = any>(endpoint: string): Promise<T> {
|
|
55
|
+
return this.request<T>('GET', endpoint);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async post<T = any>(endpoint: string, data?: any): Promise<T> {
|
|
59
|
+
return this.request<T>('POST', endpoint, data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async put<T = any>(endpoint: string, data?: any): Promise<T> {
|
|
63
|
+
return this.request<T>('PUT', endpoint, data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async delete<T = any>(endpoint: string): Promise<T> {
|
|
67
|
+
return this.request<T>('DELETE', endpoint);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async upload<T = any>(endpoint: string, file: File): Promise<T> {
|
|
71
|
+
const token = this.getToken();
|
|
72
|
+
const formData = new FormData();
|
|
73
|
+
formData.append('file', file);
|
|
74
|
+
|
|
75
|
+
const headers: HeadersInit = {};
|
|
76
|
+
if (token) {
|
|
77
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers,
|
|
83
|
+
body: formData,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = await response.json();
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(result.message || 'Upload failed');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const api = new ApiClient(API_URL);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
4
|
+
import { translations, Locale, TranslationKey } from './translations';
|
|
5
|
+
|
|
6
|
+
interface I18nContextType {
|
|
7
|
+
locale: Locale;
|
|
8
|
+
setLocale: (locale: Locale) => void;
|
|
9
|
+
t: (key: TranslationKey) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const I18nContext = createContext<I18nContextType | null>(null);
|
|
13
|
+
|
|
14
|
+
const LOCALE_STORAGE_KEY = 'pxlr-cms-locale';
|
|
15
|
+
|
|
16
|
+
export function I18nProvider({ children }: { children: ReactNode }) {
|
|
17
|
+
const [locale, setLocaleState] = useState<Locale>('en');
|
|
18
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const stored = localStorage.getItem(LOCALE_STORAGE_KEY) as Locale | null;
|
|
22
|
+
if (stored && translations[stored]) {
|
|
23
|
+
setLocaleState(stored);
|
|
24
|
+
} else {
|
|
25
|
+
// Auto-detect from browser
|
|
26
|
+
const browserLang = navigator.language.split('-')[0];
|
|
27
|
+
if (browserLang === 'ru') {
|
|
28
|
+
setLocaleState('ru');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
setIsLoaded(true);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const setLocale = (newLocale: Locale) => {
|
|
35
|
+
setLocaleState(newLocale);
|
|
36
|
+
localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const t = (key: TranslationKey): string => {
|
|
40
|
+
return translations[locale][key] || translations.en[key] || key;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (!isLoaded) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<I18nContext.Provider value={{ locale, setLocale, t }}>
|
|
49
|
+
{children}
|
|
50
|
+
</I18nContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useI18n() {
|
|
55
|
+
const context = useContext(I18nContext);
|
|
56
|
+
if (!context) {
|
|
57
|
+
throw new Error('useI18n must be used within an I18nProvider');
|
|
58
|
+
}
|
|
59
|
+
return context;
|
|
60
|
+
}
|