@adlas/create-app 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 +476 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/figma.d.ts +16 -0
- package/dist/commands/figma.d.ts.map +1 -0
- package/dist/commands/figma.js +172 -0
- package/dist/commands/figma.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +5 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +1471 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/swagger.d.ts +16 -0
- package/dist/commands/swagger.d.ts.map +1 -0
- package/dist/commands/swagger.js +404 -0
- package/dist/commands/swagger.js.map +1 -0
- package/dist/commands/update.d.ts +15 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +93 -0
- package/dist/commands/update.js.map +1 -0
- package/package.json +63 -0
- package/templates/.vscode/extensions.json +9 -0
- package/templates/.vscode/launch.json +26 -0
- package/templates/.vscode/settings.json +67 -0
- package/templates/.vscode/tasks.json +21 -0
- package/templates/boilerplate/config/fonts.ts +10 -0
- package/templates/boilerplate/config/navigationUrls.ts +47 -0
- package/templates/boilerplate/config/site.ts +96 -0
- package/templates/boilerplate/libs/I18n.ts +15 -0
- package/templates/boilerplate/libs/I18nNavigation.ts +5 -0
- package/templates/boilerplate/libs/I18nRouting.ts +9 -0
- package/templates/boilerplate/libs/env.ts +21 -0
- package/templates/boilerplate/libs/react-query/ReactQueryProvider.tsx +21 -0
- package/templates/boilerplate/libs/react-query/index.ts +2 -0
- package/templates/boilerplate/libs/react-query/queryClient.ts +62 -0
- package/templates/boilerplate/libs/react-query/queryKeys.ts +5 -0
- package/templates/boilerplate/public/images/index.ts +1 -0
- package/templates/boilerplate/reset.d.ts +2 -0
- package/templates/boilerplate/styles/globals.css +308 -0
- package/templates/boilerplate/types/i18n.ts +10 -0
- package/templates/boilerplate/types/locale.ts +8 -0
- package/templates/boilerplate/utils/file/fileConfig.ts +123 -0
- package/templates/boilerplate/utils/file/fileValidation.ts +78 -0
- package/templates/boilerplate/utils/file/imageCompression.ts +182 -0
- package/templates/boilerplate/utils/file/index.ts +3 -0
- package/templates/boilerplate/utils/helpers.ts +55 -0
- package/templates/boilerplate/validations/auth.validation.ts +92 -0
- package/templates/boilerplate/validations/commonValidations.ts +258 -0
- package/templates/boilerplate/validations/zodErrorMap.ts +101 -0
- package/templates/configs/.env.example +8 -0
- package/templates/configs/.prettierignore +23 -0
- package/templates/configs/.prettierrc.cjs +26 -0
- package/templates/configs/.prettierrc.icons.cjs +11 -0
- package/templates/configs/Dockerfile +6 -0
- package/templates/configs/commitlint.config.ts +8 -0
- package/templates/configs/eslint.config.mjs +119 -0
- package/templates/configs/knip.config.ts +32 -0
- package/templates/configs/lefthook.yml +42 -0
- package/templates/configs/lint-staged.config.js +8 -0
- package/templates/configs/next.config.template.ts +77 -0
- package/templates/configs/next.config.ts +43 -0
- package/templates/configs/package.json +75 -0
- package/templates/configs/postcss.config.mjs +15 -0
- package/templates/configs/svgr.config.mjs +129 -0
- package/templates/configs/tsconfig.json +75 -0
- package/templates/docs/AI_QUICK_REFERENCE.md +379 -0
- package/templates/docs/ARCHITECTURE_PATTERNS.md +927 -0
- package/templates/docs/DOCUMENTATION_INDEX.md +411 -0
- package/templates/docs/FIGMA_TO_CODE_GUIDE.md +768 -0
- package/templates/docs/IMPLEMENTATION_GUIDE.md +892 -0
- package/templates/docs/PROJECT_OVERVIEW.md +302 -0
- package/templates/docs/REFACTOR_PROGRESS.md +1113 -0
- package/templates/docs/SHADCN_TO_HEROUI_MIGRATION.md +1375 -0
- package/templates/docs/UI_COMPONENTS_GUIDE.md +893 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@plugin './hero.ts';
|
|
3
|
+
@source '../../node_modules/@heroui/theme/dist';
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
@custom-variant max-2xl (@media (max-width: 1440px));
|
|
7
|
+
@custom-variant max-xl (@media (max-width: 1280px));
|
|
8
|
+
@custom-variant max-lg (@media (max-width: 1024px));
|
|
9
|
+
@custom-variant max-md (@media (max-width: 768px));
|
|
10
|
+
@custom-variant max-sm (@media (max-width: 640px));
|
|
11
|
+
@custom-variant max-xs (@media (max-width: 480px));
|
|
12
|
+
@custom-variant max-2xs (@media (max-width: 390px));
|
|
13
|
+
|
|
14
|
+
@theme {
|
|
15
|
+
/* Fonts */
|
|
16
|
+
--font-raleway: Raleway, sans-serif;
|
|
17
|
+
--font-inter: Inter, sans-serif;
|
|
18
|
+
--font-waterbrush: Waterbrush, sans-serif;
|
|
19
|
+
--default-font-family: var(--font-raleway);
|
|
20
|
+
|
|
21
|
+
/* Custom font sizes */
|
|
22
|
+
--text-2xs: 0.625rem;
|
|
23
|
+
|
|
24
|
+
/* Line heights */
|
|
25
|
+
--full: 100%;
|
|
26
|
+
|
|
27
|
+
/* Stroke widths */
|
|
28
|
+
--stroke-width-1.5: 1.5px;
|
|
29
|
+
|
|
30
|
+
/* Custom box shadows */
|
|
31
|
+
--shadow-card: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
32
|
+
|
|
33
|
+
--shadow-carousel: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
34
|
+
|
|
35
|
+
/* Custom breakpoints for mobile-first utilities (min-width) */
|
|
36
|
+
--breakpoint-2xs: 390px;
|
|
37
|
+
--breakpoint-xs: 480px;
|
|
38
|
+
/* Standard Tailwind breakpoints stay at defaults for min-width utilities */
|
|
39
|
+
/* Use max-* variants for desktop-first approach (max-width) */
|
|
40
|
+
|
|
41
|
+
--color-default-25: #fcfcfc;
|
|
42
|
+
--color-danger-25: #f8e9e7;
|
|
43
|
+
--color-success-25: #e9f9ed;
|
|
44
|
+
--color-warning-25: #fefce8;
|
|
45
|
+
--color-info-25: #e8f4f9;
|
|
46
|
+
--color-info-50: #d6ebf5;
|
|
47
|
+
--color-info-100: #add7eb;
|
|
48
|
+
--color-info-200: #85c3e1;
|
|
49
|
+
--color-info-300: #5cafd7;
|
|
50
|
+
--color-info-400: #339bcd;
|
|
51
|
+
--color-info-500: #2a7ca4;
|
|
52
|
+
--color-info-600: #215d7b;
|
|
53
|
+
--color-info-700: #183e52;
|
|
54
|
+
--color-info-800: #0f1f29;
|
|
55
|
+
--color-info-900: #060f14;
|
|
56
|
+
--color-info-DEFAULT: #2a7ca4;
|
|
57
|
+
|
|
58
|
+
/* Tertiary colors - Light theme */
|
|
59
|
+
--color-tertiary-50: #faf8f9;
|
|
60
|
+
--color-tertiary-100: #f5f0f3;
|
|
61
|
+
--color-tertiary-200: #ebe1e6;
|
|
62
|
+
--color-tertiary-300: #e0d2da;
|
|
63
|
+
--color-tertiary-400: #d2bbc5;
|
|
64
|
+
--color-tertiary-500: #c4a4b0;
|
|
65
|
+
--color-tertiary-600: #a8849a;
|
|
66
|
+
--color-tertiary-700: #8a6a7e;
|
|
67
|
+
--color-tertiary-800: #6d5364;
|
|
68
|
+
--color-tertiary-900: #523e4b;
|
|
69
|
+
--color-tertiary: #d2bbc5;
|
|
70
|
+
|
|
71
|
+
/* Custom animations */
|
|
72
|
+
--animate-flash: flash 1s ease-in-out 3;
|
|
73
|
+
--animate-scale-in: scale-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
74
|
+
|
|
75
|
+
@keyframes flash {
|
|
76
|
+
0%,
|
|
77
|
+
100% {
|
|
78
|
+
background-color: transparent;
|
|
79
|
+
}
|
|
80
|
+
50% {
|
|
81
|
+
background-color: rgb(244 244 245);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@keyframes scale-in {
|
|
86
|
+
0% {
|
|
87
|
+
transform: scale(0);
|
|
88
|
+
opacity: 0;
|
|
89
|
+
}
|
|
90
|
+
100% {
|
|
91
|
+
transform: scale(1);
|
|
92
|
+
opacity: 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@theme dark {
|
|
98
|
+
/* Tertiary colors - Dark theme */
|
|
99
|
+
--color-tertiary-50: #523e4b;
|
|
100
|
+
--color-tertiary-100: #6d5364;
|
|
101
|
+
--color-tertiary-200: #8a6a7e;
|
|
102
|
+
--color-tertiary-300: #a8849a;
|
|
103
|
+
--color-tertiary-400: #c4a4b0;
|
|
104
|
+
--color-tertiary-500: #d2bbc5;
|
|
105
|
+
--color-tertiary-600: #e0d2da;
|
|
106
|
+
--color-tertiary-700: #ebe1e6;
|
|
107
|
+
--color-tertiary-800: #f5f0f3;
|
|
108
|
+
--color-tertiary-900: #faf8f9;
|
|
109
|
+
--color-tertiary: #d2bbc5;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#__next {
|
|
113
|
+
@apply flex h-full flex-col overflow-x-hidden;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@layer base {
|
|
117
|
+
html {
|
|
118
|
+
@apply h-full w-full;
|
|
119
|
+
padding-top: env(safe-area-inset-top);
|
|
120
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
121
|
+
padding-left: env(safe-area-inset-left);
|
|
122
|
+
padding-right: env(safe-area-inset-right);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
body {
|
|
126
|
+
@apply bg-background text-primary h-dvh w-full antialiased;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
* {
|
|
130
|
+
-webkit-tap-highlight-color: transparent;
|
|
131
|
+
-webkit-font-smoothing: antialiased;
|
|
132
|
+
-moz-osx-font-smoothing: grayscale;
|
|
133
|
+
-webkit-appearance: none !important;
|
|
134
|
+
appearance: none !important;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Remove outline from all HeroUI components */
|
|
138
|
+
[data-slot] {
|
|
139
|
+
outline: none !important;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
button:focus,
|
|
143
|
+
input:focus,
|
|
144
|
+
textarea:focus,
|
|
145
|
+
select:focus,
|
|
146
|
+
[role='button']:focus,
|
|
147
|
+
[role='checkbox']:focus,
|
|
148
|
+
[role='radio']:focus,
|
|
149
|
+
[role='switch']:focus,
|
|
150
|
+
[role='slider']:focus,
|
|
151
|
+
[role='tab']:focus {
|
|
152
|
+
outline: none !important;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Remove Tailwind ring utilities */
|
|
156
|
+
.ring,
|
|
157
|
+
.ring-0,
|
|
158
|
+
.ring-1,
|
|
159
|
+
.ring-2,
|
|
160
|
+
.ring-3,
|
|
161
|
+
.ring-4,
|
|
162
|
+
.ring-8,
|
|
163
|
+
[class*='ring-'],
|
|
164
|
+
[class*='focus:ring'],
|
|
165
|
+
[class*='focus-visible:ring'] {
|
|
166
|
+
--tw-ring-shadow: 0 0 #0000 !important;
|
|
167
|
+
--tw-ring-offset-shadow: 0 0 #0000 !important;
|
|
168
|
+
box-shadow: none !important;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
*::-webkit-scrollbar {
|
|
172
|
+
@apply h-1 w-1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
*::-webkit-scrollbar-track {
|
|
176
|
+
@apply bg-default-50 w-1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
*::-webkit-scrollbar-thumb {
|
|
180
|
+
@apply bg-default-300 rounded-md border-0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@layer utilities {
|
|
185
|
+
.auth-container {
|
|
186
|
+
@apply mx-auto flex min-h-full max-w-444 flex-1 flex-row;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.dashboard-container {
|
|
190
|
+
@apply mx-auto my-18 box-border flex w-full max-w-310 flex-col gap-16;
|
|
191
|
+
@apply max-lg:max-w-full max-lg:px-4 max-md:my-10 max-md:gap-10;
|
|
192
|
+
@apply max-2xs:gap-6 max-xs:my-10 max-xs:gap-10 max-xs:px-4 max-2xs:my-6;
|
|
193
|
+
}
|
|
194
|
+
.navbar-container {
|
|
195
|
+
@apply mx-auto my-0 box-border flex max-w-7xl flex-row items-center justify-between;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.footer-container {
|
|
199
|
+
@apply bg-primary mx-auto flex w-full max-w-281 flex-col items-center py-20;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.web-container {
|
|
203
|
+
@apply max-xs:px-4 mx-auto my-0 box-border flex w-full max-w-324 flex-col px-20 max-lg:px-12 max-md:px-8 max-sm:px-6;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.header-title {
|
|
207
|
+
@apply mt-15 mb-12 text-4xl font-medium max-lg:my-8 max-lg:text-3xl;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Toast Styles - Sonner */
|
|
211
|
+
[data-sonner-toast] {
|
|
212
|
+
@apply shadow-card! font-raleway! rounded-none! border-1!;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
[data-sonner-toast][data-type='success'] {
|
|
216
|
+
@apply bg-success-25! border-success! text-success-900!;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[data-sonner-toast][data-type='success'] [data-icon] {
|
|
220
|
+
@apply text-success!;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
[data-sonner-toast][data-type='error'] {
|
|
224
|
+
@apply bg-danger-25! border-danger! text-danger-900!;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
[data-sonner-toast][data-type='error'] [data-icon] {
|
|
228
|
+
@apply text-danger!;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
[data-sonner-toast][data-type='warning'] {
|
|
232
|
+
@apply bg-warning-25! border-warning! text-warning-900!;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
[data-sonner-toast][data-type='warning'] [data-icon] {
|
|
236
|
+
@apply text-warning-700!;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
[data-sonner-toast][data-type='info'] {
|
|
240
|
+
@apply bg-info-25! border-info-DEFAULT! text-info-900!;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
[data-sonner-toast][data-type='info'] [data-icon] {
|
|
244
|
+
@apply text-info-DEFAULT!;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Toast description styling */
|
|
248
|
+
[data-sonner-toast] [data-description] {
|
|
249
|
+
@apply text-current! opacity-80!;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Toast button styling */
|
|
253
|
+
[data-sonner-toast] [data-button] {
|
|
254
|
+
@apply text-background! bg-current!;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Toast close button */
|
|
258
|
+
[data-sonner-toast] [data-close-button] {
|
|
259
|
+
@apply border-current! bg-transparent! text-current! hover:bg-current/10!;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.leaflet-container {
|
|
263
|
+
@apply h-full w-full flex-1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Grayscale filter for the entire map */
|
|
267
|
+
.leaflet-tile-pane {
|
|
268
|
+
filter: grayscale(100%) brightness(1.1) contrast(0.9);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Remove default marker styling */
|
|
272
|
+
.store-marker,
|
|
273
|
+
.store-marker-selected {
|
|
274
|
+
@apply border-none! bg-transparent!;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* Zoom control styling */
|
|
278
|
+
.leaflet-control-zoom {
|
|
279
|
+
@apply border-primary! border-1!;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.leaflet-control-zoom a {
|
|
283
|
+
@apply text-primary! border-none! bg-white!;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.leaflet-control-zoom a:hover {
|
|
287
|
+
@apply bg-secondary!;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* Attribution styling */
|
|
291
|
+
.leaflet-control-attribution {
|
|
292
|
+
@apply hidden!;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.tickCircleFillIcon {
|
|
296
|
+
@apply [&_path]:stroke-background [&_rect]:fill-success;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Editor.js content link styling */
|
|
300
|
+
.editor-js-content a {
|
|
301
|
+
@apply text-primary decoration-primary hover:text-primary/80 underline underline-offset-4 transition-all duration-200;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* Animation utilities */
|
|
305
|
+
.animate-flash {
|
|
306
|
+
animation: var(--animate-flash);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { routing } from '@/libs/I18nRouting';
|
|
2
|
+
import type messages from '@/locales/en.json';
|
|
3
|
+
|
|
4
|
+
declare module 'next-intl' {
|
|
5
|
+
// eslint-disable-next-line ts/consistent-type-definitions
|
|
6
|
+
interface AppConfig {
|
|
7
|
+
Locale: (typeof routing.locales)[number];
|
|
8
|
+
Messages: typeof messages;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { siteConfig } from '@/config/site';
|
|
2
|
+
|
|
3
|
+
export const FILE_CONFIG = {
|
|
4
|
+
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB in bytes
|
|
5
|
+
ALLOWED_EXTENSIONS: {
|
|
6
|
+
IMAGES: ['JPG', 'JPEG', 'PNG', 'WEBP', 'HEIC', 'JPEG', 'HEIF', 'AVIF'],
|
|
7
|
+
DOCUMENTS: ['PDF', 'DOC', 'DOCX'],
|
|
8
|
+
VIDEOS: ['MP4', 'MOV', 'AVI', 'MKV', 'WMV', 'FLV', 'WEBM'],
|
|
9
|
+
AUDIO: ['MP3', 'WAV', 'OGG', 'AAC'],
|
|
10
|
+
},
|
|
11
|
+
MIME_TYPE_EXTENSIONS: {
|
|
12
|
+
'image/*': ['JPG', 'PNG', 'WEBP', 'HEIC', 'JPEG', 'HEIF', 'AVIF'],
|
|
13
|
+
'application/pdf': ['PDF'],
|
|
14
|
+
'video/*': ['MP4', 'MOV', 'AVI', 'MKV', 'WEBM'],
|
|
15
|
+
'audio/*': ['MP3', 'WAV', 'OGG'],
|
|
16
|
+
},
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
// File size units for different locales
|
|
20
|
+
const FILE_SIZE_UNITS = {
|
|
21
|
+
en: ['Bytes', 'KB', 'MB', 'GB'],
|
|
22
|
+
de: ['Bytes', 'KB', 'MB', 'GB'],
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
// Helper function to format file size
|
|
26
|
+
export const formatFileSize = (bytes: number, locale: 'en' | 'de' = 'en'): string => {
|
|
27
|
+
if (bytes === 0) {
|
|
28
|
+
return `0 ${FILE_SIZE_UNITS[locale][0]}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const k = 1024;
|
|
32
|
+
const sizes = FILE_SIZE_UNITS[locale];
|
|
33
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
34
|
+
const size = Number.parseFloat((bytes / k ** i).toFixed(1));
|
|
35
|
+
|
|
36
|
+
return `${size} ${sizes[i]}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Helper function to get allowed extensions from accept string
|
|
40
|
+
export const getAllowedExtensions = (accept: string): string[] => {
|
|
41
|
+
const acceptTypes = accept.split(',').map(type => type.trim());
|
|
42
|
+
const extensions = new Set<string>();
|
|
43
|
+
|
|
44
|
+
acceptTypes.forEach(type => {
|
|
45
|
+
if (type.includes('.')) {
|
|
46
|
+
// Direct extension like .pdf, .jpg
|
|
47
|
+
extensions.add(type.replace('.', '').toUpperCase());
|
|
48
|
+
} else if (
|
|
49
|
+
FILE_CONFIG.MIME_TYPE_EXTENSIONS[type as keyof typeof FILE_CONFIG.MIME_TYPE_EXTENSIONS]
|
|
50
|
+
) {
|
|
51
|
+
// MIME type like image/*, video/*
|
|
52
|
+
const typeExtensions =
|
|
53
|
+
FILE_CONFIG.MIME_TYPE_EXTENSIONS[type as keyof typeof FILE_CONFIG.MIME_TYPE_EXTENSIONS];
|
|
54
|
+
|
|
55
|
+
typeExtensions.forEach(ext => extensions.add(ext));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return Array.from(extensions).sort();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Helper function to generate file info text
|
|
63
|
+
export const getFileInfoText = (
|
|
64
|
+
accept: string,
|
|
65
|
+
maxSize?: number,
|
|
66
|
+
locale: 'en' | 'de' = 'en',
|
|
67
|
+
formatLabel?: string,
|
|
68
|
+
maxLabel?: string,
|
|
69
|
+
): string => {
|
|
70
|
+
const allowedExtensions = getAllowedExtensions(accept);
|
|
71
|
+
const maxFileSize = maxSize || FILE_CONFIG.MAX_FILE_SIZE;
|
|
72
|
+
const formattedSize = formatFileSize(maxFileSize, locale);
|
|
73
|
+
|
|
74
|
+
const format = formatLabel || (locale === siteConfig.locales[1] ? 'Format' : 'Format');
|
|
75
|
+
const max = maxLabel || (locale === siteConfig.locales[1] ? 'max' : 'max');
|
|
76
|
+
|
|
77
|
+
return `${format}: ${allowedExtensions.join(', ')} (${max} ${formattedSize})`;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const FILE_TYPES = {
|
|
81
|
+
IMAGES: {
|
|
82
|
+
ALL: 'image/*',
|
|
83
|
+
JPEG: 'image/jpeg',
|
|
84
|
+
JPG: 'image/jpg', // Non-standard but used by some browsers
|
|
85
|
+
PNG: 'image/png',
|
|
86
|
+
WEBP: 'image/webp',
|
|
87
|
+
AVIF: 'image/avif',
|
|
88
|
+
HEIC: 'image/heic',
|
|
89
|
+
HEIF: 'image/heif',
|
|
90
|
+
GIF: 'image/gif',
|
|
91
|
+
SVG: 'image/svg+xml',
|
|
92
|
+
BMP: 'image/bmp',
|
|
93
|
+
},
|
|
94
|
+
DOCUMENTS: {
|
|
95
|
+
PDF: 'application/pdf',
|
|
96
|
+
DOC: 'application/msword',
|
|
97
|
+
DOCX: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
98
|
+
},
|
|
99
|
+
VIDEOS: {
|
|
100
|
+
ALL: 'video/*',
|
|
101
|
+
MP4: 'video/mp4',
|
|
102
|
+
MOV: 'video/quicktime',
|
|
103
|
+
AVI: 'video/x-msvideo',
|
|
104
|
+
MKV: 'video/x-matroska',
|
|
105
|
+
WMV: 'video/x-ms-wmv',
|
|
106
|
+
FLV: 'video/x-flv',
|
|
107
|
+
WEBM: 'video/webm',
|
|
108
|
+
},
|
|
109
|
+
AUDIO: {
|
|
110
|
+
ALL: 'audio/*',
|
|
111
|
+
MP3: 'audio/mpeg',
|
|
112
|
+
WAV: 'audio/wav',
|
|
113
|
+
OGG: 'audio/ogg',
|
|
114
|
+
},
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
export const ACCEPT_TYPES = {
|
|
118
|
+
IMAGES_ONLY: FILE_TYPES.IMAGES.ALL,
|
|
119
|
+
IMAGES_AND_PDF: `${FILE_TYPES.IMAGES.ALL},${FILE_TYPES.DOCUMENTS.PDF}`,
|
|
120
|
+
VIDEOS_ONLY: FILE_TYPES.VIDEOS.ALL,
|
|
121
|
+
DOCUMENTS_ONLY: FILE_TYPES.DOCUMENTS.PDF,
|
|
122
|
+
ALL_MEDIA: `${FILE_TYPES.IMAGES.ALL},${FILE_TYPES.VIDEOS.ALL},${FILE_TYPES.DOCUMENTS.PDF}`,
|
|
123
|
+
} as const;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { FILE_CONFIG } from './fileConfig';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of image validation
|
|
5
|
+
*/
|
|
6
|
+
type ImageValidationResult = {
|
|
7
|
+
valid: boolean;
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Type for i18n translation function (next-intl compatible)
|
|
13
|
+
*/
|
|
14
|
+
type TranslationFunction = (key: string, params?: Record<string, string | number>) => string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize filename to prevent path traversal and other security issues
|
|
18
|
+
* @param filename - The original filename
|
|
19
|
+
* @returns Sanitized filename
|
|
20
|
+
*/
|
|
21
|
+
export function sanitizeFilename(filename: string): string {
|
|
22
|
+
// Remove any path components
|
|
23
|
+
const name = filename.split('/').pop()?.split('\\').pop() || 'upload';
|
|
24
|
+
|
|
25
|
+
// Remove special characters except dots and hyphens
|
|
26
|
+
const sanitized = name.replace(/[^a-z0-9.-]/gi, '_');
|
|
27
|
+
|
|
28
|
+
// If no extension, add .jpg as default
|
|
29
|
+
const hasExtension = sanitized.includes('.') && sanitized.split('.').length > 1;
|
|
30
|
+
|
|
31
|
+
if (!hasExtension) {
|
|
32
|
+
return `${sanitized}.jpg`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return sanitized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate image file for Saleor upload
|
|
40
|
+
*
|
|
41
|
+
* Validates against Saleor's supported image formats:
|
|
42
|
+
* - JPEG/JPG
|
|
43
|
+
* - PNG
|
|
44
|
+
* - WebP
|
|
45
|
+
*
|
|
46
|
+
* Also checks file size (max 10MB from FILE_CONFIG)
|
|
47
|
+
*
|
|
48
|
+
* @param file - The file to validate
|
|
49
|
+
* @param t - Translation function for error messages (optional)
|
|
50
|
+
* @returns Validation result with error message if invalid
|
|
51
|
+
*/
|
|
52
|
+
export function validateImageFile(file: File, t?: TranslationFunction): ImageValidationResult {
|
|
53
|
+
// Check if file is an image (Saleor accepts any image/* MIME type)
|
|
54
|
+
if (!file.type.startsWith('image/')) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
error: t
|
|
58
|
+
? t('imageFileTypeError')
|
|
59
|
+
: 'File must be an image. Please upload a valid image file.',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check file size
|
|
64
|
+
if (file.size > FILE_CONFIG.MAX_FILE_SIZE) {
|
|
65
|
+
const maxSizeMB = FILE_CONFIG.MAX_FILE_SIZE / (1024 * 1024);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
error: t
|
|
70
|
+
? t('imageSizeError', { maxSize: maxSizeMB.toString() })
|
|
71
|
+
: `Image file size exceeds ${maxSizeMB}MB limit.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// No extension check needed - we trust the MIME type
|
|
76
|
+
// This allows files without extensions (like macOS screenshots)
|
|
77
|
+
return { valid: true };
|
|
78
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image compression utility for reducing file size while maintaining quality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
type CompressionOptions = {
|
|
6
|
+
maxWidth?: number;
|
|
7
|
+
maxHeight?: number;
|
|
8
|
+
quality?: number;
|
|
9
|
+
maxSizeKB?: number;
|
|
10
|
+
outputFormat?: 'jpeg' | 'webp' | 'png';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OPTIONS: Required<CompressionOptions> = {
|
|
14
|
+
maxWidth: 1920,
|
|
15
|
+
maxHeight: 1080,
|
|
16
|
+
quality: 0.8,
|
|
17
|
+
maxSizeKB: 2 * 1024, // 2MB
|
|
18
|
+
outputFormat: 'jpeg',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compresses an image file by resizing and adjusting quality
|
|
23
|
+
*/
|
|
24
|
+
export async function compressImage(file: File, options: CompressionOptions = {}): Promise<File> {
|
|
25
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
26
|
+
|
|
27
|
+
// Skip compression for non-image files
|
|
28
|
+
if (!file.type.startsWith('image/')) {
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Skip compression if file is already small enough
|
|
33
|
+
if (file.size <= opts.maxSizeKB * 1024) {
|
|
34
|
+
return file;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const canvas = document.createElement('canvas');
|
|
39
|
+
const ctx = canvas.getContext('2d');
|
|
40
|
+
const img = new Image();
|
|
41
|
+
|
|
42
|
+
if (!ctx) {
|
|
43
|
+
reject(new Error('Canvas context not available'));
|
|
44
|
+
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
img.onload = () => {
|
|
49
|
+
try {
|
|
50
|
+
// Calculate new dimensions while maintaining aspect ratio
|
|
51
|
+
const { width: newWidth, height: newHeight } = calculateDimensions(
|
|
52
|
+
img.width,
|
|
53
|
+
img.height,
|
|
54
|
+
opts.maxWidth,
|
|
55
|
+
opts.maxHeight,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Set canvas dimensions
|
|
59
|
+
canvas.width = newWidth;
|
|
60
|
+
canvas.height = newHeight;
|
|
61
|
+
|
|
62
|
+
// Draw and compress the image
|
|
63
|
+
ctx.drawImage(img, 0, 0, newWidth, newHeight);
|
|
64
|
+
|
|
65
|
+
// Convert to blob with compression
|
|
66
|
+
canvas.toBlob(
|
|
67
|
+
blob => {
|
|
68
|
+
if (!blob) {
|
|
69
|
+
reject(new Error('Failed to compress image'));
|
|
70
|
+
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create new file with compressed data
|
|
75
|
+
const compressedFile = new File(
|
|
76
|
+
[blob],
|
|
77
|
+
getCompressedFileName(file.name, opts.outputFormat),
|
|
78
|
+
{
|
|
79
|
+
type: `image/${opts.outputFormat}`,
|
|
80
|
+
lastModified: Date.now(),
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// If still too large, try with lower quality
|
|
85
|
+
if (compressedFile.size > opts.maxSizeKB * 1024 && opts.quality > 0.3) {
|
|
86
|
+
const lowerQualityOptions = { ...opts, quality: opts.quality * 0.7 };
|
|
87
|
+
|
|
88
|
+
compressImage(file, lowerQualityOptions).then(resolve).catch(reject);
|
|
89
|
+
} else {
|
|
90
|
+
resolve(compressedFile);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
`image/${opts.outputFormat}`,
|
|
94
|
+
opts.quality,
|
|
95
|
+
);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
reject(error);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
img.onerror = () => {
|
|
102
|
+
reject(new Error('Failed to load image'));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Load the image
|
|
106
|
+
img.src = URL.createObjectURL(file);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Calculate new dimensions while maintaining aspect ratio
|
|
112
|
+
*/
|
|
113
|
+
function calculateDimensions(
|
|
114
|
+
originalWidth: number,
|
|
115
|
+
originalHeight: number,
|
|
116
|
+
maxWidth: number,
|
|
117
|
+
maxHeight: number,
|
|
118
|
+
): { width: number; height: number } {
|
|
119
|
+
const { width, height } = { width: originalWidth, height: originalHeight };
|
|
120
|
+
|
|
121
|
+
// If image is smaller than max dimensions, don't upscale
|
|
122
|
+
if (width <= maxWidth && height <= maxHeight) {
|
|
123
|
+
return { width, height };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Calculate scaling factor
|
|
127
|
+
const widthRatio = maxWidth / width;
|
|
128
|
+
const heightRatio = maxHeight / height;
|
|
129
|
+
const scalingFactor = Math.min(widthRatio, heightRatio);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
width: Math.round(width * scalingFactor),
|
|
133
|
+
height: Math.round(height * scalingFactor),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate compressed file name
|
|
139
|
+
*/
|
|
140
|
+
function getCompressedFileName(originalName: string, outputFormat: string): string {
|
|
141
|
+
const nameWithoutExtension = originalName.replace(/\.[^/.]+$/, '');
|
|
142
|
+
|
|
143
|
+
return `${nameWithoutExtension}_compressed.${outputFormat}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get optimal compression options based on file size and type
|
|
148
|
+
*/
|
|
149
|
+
export function getOptimalCompressionOptions(file: File): CompressionOptions {
|
|
150
|
+
const fileSizeKB = file.size / 1024;
|
|
151
|
+
|
|
152
|
+
// For very large files, use more aggressive compression
|
|
153
|
+
if (fileSizeKB > 2000) {
|
|
154
|
+
return {
|
|
155
|
+
maxWidth: 1280,
|
|
156
|
+
maxHeight: 720,
|
|
157
|
+
quality: 0.7,
|
|
158
|
+
maxSizeKB: 400,
|
|
159
|
+
outputFormat: 'jpeg',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// For medium files, moderate compression
|
|
164
|
+
if (fileSizeKB > 1000) {
|
|
165
|
+
return {
|
|
166
|
+
maxWidth: 1600,
|
|
167
|
+
maxHeight: 900,
|
|
168
|
+
quality: 0.8,
|
|
169
|
+
maxSizeKB: 500,
|
|
170
|
+
outputFormat: 'jpeg',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// For smaller files, light compression
|
|
175
|
+
return {
|
|
176
|
+
maxWidth: 1920,
|
|
177
|
+
maxHeight: 1080,
|
|
178
|
+
quality: 0.85,
|
|
179
|
+
maxSizeKB: 600,
|
|
180
|
+
outputFormat: 'jpeg',
|
|
181
|
+
};
|
|
182
|
+
}
|