@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.
- package/README.md +40 -0
- package/app/ClientLayout.tsx +66 -0
- package/app/about/page.tsx +11 -248
- package/app/adRequest/page.tsx +101 -25
- package/app/admin-profile/page.tsx +123 -0
- package/app/analytics/page.tsx +41 -5
- package/app/api/about/route.ts +2 -18
- package/app/api/adRequest/route.ts +7 -27
- package/app/api/analytics/[reportType]/route.ts +1 -64
- package/app/api/bio/route.ts +1 -17
- package/app/api/blog/route.ts +1 -19
- package/app/api/contacts/route.ts +1 -46
- package/app/api/files/route.ts +1 -15
- package/app/api/gallery-data/route.ts +53 -61
- package/app/api/schedule/route.ts +5 -21
- package/app/api/signup/route.ts +129 -0
- package/app/api/sync-user/route.ts +268 -94
- package/app/api/verify-admin/route.ts +46 -0
- package/app/blog/[id]/page.tsx +71 -52
- package/app/blog/page.tsx +43 -10
- package/app/favicon.ico +0 -0
- package/app/gallery/page.tsx +27 -6
- package/app/layout.tsx +31 -82
- package/app/page.tsx +20 -311
- package/app/products/constants/product.ts +27 -0
- package/app/products/page.tsx +296 -0
- package/app/products/productOne/page.tsx +266 -0
- package/app/products/productTwo/page.tsx +272 -0
- package/app/schedule/page.tsx +78 -40
- package/bin/init.js +0 -12
- package/components/addOns/functional/ClassList.tsx +21 -17
- package/components/addOns/functional/ProductList.tsx +1027 -0
- package/components/addOns/functional/aboutSections/AboutSection.tsx +107 -70
- package/components/addOns/functional/aboutSections/constants/aboutSection.ts +9 -4
- package/components/addOns/functional/banner/Banner.tsx +150 -0
- package/components/addOns/functional/banner/BannerDashboard.tsx +283 -0
- package/components/addOns/functional/bioSections/BioEditor.tsx +471 -0
- package/components/addOns/functional/bioSections/constants/bioEditor.ts +36 -0
- package/components/addOns/functional/blogSections/BlogDashboard.tsx +1 -1
- package/components/addOns/functional/blogSections/BlogFormPopUp.tsx +2 -1
- package/components/addOns/functional/{ImageDescCarousel.tsx → carousels/ImageDescCarousel.tsx} +166 -57
- package/components/addOns/functional/carousels/ProductDescCarousel.tsx +1129 -0
- package/components/addOns/functional/{ScheduleCarousel.tsx → carousels/ScheduleCarousel.tsx} +110 -50
- package/components/addOns/functional/carousels/constants.ts/productDescCarousel.ts +197 -0
- package/components/addOns/functional/carousels/constants.ts/scheduleCarousel.ts +20 -0
- package/components/addOns/functional/contactsDashboard/ContactsDashboard.tsx +1 -1
- package/components/addOns/functional/fileUploaders/FileUploader.tsx +437 -0
- package/components/addOns/functional/fileUploaders/constants/fileUploader.ts +45 -0
- package/components/addOns/functional/galleries/GalleryComplex.tsx +468 -267
- package/components/addOns/functional/galleries/GallerySimple.tsx +78 -50
- package/components/addOns/functional/galleries/ThreeSetGallery.tsx +260 -0
- package/components/addOns/functional/schedules/ScheduleGridOne.tsx +22 -8
- package/components/addOns/functional/schedules/ScheduleGridTwo.tsx +12 -7
- package/components/addOns/functional/schedules/ScheduleGridTwoBasic.tsx +12 -7
- package/components/addOns/non-functional/SampleCarousel.tsx +3 -3
- package/components/addOns/non-functional/ThreeSetGallery.tsx +3 -3
- package/components/addOns/non-functional/featureSections/FeaturesSection.tsx +74 -0
- package/components/addOns/non-functional/featureSections/constants/featuresSection.ts +30 -0
- package/components/addOns/non-functional/{Heros/HeroSection.tsx → heros/HomeHero.tsx} +17 -15
- package/components/addOns/non-functional/heros/ProductHero.tsx +111 -0
- package/components/addOns/non-functional/heros/constants/hero.ts +62 -0
- package/components/addOns/non-functional/imageCarousels/ProductSlider.tsx +6 -6
- package/components/addOns/non-functional/imageCarousels/ProgramCarousel.tsx +10 -10
- package/components/footers/footer.tsx +161 -198
- package/components/other/admin-menu.tsx +1 -1
- package/lib/auth/auth-context.tsx +225 -0
- package/lib/auth/auth-utils.tsx +30 -0
- package/lib/constants/adRequest.ts +199 -56
- package/lib/constants/admin-profile.ts +12 -0
- package/lib/constants/page.ts +15 -15
- package/lib/google/google-analytics-tracking.tsx +44 -0
- package/lib/types.ts +235 -0
- package/lib/utils/compressImage.tsx +32 -0
- package/middleware.ts +9 -5
- package/next.config.js +1 -1
- package/package.json +3 -2
- package/public/images/test.png +0 -0
- package/components/addOns/functional/BioEditor.tsx +0 -447
- package/components/addOns/functional/FileUploader.tsx +0 -295
- package/components/addOns/non-functional/FeaturesSection.tsx +0 -63
- package/components/types.ts +0 -50
- package/lib/auth-context.tsx +0 -131
- package/lib/verify-user.ts +0 -118
- /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 {
|
|
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 {
|
|
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:
|
|
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 } =
|
|
49
|
-
const { getToken } = useAuth();
|
|
65
|
+
const { getToken, isSignedIn } = useAuth();
|
|
50
66
|
const { checkSession } = useStrapiAuth();
|
|
51
|
-
const isAdmin =
|
|
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 [
|
|
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.
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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",
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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-
|
|
369
|
-
{["all",
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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",
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
<Card className="overflow-hidden
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
className="
|
|
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
|
-
|
|
664
|
-
|
|
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
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
className="
|
|
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
|
-
|
|
778
|
-
|
|
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>
|