@devvistatech/devvista-kit 0.0.12 → 0.0.13

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.
Files changed (84) hide show
  1. package/README.md +40 -0
  2. package/app/ClientLayout.tsx +66 -0
  3. package/app/about/page.tsx +11 -248
  4. package/app/adRequest/page.tsx +101 -25
  5. package/app/admin-profile/page.tsx +123 -0
  6. package/app/analytics/page.tsx +41 -5
  7. package/app/api/about/route.ts +2 -18
  8. package/app/api/adRequest/route.ts +7 -27
  9. package/app/api/analytics/[reportType]/route.ts +1 -64
  10. package/app/api/bio/route.ts +1 -17
  11. package/app/api/blog/route.ts +1 -19
  12. package/app/api/contacts/route.ts +1 -46
  13. package/app/api/files/route.ts +1 -15
  14. package/app/api/gallery-data/route.ts +53 -61
  15. package/app/api/schedule/route.ts +5 -21
  16. package/app/api/signup/route.ts +129 -0
  17. package/app/api/sync-user/route.ts +268 -94
  18. package/app/api/verify-admin/route.ts +46 -0
  19. package/app/blog/[id]/page.tsx +71 -52
  20. package/app/blog/page.tsx +43 -10
  21. package/app/favicon.ico +0 -0
  22. package/app/gallery/page.tsx +27 -6
  23. package/app/layout.tsx +31 -82
  24. package/app/page.tsx +20 -311
  25. package/app/products/constants/product.ts +27 -0
  26. package/app/products/page.tsx +296 -0
  27. package/app/products/productOne/page.tsx +266 -0
  28. package/app/products/productTwo/page.tsx +272 -0
  29. package/app/schedule/page.tsx +78 -40
  30. package/bin/init.js +0 -12
  31. package/components/addOns/functional/ClassList.tsx +21 -17
  32. package/components/addOns/functional/ProductList.tsx +1027 -0
  33. package/components/addOns/functional/aboutSections/AboutSection.tsx +107 -70
  34. package/components/addOns/functional/aboutSections/constants/aboutSection.ts +9 -4
  35. package/components/addOns/functional/banner/Banner.tsx +150 -0
  36. package/components/addOns/functional/banner/BannerDashboard.tsx +283 -0
  37. package/components/addOns/functional/bioSections/BioEditor.tsx +471 -0
  38. package/components/addOns/functional/bioSections/constants/bioEditor.ts +36 -0
  39. package/components/addOns/functional/blogSections/BlogDashboard.tsx +1 -1
  40. package/components/addOns/functional/blogSections/BlogFormPopUp.tsx +2 -1
  41. package/components/addOns/functional/{ImageDescCarousel.tsx → carousels/ImageDescCarousel.tsx} +166 -57
  42. package/components/addOns/functional/carousels/ProductDescCarousel.tsx +1129 -0
  43. package/components/addOns/functional/{ScheduleCarousel.tsx → carousels/ScheduleCarousel.tsx} +110 -50
  44. package/components/addOns/functional/carousels/constants.ts/productDescCarousel.ts +197 -0
  45. package/components/addOns/functional/carousels/constants.ts/scheduleCarousel.ts +20 -0
  46. package/components/addOns/functional/contactsDashboard/ContactsDashboard.tsx +1 -1
  47. package/components/addOns/functional/fileUploaders/FileUploader.tsx +437 -0
  48. package/components/addOns/functional/fileUploaders/constants/fileUploader.ts +45 -0
  49. package/components/addOns/functional/galleries/GalleryComplex.tsx +468 -267
  50. package/components/addOns/functional/galleries/GallerySimple.tsx +78 -50
  51. package/components/addOns/functional/galleries/ThreeSetGallery.tsx +260 -0
  52. package/components/addOns/functional/schedules/ScheduleGridOne.tsx +22 -8
  53. package/components/addOns/functional/schedules/ScheduleGridTwo.tsx +12 -7
  54. package/components/addOns/functional/schedules/ScheduleGridTwoBasic.tsx +12 -7
  55. package/components/addOns/non-functional/SampleCarousel.tsx +3 -3
  56. package/components/addOns/non-functional/ThreeSetGallery.tsx +3 -3
  57. package/components/addOns/non-functional/featureSections/FeaturesSection.tsx +74 -0
  58. package/components/addOns/non-functional/featureSections/constants/featuresSection.ts +30 -0
  59. package/components/addOns/non-functional/{Heros/HeroSection.tsx → heros/HomeHero.tsx} +17 -15
  60. package/components/addOns/non-functional/heros/ProductHero.tsx +111 -0
  61. package/components/addOns/non-functional/heros/constants/hero.ts +62 -0
  62. package/components/addOns/non-functional/imageCarousels/ProductSlider.tsx +6 -6
  63. package/components/addOns/non-functional/imageCarousels/ProgramCarousel.tsx +10 -10
  64. package/components/footers/footer.tsx +161 -198
  65. package/components/other/admin-menu.tsx +1 -1
  66. package/lib/auth/auth-context.tsx +225 -0
  67. package/lib/auth/auth-utils.tsx +30 -0
  68. package/lib/constants/adRequest.ts +199 -56
  69. package/lib/constants/admin-profile.ts +12 -0
  70. package/lib/constants/page.ts +15 -15
  71. package/lib/google/google-analytics-tracking.tsx +44 -0
  72. package/lib/types.ts +235 -0
  73. package/lib/utils/compressImage.tsx +32 -0
  74. package/middleware.ts +9 -5
  75. package/next.config.js +1 -1
  76. package/package.json +3 -2
  77. package/public/images/test.png +0 -0
  78. package/components/addOns/functional/BioEditor.tsx +0 -447
  79. package/components/addOns/functional/FileUploader.tsx +0 -295
  80. package/components/addOns/non-functional/FeaturesSection.tsx +0 -63
  81. package/components/types.ts +0 -50
  82. package/lib/auth-context.tsx +0 -131
  83. package/lib/verify-user.ts +0 -118
  84. /package/lib/{google-analytics.tsx → google/google-analytics.tsx} +0 -0
@@ -6,18 +6,23 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/other/tab
6
6
  import { motion, useScroll, useTransform } from "framer-motion";
7
7
  import Image from "next/image";
8
8
  import { ActionButton, EditIconButton, TrashIconButton, CloseButton, SubmitButton, CancelButton, FilterButton, DeleteButton } from "@/components/other/button";
9
- import { Upload, X } from "lucide-react";
10
- import { useUser, useAuth } from "@clerk/nextjs";
11
- import { useStrapiAuth } from "@/lib/auth-context";
9
+ import { Upload, X, Star } from "lucide-react";
10
+ import { useAuth } from "@clerk/nextjs";
11
+ import { useStrapiAuth } from "@/lib/auth/auth-context";
12
+ import { isAdminUser } from "@/lib/auth/auth-utils";
12
13
  import { GALLERY_COMPLEX } from "./constants/galleryComplex";
13
- import { StrapiUser, UploadedImage, EditFormState, Category } from "@/components/types";
14
+ import { compressImage } from "@/lib/utils/compressImage";
15
+ import { StrapiUser, UploadedImage, Category, EditFormState, UploadFormState } from "@/lib/types";
16
+
17
+ const PRODUCT_CATEGORIES: Category[] = ["indoor", "outdoor", "commercial"];
18
+ const VISIBLE_FILTER_CATEGORIES: Category[] = ["indoor", "outdoor", "commercial"];
14
19
 
15
20
  interface GallerySectionProps {
16
21
  user: StrapiUser | null;
17
22
  setUser: (user: StrapiUser | null) => void;
18
23
  authLoading: boolean;
19
24
  uploadedImages: UploadedImage[];
20
- setUploadedImages: (images: UploadedImage[]) => void;
25
+ setUploadedImages: React.Dispatch<React.SetStateAction<UploadedImage[]>>;
21
26
  error: string | null;
22
27
  setError: (error: string | null) => void;
23
28
  isLoading: boolean;
@@ -27,11 +32,23 @@ interface GallerySectionProps {
27
32
  file: File | null,
28
33
  title: string,
29
34
  description: string,
30
- category: Category
35
+ category: Category,
36
+ subCategory: string,
37
+ favorite: boolean
31
38
  ) => Promise<void>;
32
39
  handleDeleteImage: (documentId: string) => Promise<void>;
33
40
  }
34
41
 
42
+ const FavoriteIconButton = ({ isFavorite, onClick }: { isFavorite: boolean; onClick: (e: React.MouseEvent) => void }) => (
43
+ <button
44
+ onClick={onClick}
45
+ className="p-2 rounded-full transition-colors bg-yellow-500 text-white"
46
+ aria-label="Remove from favorites"
47
+ >
48
+ <Star className="h-5 w-5 fill-current" />
49
+ </button>
50
+ );
51
+
35
52
  export function GallerySection({
36
53
  user,
37
54
  setUser,
@@ -45,10 +62,9 @@ export function GallerySection({
45
62
  handleImageUpload,
46
63
  handleDeleteImage,
47
64
  }: GallerySectionProps) {
48
- const { isSignedIn } = useUser();
49
- const { getToken } = useAuth();
65
+ const { getToken, isSignedIn } = useAuth();
50
66
  const { checkSession } = useStrapiAuth();
51
- const isAdmin = isSignedIn && !!user?.businessAdminId || false;
67
+ const [isAdmin, setIsAdmin] = useState(false);
52
68
  const [activeTab, setActiveTab] = useState("photos");
53
69
  const [activeSubTab, setActiveSubTab] = useState("all");
54
70
  const [selectedImage, setSelectedImage] = useState<UploadedImage | null>(null);
@@ -64,14 +80,25 @@ export function GallerySection({
64
80
  title: "",
65
81
  description: "",
66
82
  category: "none",
83
+ subCategory: "",
67
84
  file: null,
85
+ favorite: false,
86
+ banner: false,
87
+ startDate: "",
88
+ endDate: "",
89
+ });
90
+ const [uploadForm, setUploadForm] = useState<UploadFormState>({
91
+ file: null,
92
+ title: "",
93
+ description: "",
94
+ category: "none",
95
+ subCategory: "",
96
+ favorite: false,
97
+ banner: false,
98
+ startDate: "",
99
+ endDate: "",
68
100
  });
69
- const [uploadForm, setUploadForm] = useState<{
70
- file: File | null;
71
- title: string;
72
- description: string;
73
- category: Category;
74
- }>({ file: null, title: "", description: "", category: "none" });
101
+ const [subCategories, setSubCategories] = useState<string[]>([]);
75
102
  const containerRef = useRef<HTMLDivElement>(null);
76
103
  const { scrollYProgress } = useScroll({
77
104
  target: containerRef,
@@ -79,6 +106,32 @@ export function GallerySection({
79
106
  });
80
107
  const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -50]);
81
108
 
109
+ useEffect(() => {
110
+ let isMounted = true;
111
+ const checkAdmin = async () => {
112
+ if (!isSignedIn || !user?.authId) {
113
+ if (isMounted) setIsAdmin(false);
114
+ return;
115
+ }
116
+ const adminStatus = await isAdminUser(isSignedIn, user);
117
+ if (isMounted) setIsAdmin(adminStatus);
118
+ };
119
+ checkAdmin();
120
+ return () => {
121
+ isMounted = false;
122
+ };
123
+ }, [isSignedIn, user]);
124
+
125
+ useEffect(() => {
126
+ const subCats = uploadedImages
127
+ .filter((img) => img.category === activeSubTab && typeof img.subCategory === "string" && img.subCategory !== "")
128
+ .reduce<string[]>((acc, img) => {
129
+ const subCategory = img.subCategory as string;
130
+ return acc.includes(subCategory) ? acc : [...acc, subCategory];
131
+ }, []);
132
+ setSubCategories(subCats);
133
+ }, [uploadedImages, activeSubTab]);
134
+
82
135
  const filteredImages = activeTab === "photos"
83
136
  ? activeSubTab === "all"
84
137
  ? uploadedImages
@@ -89,7 +142,7 @@ export function GallerySection({
89
142
  const handleLogin = () => checkSession();
90
143
  const handleLogout = () => checkSession();
91
144
  window.addEventListener("user-login", handleLogin);
92
- window.removeEventListener("user-logout", handleLogout);
145
+ window.addEventListener("user-logout", handleLogout);
93
146
  return () => {
94
147
  window.removeEventListener("user-login", handleLogin);
95
148
  window.removeEventListener("user-logout", handleLogout);
@@ -124,15 +177,10 @@ export function GallerySection({
124
177
  useEffect(() => {
125
178
  const handleEsc = (e: KeyboardEvent) => {
126
179
  if (e.key === "Escape") {
127
- if (isConfirmDeleteOpen) {
128
- handleCancelDelete();
129
- } else if (selectedImage) {
130
- handleCloseModal();
131
- } else if (isEditModalOpen) {
132
- handleCloseEditModal();
133
- } else if (isUploadModalOpen) {
134
- handleCloseUploadModal();
135
- }
180
+ if (isConfirmDeleteOpen) handleCancelDelete();
181
+ else if (selectedImage) handleCloseModal();
182
+ else if (isEditModalOpen) handleCloseEditModal();
183
+ else if (isUploadModalOpen) handleCloseUploadModal();
136
184
  }
137
185
  };
138
186
  window.addEventListener("keydown", handleEsc);
@@ -172,7 +220,7 @@ export function GallerySection({
172
220
  };
173
221
 
174
222
  const handleImageClick = (image: UploadedImage) => {
175
- setSelectedImage(image);
223
+ if (image.url) setSelectedImage(image);
176
224
  };
177
225
 
178
226
  const handleCloseModal = () => {
@@ -190,19 +238,75 @@ export function GallerySection({
190
238
  title: image.title?.trim() && !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) ? image.title : "",
191
239
  description: image.description || "",
192
240
  category: image.category || "none",
241
+ subCategory: image.subCategory || "",
193
242
  file: null,
243
+ favorite: image.favorite,
244
+ banner: image.banner,
245
+ startDate: image.startDate || "",
246
+ endDate: image.endDate || "",
194
247
  });
195
248
  setIsEditModalOpen(true);
196
249
  };
197
250
 
198
251
  const handleCloseEditModal = () => {
199
252
  setIsEditModalOpen(false);
200
- setEditForm({ id: 0, documentId: "", title: "", description: "", category: "none", file: null });
253
+ setEditForm({ id: 0, documentId: "", title: "", description: "", category: "none", subCategory: "", file: null, favorite: false, banner: false, startDate: "", endDate: "" });
201
254
  };
202
255
 
203
256
  const handleCloseUploadModal = () => {
204
257
  setIsUploadModalOpen(false);
205
- setUploadForm({ file: null, title: "", description: "", category: "none" });
258
+ setUploadForm({ file: null, title: "", description: "", category: "none", subCategory: "", favorite: false, banner: false, startDate: "", endDate: "" });
259
+ };
260
+
261
+ const handleToggleFavorite = async (image: UploadedImage, e: React.MouseEvent) => {
262
+ e.stopPropagation();
263
+ if (!isSignedIn) {
264
+ setError(GALLERY_COMPLEX.ERRORS.AUTHENTICATION_ERROR);
265
+ return;
266
+ }
267
+
268
+ setIsLoading(true);
269
+ try {
270
+ const token = await getToken();
271
+ if (!token) {
272
+ console.error("GallerySection: No authentication token available", { timestamp: new Date().toISOString() });
273
+ setError(GALLERY_COMPLEX.ERRORS.AUTHENTICATION_ERROR);
274
+ setUser(null);
275
+ return;
276
+ }
277
+
278
+ const formData = new FormData();
279
+ formData.append("documentId", image.documentId);
280
+ formData.append("favorite", (!image.favorite).toString());
281
+
282
+ const response = await fetch("/api/gallery-data", {
283
+ method: "PUT",
284
+ headers: {
285
+ Authorization: `Bearer ${token}`,
286
+ },
287
+ body: formData,
288
+ });
289
+
290
+ if (!response.ok) {
291
+ const errorData = await response.json();
292
+ console.error("GallerySection: Toggle favorite failed", { status: response.status, errorData, timestamp: new Date().toISOString() });
293
+ if (response.status === 401) {
294
+ setError(GALLERY_COMPLEX.ERRORS.AUTHENTICATION_ERROR);
295
+ setUser(null);
296
+ return;
297
+ }
298
+ throw new Error(errorData.error || GALLERY_COMPLEX.ERRORS.EDIT_FAILED_STATUS.replace("${response.status}", response.status.toString()));
299
+ }
300
+
301
+ const { data } = await response.json();
302
+ setUploadedImages(data || []);
303
+ setError(null);
304
+ } catch (err) {
305
+ console.error("GallerySection: Toggle Favorite Error", { error: err instanceof Error ? err.message : "Unknown error", timestamp: new Date().toISOString() });
306
+ setError(err instanceof Error ? err.message : GALLERY_COMPLEX.ERRORS.EDIT_FAILED);
307
+ } finally {
308
+ setIsLoading(false);
309
+ }
206
310
  };
207
311
 
208
312
  const handleEditImage = async (e: React.FormEvent<HTMLFormElement>) => {
@@ -210,7 +314,11 @@ export function GallerySection({
210
314
  if (!isAdmin) {
211
315
  console.error("GallerySection: Unauthorized edit attempt", {
212
316
  isSignedIn,
317
+ authId: user?.authId,
213
318
  businessAdminId: user?.businessAdminId,
319
+ userRole: user?.userRole,
320
+ businessOwner: user?.businessOwner ?? null,
321
+ timestamp: new Date().toISOString(),
214
322
  });
215
323
  setError(GALLERY_COMPLEX.ERRORS.UNAUTHORIZED_EDIT);
216
324
  return;
@@ -220,7 +328,9 @@ export function GallerySection({
220
328
  setIsLoading(true);
221
329
  const token = await getToken();
222
330
  if (!token) {
223
- console.error("GallerySection: No authentication token available");
331
+ console.error("GallerySection: No authentication token available", {
332
+ timestamp: new Date().toISOString(),
333
+ });
224
334
  setError(GALLERY_COMPLEX.ERRORS.AUTHENTICATION_ERROR);
225
335
  setUser(null);
226
336
  return;
@@ -231,8 +341,11 @@ export function GallerySection({
231
341
  formData.append("title", editForm.title || `Image ${new Date().toISOString()}`);
232
342
  formData.append("description", editForm.description || "");
233
343
  formData.append("category", editForm.category || "none");
344
+ formData.append("subCategory", editForm.subCategory || "");
345
+ formData.append("favorite", editForm.favorite.toString());
234
346
  if (editForm.file) {
235
- formData.append("file", editForm.file);
347
+ const compressedImage = await compressImage(editForm.file);
348
+ formData.append("file", compressedImage);
236
349
  }
237
350
 
238
351
  const response = await fetch("/api/gallery-data", {
@@ -245,7 +358,7 @@ export function GallerySection({
245
358
 
246
359
  if (!response.ok) {
247
360
  const errorData = await response.json();
248
- console.error("GallerySection: Edit failed", { status: response.status, errorData });
361
+ console.error("GallerySection: Edit failed", { status: response.status, errorData, timestamp: new Date().toISOString() });
249
362
  if (response.status === 401) {
250
363
  setError(GALLERY_COMPLEX.ERRORS.AUTHENTICATION_ERROR);
251
364
  setUser(null);
@@ -258,9 +371,9 @@ export function GallerySection({
258
371
  setUploadedImages(data || []);
259
372
  setError(null);
260
373
  setIsEditModalOpen(false);
261
- setEditForm({ id: 0, documentId: "", title: "", description: "", category: "none", file: null });
374
+ setEditForm({ id: 0, documentId: "", title: "", description: "", category: "none", subCategory: "", file: null, favorite: false, banner: false, startDate: "", endDate: "" });
262
375
  } catch (err) {
263
- console.error("GallerySection: Edit Error", err);
376
+ console.error("GallerySection: Edit Error", { error: err instanceof Error ? err.message : "Unknown error", timestamp: new Date().toISOString() });
264
377
  setError(err instanceof Error ? err.message : GALLERY_COMPLEX.ERRORS.EDIT_FAILED);
265
378
  } finally {
266
379
  setIsLoading(false);
@@ -272,24 +385,34 @@ export function GallerySection({
272
385
  if (!isAdmin) {
273
386
  console.error("GallerySection: Unauthorized upload attempt", {
274
387
  isSignedIn,
388
+ authId: user?.authId,
275
389
  businessAdminId: user?.businessAdminId,
390
+ userRole: user?.userRole,
391
+ businessOwner: user?.businessOwner ?? null,
392
+ timestamp: new Date().toISOString(),
276
393
  });
277
394
  setError(GALLERY_COMPLEX.ERRORS.UNAUTHORIZED_UPLOAD);
278
395
  return;
279
396
  }
280
397
  setIsLoading(true);
281
398
  try {
399
+ let compressedFile: File | null = null;
400
+ if (uploadForm.file) {
401
+ compressedFile = await compressImage(uploadForm.file);
402
+ }
282
403
  await handleImageUpload(
283
404
  e,
284
- uploadForm.file,
405
+ compressedFile,
285
406
  uploadForm.title,
286
407
  uploadForm.description,
287
- uploadForm.category || "none"
408
+ uploadForm.category || "none",
409
+ uploadForm.subCategory || "",
410
+ uploadForm.favorite
288
411
  );
289
- setUploadForm({ file: null, title: "", description: "", category: "none" });
412
+ setUploadForm({ file: null, title: "", description: "", category: "none", subCategory: "", favorite: false, banner: false, startDate: "", endDate: "" });
290
413
  setIsUploadModalOpen(false);
291
414
  } catch (err) {
292
- console.error("GallerySection: Upload Error", err);
415
+ console.error("GallerySection: Upload Error", { error: err instanceof Error ? err.message : "Unknown error", timestamp: new Date().toISOString() });
293
416
  setError(err instanceof Error ? err.message : GALLERY_COMPLEX.ERRORS.UPLOAD_FAILED);
294
417
  } finally {
295
418
  setIsLoading(false);
@@ -324,7 +447,96 @@ export function GallerySection({
324
447
  };
325
448
 
326
449
  return (
327
- <div className="w-full font-inter">
450
+ <div className="w-full">
451
+ <style jsx>{`
452
+ :root {
453
+ --jubilee: #F47C7C;
454
+ --exuberant-blue: #FF69B4;
455
+ --modern-purple: #D946EF;
456
+ --primary-color: #2563eb;
457
+ --background-color: #ffffff;
458
+ --text-color: #374151;
459
+ --border-color: #e5e7eb;
460
+ }
461
+
462
+ .glassmorphism {
463
+ background: rgba(255, 255, 255, 0.08);
464
+ backdrop-filter: blur(10px);
465
+ -webkit-backdrop-filter: blur(10px);
466
+ border: 2px solid transparent;
467
+ border-image: linear-gradient(45deg, var(--exuberant-blue), var(--jubilee), var(--modern-purple)) 1;
468
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.1);
469
+ }
470
+
471
+ .modal-glassmorphism {
472
+ background: rgba(255, 255, 255, 0.08);
473
+ backdrop-filter: blur(10px);
474
+ -webkit-backdrop-filter: blur(10px);
475
+ border: 2px solid transparent;
476
+ border-image: linear-gradient(45deg, var(--exuberant-blue), var(--jubilee), var(--modern-purple)) 1;
477
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.1);
478
+ border-radius: 2rem;
479
+ max-height: 90vh;
480
+ overflow: hidden;
481
+ }
482
+
483
+ @supports not (backdrop-filter: blur(10px)) {
484
+ .glassmorphism {
485
+ background: rgba(255, 255, 255, 0.2);
486
+ }
487
+ .modal-glassmorphism {
488
+ background: rgba(255, 255, 255, 0.2);
489
+ }
490
+ }
491
+
492
+ @media (max-width: 768px) {
493
+ .glassmorphism {
494
+ backdrop-filter: none;
495
+ background: rgba(255, 255, 255, 0.2);
496
+ }
497
+ .modal-glassmorphism {
498
+ padding: 1rem;
499
+ max-height: 85vh;
500
+ }
501
+ }
502
+
503
+ .motion-card {
504
+ will-change: transform, opacity;
505
+ }
506
+
507
+ .tabs-list {
508
+ background: var(--background-color);
509
+ border: 1px solid var(--border-color);
510
+ border-radius: 0.5rem;
511
+ padding: 0.5rem;
512
+ height: auto;
513
+ }
514
+
515
+ .tabs-trigger {
516
+ color: var(--text-color);
517
+ font-weight: 500;
518
+ font-size: 1rem;
519
+ border-radius: 0.25rem;
520
+ padding: 0.5rem 1rem;
521
+ transition: all 0.2s ease-in-out;
522
+ }
523
+
524
+ .tabs-trigger[data-state="active"] {
525
+ background: var(--primary-color);
526
+ color: #ffffff;
527
+ }
528
+
529
+ @media (max-width: 768px) {
530
+ .tabs-list {
531
+ padding: 0.25rem;
532
+ }
533
+
534
+ .tabs-trigger {
535
+ font-size: 0.875rem;
536
+ padding: 0.5rem;
537
+ }
538
+ }
539
+ `}</style>
328
540
  <motion.div
329
541
  variants={sectionVariants}
330
542
  initial="hidden"
@@ -334,13 +546,9 @@ export function GallerySection({
334
546
  ref={containerRef}
335
547
  >
336
548
  <Tabs defaultValue="photos" className="w-full" onValueChange={(value) => setActiveTab(value)}>
337
- <TabsList className="grid w-full md:w-3/4 mx-auto grid-cols-2 mb-12 bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl rounded-[2rem] p-4 h-24 supports-[not(backdrop-filter:blur(10px))]:bg-white/20">
549
+ <TabsList className="grid w-full md:w-3/4 mx-auto grid-cols-2 mb-12 tabs-list">
338
550
  {["photos", "videos"].map((tab) => (
339
- <TabsTrigger
340
- key={tab}
341
- value={tab}
342
- className="rounded-[1.5rem] text-xl sm:text-2xl font-bold text-gray-700 data-[state=active]:text-white data-[state=active]:bg-blue-600 h-full flex items-center justify-center transition-all duration-300"
343
- >
551
+ <TabsTrigger key={tab} value={tab} className="tabs-trigger">
344
552
  {tab === "photos" ? GALLERY_COMPLEX.UI.PHOTOS_TAB : GALLERY_COMPLEX.UI.VIDEOS_TAB}
345
553
  </TabsTrigger>
346
554
  ))}
@@ -354,19 +562,19 @@ export function GallerySection({
354
562
  onChange={(e) => setActiveSubTab(e.target.value)}
355
563
  className="px-4 py-2 text-base font-medium text-gray-600 bg-white rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full max-w-xs"
356
564
  >
357
- {["all", "indoor", "outdoor", "commercial"].map((subTab) => (
565
+ {["all", ...VISIBLE_FILTER_CATEGORIES].map((subTab) => (
358
566
  <option key={subTab} value={subTab}>
359
567
  {subTab === "all" ? GALLERY_COMPLEX.UI.ALL_SUB_TAB :
360
- subTab === "indoor" ? GALLERY_COMPLEX.UI.INDOOR_SUB_TAB :
361
- subTab === "outdoor" ? GALLERY_COMPLEX.UI.OUTDOOR_SUB_TAB :
362
- GALLERY_COMPLEX.UI.COMMERCIAL_SUB_TAB}
568
+ subTab === "indoor" ? GALLERY_COMPLEX.UI.CATEGORY_INDOOR :
569
+ subTab === "outdoor" ? GALLERY_COMPLEX.UI.CATEGORY_OUTDOOR :
570
+ GALLERY_COMPLEX.UI.CATEGORY_COMMERCIAL}
363
571
  </option>
364
572
  ))}
365
573
  </select>
366
574
  </div>
367
575
  ) : (
368
- <div className="flex justify-center gap-4 mb-12 max-w-2xl mx-auto">
369
- {["all", "indoor", "outdoor", "commercial"].map((subTab) => (
576
+ <div className="flex flex-wrap justify-center gap-4 mb-12 max-w-4xl mx-auto">
577
+ {["all", ...VISIBLE_FILTER_CATEGORIES].map((subTab) => (
370
578
  <FilterButton
371
579
  key={subTab}
372
580
  isActive={activeSubTab === subTab}
@@ -374,15 +582,15 @@ export function GallerySection({
374
582
  onClick={() => setActiveSubTab(subTab)}
375
583
  >
376
584
  {subTab === "all" ? GALLERY_COMPLEX.UI.ALL_SUB_TAB :
377
- subTab === "indoor" ? GALLERY_COMPLEX.UI.INDOOR_SUB_TAB :
378
- subTab === "outdoor" ? GALLERY_COMPLEX.UI.OUTDOOR_SUB_TAB :
379
- GALLERY_COMPLEX.UI.COMMERCIAL_SUB_TAB}
585
+ subTab === "indoor" ? GALLERY_COMPLEX.UI.CATEGORY_INDOOR :
586
+ subTab === "outdoor" ? GALLERY_COMPLEX.UI.CATEGORY_OUTDOOR :
587
+ GALLERY_COMPLEX.UI.CATEGORY_COMMERCIAL}
380
588
  </FilterButton>
381
589
  ))}
382
590
  </div>
383
591
  )}
384
592
 
385
- {["all", "indoor", "outdoor", "commercial"].map((subTab) => (
593
+ {["all", ...VISIBLE_FILTER_CATEGORIES].map((subTab) => (
386
594
  <TabsContent key={subTab} value={subTab}>
387
595
  <div className="space-y-6">
388
596
  {error && (
@@ -407,71 +615,76 @@ export function GallerySection({
407
615
  style={{ y: isMobile ? 0 : parallaxY }}
408
616
  >
409
617
  {filteredImages.slice(0, visiblePhotos).map((image, index) => (
410
- <motion.div
411
- key={image.documentId}
412
- variants={cardVariants}
413
- initial="hidden"
414
- whileInView="visible"
415
- viewport={{ once: true, amount: 0.2 }}
416
- onClick={() => handleImageClick(image)}
417
- className="cursor-pointer will-change-transform will-change-opacity"
418
- >
419
- <Card className="overflow-hidden bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl rounded-[1.5rem] p-2 sm:p-4 relative supports-[not(backdrop-filter:blur(10px))]:bg-white/20">
618
+ <motion.div
619
+ key={image.documentId}
620
+ variants={cardVariants}
621
+ initial="hidden"
622
+ whileInView="visible"
623
+ viewport={{ once: true, amount: 0.2 }}
624
+ onClick={() => handleImageClick(image)}
625
+ className="cursor-pointer motion-card"
626
+ >
627
+ <Card className="overflow-hidden glassmorphism rounded-[1.5rem] shadow-4xl p-2 sm:p-4 relative">
420
628
  <div className="relative w-full aspect-[3/2]">
421
- <Image
422
- src={image.url}
423
- alt={
424
- image.title?.trim() &&
629
+ <Image
630
+ src={image.url}
631
+ alt={
632
+ image.title?.trim() &&
425
633
  !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
426
- ? image.title
427
- : GALLERY_COMPLEX.UI.DEFAULT_IMAGE_ALT
428
- }
429
- fill
430
- className="object-cover object-center rounded-[1rem] transition-all duration-300"
431
- priority={index === 0}
432
- loading={index === 0 ? "eager" : "lazy"}
433
- quality={85}
434
- />
435
- </div>
436
- {isAdmin && (
437
- <div className="absolute top-2 right-2 flex space-x-2">
438
- <EditIconButton
439
- onClick={(e) => {
440
- e.stopPropagation();
441
- openEditModal(image);
442
- }}
443
- />
444
- <TrashIconButton
445
- onClick={(e) => {
446
- e.stopPropagation();
447
- openConfirmDelete(image.documentId);
448
- }}
634
+ ? image.title
635
+ : GALLERY_COMPLEX.UI.DEFAULT_IMAGE_ALT
636
+ }
637
+ fill
638
+ className="object-cover object-center rounded-[1rem] transition-all duration-300"
639
+ priority={index === 0}
640
+ loading={index === 0 ? "eager" : "lazy"}
641
+ quality={85}
449
642
  />
450
643
  </div>
451
- )}
452
- {(image.title?.trim() &&
453
- !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) ||
454
- image.description?.trim()) && (
455
- <div className="p-4">
456
- {image.title?.trim() &&
457
- !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) && (
458
- <h3 className="text-lg font-semibold text-gray-700">{image.title}</h3>
459
- )}
460
- {image.description?.trim() && (
461
- <p className="text-sm text-gray-600 line-clamp-2">{image.description}</p>
462
- )}
644
+ {isSignedIn && image.favorite && (
645
+ <div className="absolute top-2 left-2">
646
+ <FavoriteIconButton
647
+ isFavorite={image.favorite}
648
+ onClick={(e) => handleToggleFavorite(image, e)}
649
+ />
463
650
  </div>
464
651
  )}
465
- </Card>
466
- </motion.div>
652
+ {isAdmin && (
653
+ <div className="absolute top-2 right-2 flex space-x-2">
654
+ <EditIconButton
655
+ onClick={(e) => {
656
+ e.stopPropagation();
657
+ openEditModal(image);
658
+ }}
659
+ />
660
+ <TrashIconButton
661
+ onClick={(e) => {
662
+ e.stopPropagation();
663
+ openConfirmDelete(image.documentId);
664
+ }}
665
+ />
666
+ </div>
667
+ )}
668
+ {(image.title?.trim() &&
669
+ !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) ||
670
+ image.description?.trim()) && (
671
+ <div className="p-4">
672
+ {image.title?.trim() &&
673
+ !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) && (
674
+ <h3 className="text-lg font-semibold text-gray-700">{image.title}</h3>
675
+ )}
676
+ {image.description?.trim() && (
677
+ <p className="text-sm text-gray-600 line-clamp-2">{image.description}</p>
678
+ )}
679
+ </div>
680
+ )}
681
+ </Card>
682
+ </motion.div>
467
683
  ))}
468
684
  </motion.div>
469
685
  {visiblePhotos < filteredImages.length && (
470
686
  <div className="mt-8 text-center">
471
- <ActionButton
472
- onClick={loadMore}
473
- className="flex items-center gap-2"
474
- >
687
+ <ActionButton onClick={loadMore} className="flex items-center gap-2">
475
688
  {GALLERY_COMPLEX.BUTTONS.LOAD_MORE_BUTTON}
476
689
  </ActionButton>
477
690
  </div>
@@ -489,11 +702,9 @@ export function GallerySection({
489
702
  </TabsContent>
490
703
 
491
704
  <TabsContent value="videos">
492
- <div className="space-y-6">
493
- <p className="text-center text-gray-700 font-bold text-base sm:text-lg md:text-xl">
494
- {GALLERY_COMPLEX.UI.NO_VIDEOS_MESSAGE || "No videos are available"}
495
- </p>
496
- </div>
705
+ <p className="text-center text-gray-700 font-bold text-base sm:text-lg md:text-xl">
706
+ There are no videos at this time.
707
+ </p>
497
708
  </TabsContent>
498
709
  </Tabs>
499
710
  <div className="mt-4 text-center">
@@ -521,13 +732,10 @@ export function GallerySection({
521
732
  aria-labelledby="modal-image"
522
733
  >
523
734
  <div
524
- className="relative max-w-5xl w-full mx-2 sm:mx-4 max-h-[90vh] bg-gray-800/50 border border-gray-700/50 rounded-2xl shadow-lg overflow-hidden max-[768px]:p-4 max-[768px]:max-h-[85vh]"
735
+ className="relative max-w-5xl w-full mx-2 sm:mx-4 max-h-[90vh] bg-gray-800/50 border border-gray-700/50 rounded-2xl shadow-lg overflow-hidden"
525
736
  onClick={(e) => e.stopPropagation()}
526
737
  >
527
- <CloseButton
528
- variant="close-form"
529
- onClick={handleCloseModal}
530
- >
738
+ <CloseButton variant="close-form" onClick={handleCloseModal}>
531
739
  <X className="h-6 w-6 sm:h-8 sm:w-8" />
532
740
  </CloseButton>
533
741
  <div className="relative w-full h-[70vh] sm:h-[80vh]">
@@ -535,7 +743,7 @@ export function GallerySection({
535
743
  src={selectedImage.url}
536
744
  alt={
537
745
  selectedImage.title?.trim() &&
538
- !selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
746
+ !selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
539
747
  ? selectedImage.title
540
748
  : GALLERY_COMPLEX.UI.DEFAULT_IMAGE_ALT
541
749
  }
@@ -545,7 +753,8 @@ export function GallerySection({
545
753
  />
546
754
  {(selectedImage.title?.trim() &&
547
755
  !selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) ||
548
- selectedImage.description?.trim()) && (
756
+ selectedImage.description?.trim() ||
757
+ (isSignedIn && selectedImage.favorite)) && (
549
758
  <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-3 sm:p-4">
550
759
  {selectedImage.title?.trim() &&
551
760
  !selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) && (
@@ -558,6 +767,14 @@ export function GallerySection({
558
767
  {selectedImage.description}
559
768
  </p>
560
769
  )}
770
+ {isSignedIn && selectedImage.favorite && (
771
+ <div className="mt-2">
772
+ <FavoriteIconButton
773
+ isFavorite={selectedImage.favorite}
774
+ onClick={(e) => handleToggleFavorite(selectedImage, e)}
775
+ />
776
+ </div>
777
+ )}
561
778
  </div>
562
779
  )}
563
780
  </div>
@@ -578,12 +795,10 @@ export function GallerySection({
578
795
  aria-labelledby="modal-title"
579
796
  >
580
797
  <div
581
- className="relative max-w-md w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-[768px]:p-4 max-[768px]:max-h-[85vh]"
798
+ className="relative max-w-md w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg"
582
799
  onClick={(e) => e.stopPropagation()}
583
800
  >
584
- <CloseButton
585
- onClick={handleCancelDelete}
586
- >
801
+ <CloseButton onClick={handleCancelDelete}>
587
802
  <X className="h-6 w-6 sm:h-8 sm:w-8" />
588
803
  </CloseButton>
589
804
  <h3 id="modal-title" className="text-xl font-bold text-white mb-4">
@@ -593,15 +808,10 @@ export function GallerySection({
593
808
  {GALLERY_COMPLEX.UI.DELETE_CONFIRMATION_MESSAGE}
594
809
  </p>
595
810
  <div className="flex space-x-3">
596
- <DeleteButton
597
- onClick={handleConfirmDelete}
598
- disabled={isLoading}
599
- >
811
+ <DeleteButton onClick={handleConfirmDelete} disabled={isLoading}>
600
812
  {isLoading ? GALLERY_COMPLEX.BUTTONS.DELETING_BUTTON : GALLERY_COMPLEX.BUTTONS.DELETE_BUTTON}
601
813
  </DeleteButton>
602
- <CancelButton
603
- onClick={handleCancelDelete}
604
- />
814
+ <CancelButton onClick={handleCancelDelete} />
605
815
  </div>
606
816
  </div>
607
817
  </motion.div>
@@ -620,12 +830,10 @@ export function GallerySection({
620
830
  aria-labelledby="edit-modal-title"
621
831
  >
622
832
  <div
623
- className="relative max-w-md w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-[768px]:p-4 max-[768px]:max-h-[85vh]"
833
+ className="relative max-w-md w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg"
624
834
  onClick={(e) => e.stopPropagation()}
625
835
  >
626
- <CloseButton
627
- onClick={handleCloseEditModal}
628
- >
836
+ <CloseButton onClick={handleCloseEditModal}>
629
837
  <X className="h-6 w-6 sm:h-8 sm:w-8" />
630
838
  </CloseButton>
631
839
  <h3 id="edit-modal-title" className="text-xl font-bold text-white mb-4">
@@ -641,71 +849,72 @@ export function GallerySection({
641
849
  type="file"
642
850
  accept="image/jpeg,image/png,image/gif"
643
851
  onChange={(e) => setEditForm({ ...editForm, file: e.target.files?.[0] || null })}
644
- className="p-2 bg-gray-700 text-white rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
852
+ className="p-2 bg-gray-700 text-white rounded-lg w-full"
645
853
  disabled={isLoading}
646
854
  />
647
855
  {editForm.file && (
648
856
  <p className="mt-2 text-gray-300 text-sm">Selected: {editForm.file.name}</p>
649
857
  )}
650
858
  </div>
651
- <div>
652
- <label htmlFor="edit-title" className="block text-sm font-medium text-gray-300 mb-1">
653
- Image Title (optional)
654
- </label>
859
+ <input
860
+ value={editForm.title}
861
+ onChange={(e) => setEditForm({ ...editForm, title: e.target.value })}
862
+ placeholder={GALLERY_COMPLEX.UI.TITLE_PLACEHOLDER}
863
+ className="p-2 bg-gray-700 text-white rounded-lg"
864
+ disabled={isLoading}
865
+ />
866
+ <textarea
867
+ value={editForm.description}
868
+ onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
869
+ placeholder={GALLERY_COMPLEX.UI.DESCRIPTION_PLACEHOLDER}
870
+ className="p-2 bg-gray-700 text-white rounded-lg h-24"
871
+ disabled={isLoading}
872
+ />
873
+ <select
874
+ value={editForm.category || "none"}
875
+ onChange={(e) =>
876
+ setEditForm({
877
+ ...editForm,
878
+ category: e.target.value as Category,
879
+ })
880
+ }
881
+ className="p-2 bg-gray-700 text-white rounded-lg"
882
+ disabled={isLoading}
883
+ >
884
+ <option value="none">{GALLERY_COMPLEX.UI.CATEGORY_NONE}</option>
885
+ {PRODUCT_CATEGORIES.map((category) => (
886
+ <option key={category} value={category}>
887
+ {category === "indoor" ? GALLERY_COMPLEX.UI.CATEGORY_INDOOR :
888
+ category === "outdoor" ? GALLERY_COMPLEX.UI.CATEGORY_OUTDOOR :
889
+ GALLERY_COMPLEX.UI.CATEGORY_COMMERCIAL}
890
+ </option>
891
+ ))}
892
+ </select>
893
+ <input
894
+ value={editForm.subCategory}
895
+ onChange={(e) => setEditForm({ ...editForm, subCategory: e.target.value })}
896
+ placeholder="Subcategory (e.g., play-sand)"
897
+ className="p-2 bg-gray-700 text-white rounded-lg"
898
+ disabled={isLoading}
899
+ />
900
+ <div className="flex items-center space-x-2">
655
901
  <input
656
- id="edit-title"
657
- value={editForm.title}
658
- onChange={(e) => setEditForm({ ...editForm, title: e.target.value })}
659
- placeholder={GALLERY_COMPLEX.UI.TITLE_PLACEHOLDER}
660
- className="p-2 bg-gray-700 text-white rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
902
+ type="checkbox"
903
+ id="edit-favorite"
904
+ checked={editForm.favorite}
905
+ onChange={(e) => setEditForm({ ...editForm, favorite: e.target.checked })}
906
+ className="h-4 w-4 text-blue-600 rounded"
661
907
  disabled={isLoading}
662
908
  />
663
- </div>
664
- <div>
665
- <label htmlFor="edit-description" className="block text-sm font-medium text-gray-300 mb-1">
666
- Image Description (optional)
909
+ <label htmlFor="edit-favorite" className="text-sm font-medium text-gray-300">
910
+ Mark as Favorite
667
911
  </label>
668
- <textarea
669
- id="edit-description"
670
- value={editForm.description}
671
- onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
672
- placeholder={GALLERY_COMPLEX.UI.DESCRIPTION_PLACEHOLDER}
673
- className="p-2 bg-gray-700 text-white rounded-lg w-full h-24 focus:outline-none focus:ring-2 focus:ring-blue-500"
674
- disabled={isLoading}
675
- />
676
- </div>
677
- <div>
678
- <label htmlFor="edit-category" className="block text-sm font-medium text-gray-300 mb-1">
679
- Category
680
- </label>
681
- <select
682
- id="edit-category"
683
- value={editForm.category || "none"}
684
- onChange={(e) =>
685
- setEditForm({
686
- ...editForm,
687
- category: e.target.value as Category,
688
- })
689
- }
690
- className="p-2 bg-gray-700 text-white rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
691
- disabled={isLoading}
692
- >
693
- <option value="none">{GALLERY_COMPLEX.UI.CATEGORY_NONE}</option>
694
- <option value="indoor">{GALLERY_COMPLEX.UI.CATEGORY_INDOOR}</option>
695
- <option value="outdoor">{GALLERY_COMPLEX.UI.CATEGORY_OUTDOOR}</option>
696
- <option value="commercial">{GALLERY_COMPLEX.UI.CATEGORY_COMMERCIAL}</option>
697
- </select>
698
912
  </div>
699
913
  <div className="flex space-x-3">
700
- <SubmitButton
701
- type="submit"
702
- disabled={isLoading}
703
- >
914
+ <SubmitButton type="submit" disabled={isLoading}>
704
915
  {isLoading ? GALLERY_COMPLEX.BUTTONS.SAVING_BUTTON : GALLERY_COMPLEX.BUTTONS.SAVE_BUTTON}
705
916
  </SubmitButton>
706
- <CancelButton
707
- onClick={handleCloseEditModal}
708
- />
917
+ <CancelButton onClick={handleCloseEditModal} />
709
918
  </div>
710
919
  </form>
711
920
  </div>
@@ -725,101 +934,93 @@ export function GallerySection({
725
934
  aria-labelledby="upload-modal-title"
726
935
  >
727
936
  <div
728
- className="relative max-w-md w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-[768px]:p-4 max-[768px]:max-h-[85vh]"
937
+ className="relative max-w-md w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg"
729
938
  onClick={(e) => e.stopPropagation()}
730
939
  >
731
- <CloseButton
732
- onClick={handleCloseUploadModal}
733
- >
940
+ <CloseButton onClick={handleCloseUploadModal}>
734
941
  <X className="h-6 w-6 sm:h-8 sm:w-8" />
735
942
  </CloseButton>
736
943
  <h3 id="upload-modal-title" className="text-xl font-bold text-white mb-4">
737
944
  {GALLERY_COMPLEX.UI.UPLOAD_MODAL_HEADING}
738
945
  </h3>
739
- <form
740
- onSubmit={handleUploadSubmit}
741
- className="flex flex-col space-y-4"
742
- >
743
- <div>
744
- <label htmlFor="image-upload" className="block text-sm font-medium text-gray-300 mb-1">
745
- Choose Image
746
- </label>
747
- <input
748
- type="file"
749
- accept="image/jpeg,image/png,image/gif"
750
- onChange={(e) => {
751
- const file = e.target.files?.[0] || null;
752
- setUploadForm({ ...uploadForm, file });
753
- }}
754
- disabled={isLoading}
755
- className="hidden"
756
- id="image-upload"
757
- />
758
- <label htmlFor="image-upload" className="cursor-pointer">
759
- <div className="flex items-center justify-between bg-gray-700 text-white px-4 py-2 rounded-lg">
760
- <span>{uploadForm.file ? uploadForm.file.name : GALLERY_COMPLEX.UI.CHOOSE_IMAGE_TEXT}</span>
761
- <Upload className="h-4 w-4" />
762
- </div>
763
- </label>
764
- </div>
765
- <div>
766
- <label htmlFor="upload-title" className="block text-sm font-medium text-gray-300 mb-1">
767
- Image Title (optional)
768
- </label>
946
+ <form onSubmit={handleUploadSubmit} className="flex flex-col space-y-4">
947
+ <input
948
+ type="file"
949
+ accept="image/jpeg,image/png,image/gif"
950
+ onChange={(e) => {
951
+ const file = e.target.files?.[0] || null;
952
+ setUploadForm({ ...uploadForm, file });
953
+ }}
954
+ disabled={isLoading}
955
+ className="hidden"
956
+ id="image-upload"
957
+ />
958
+ <label htmlFor="image-upload" className="cursor-pointer">
959
+ <div className="flex items-center justify-between bg-gray-700 text-white px-4 py-2 rounded-lg">
960
+ <span>{uploadForm.file ? uploadForm.file.name : GALLERY_COMPLEX.UI.CHOOSE_IMAGE_TEXT}</span>
961
+ <Upload className="h-4 w-4" />
962
+ </div>
963
+ </label>
964
+ <input
965
+ value={uploadForm.title}
966
+ onChange={(e) => setUploadForm({ ...uploadForm, title: e.target.value })}
967
+ placeholder={GALLERY_COMPLEX.UI.TITLE_PLACEHOLDER}
968
+ className="p-2 bg-gray-700 text-white rounded-lg"
969
+ disabled={isLoading}
970
+ />
971
+ <textarea
972
+ value={uploadForm.description}
973
+ onChange={(e) => setUploadForm({ ...uploadForm, description: e.target.value })}
974
+ placeholder={GALLERY_COMPLEX.UI.DESCRIPTION_PLACEHOLDER}
975
+ className="p-2 bg-gray-700 text-white rounded-lg h-24"
976
+ disabled={isLoading}
977
+ />
978
+ <select
979
+ value={uploadForm.category || "none"}
980
+ onChange={(e) =>
981
+ setUploadForm({
982
+ ...uploadForm,
983
+ category: e.target.value as Category,
984
+ })
985
+ }
986
+ className="p-2 bg-gray-700 text-white rounded-lg"
987
+ disabled={isLoading}
988
+ >
989
+ <option value="none">{GALLERY_COMPLEX.UI.CATEGORY_NONE}</option>
990
+ {PRODUCT_CATEGORIES.map((category) => (
991
+ <option key={category} value={category}>
992
+ {category === "indoor" ? GALLERY_COMPLEX.UI.CATEGORY_INDOOR :
993
+ category === "outdoor" ? GALLERY_COMPLEX.UI.CATEGORY_OUTDOOR :
994
+ GALLERY_COMPLEX.UI.CATEGORY_COMMERCIAL}
995
+ </option>
996
+ ))}
997
+ </select>
998
+ <input
999
+ value={uploadForm.subCategory}
1000
+ onChange={(e) => setUploadForm({ ...uploadForm, subCategory: e.target.value })}
1001
+ placeholder="Subcategory (e.g., play-sand)"
1002
+ className="p-2 bg-gray-700 text-white rounded-lg"
1003
+ disabled={isLoading}
1004
+ autoComplete="off"
1005
+ />
1006
+ <div className="flex items-center space-x-2">
769
1007
  <input
770
- id="upload-title"
771
- value={uploadForm.title}
772
- onChange={(e) => setUploadForm({ ...uploadForm, title: e.target.value })}
773
- placeholder={GALLERY_COMPLEX.UI.TITLE_PLACEHOLDER}
774
- className="p-2 bg-gray-700 text-white rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
1008
+ type="checkbox"
1009
+ id="upload-favorite"
1010
+ checked={uploadForm.favorite}
1011
+ onChange={(e) => setUploadForm({ ...uploadForm, favorite: e.target.checked })}
1012
+ className="h-4 w-4 text-blue-600 rounded"
775
1013
  disabled={isLoading}
776
1014
  />
777
- </div>
778
- <div>
779
- <label htmlFor="upload-description" className="block text-sm font-medium text-gray-300 mb-1">
780
- Image Description (optional)
1015
+ <label htmlFor="upload-favorite" className="text-sm font-medium text-gray-300">
1016
+ Mark as Favorite
781
1017
  </label>
782
- <textarea
783
- id="upload-description"
784
- value={uploadForm.description}
785
- onChange={(e) => setUploadForm({ ...uploadForm, description: e.target.value })}
786
- placeholder={GALLERY_COMPLEX.UI.DESCRIPTION_PLACEHOLDER}
787
- className="p-2 bg-gray-700 text-white rounded-lg w-full h-24 focus:outline-none focus:ring-2 focus:ring-blue-500"
788
- disabled={isLoading}
789
- />
790
- </div>
791
- <div>
792
- <label htmlFor="upload-category" className="block text-sm font-medium text-gray-300 mb-1">
793
- Category
794
- </label>
795
- <select
796
- id="upload-category"
797
- value={uploadForm.category || "none"}
798
- onChange={(e) =>
799
- setUploadForm({
800
- ...uploadForm,
801
- category: e.target.value as Category,
802
- })
803
- }
804
- className="p-2 bg-gray-700 text-white rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
805
- disabled={isLoading}
806
- >
807
- <option value="none">{GALLERY_COMPLEX.UI.CATEGORY_NONE}</option>
808
- <option value="indoor">{GALLERY_COMPLEX.UI.CATEGORY_INDOOR}</option>
809
- <option value="outdoor">{GALLERY_COMPLEX.UI.CATEGORY_OUTDOOR}</option>
810
- <option value="commercial">{GALLERY_COMPLEX.UI.CATEGORY_COMMERCIAL}</option>
811
- </select>
812
1018
  </div>
813
1019
  <div className="flex space-x-3">
814
- <SubmitButton
815
- type="submit"
816
- disabled={isLoading || !uploadForm.file}
817
- >
1020
+ <SubmitButton type="submit" disabled={isLoading || !uploadForm.file}>
818
1021
  {isLoading ? GALLERY_COMPLEX.BUTTONS.UPLOADING_BUTTON : GALLERY_COMPLEX.BUTTONS.UPLOAD_BUTTON}
819
1022
  </SubmitButton>
820
- <CancelButton
821
- onClick={handleCloseUploadModal}
822
- />
1023
+ <CancelButton onClick={handleCloseUploadModal} />
823
1024
  </div>
824
1025
  </form>
825
1026
  </div>