@devvistatech/devvista-kit 0.0.10 → 0.0.12
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/CHANGELOG.md +12 -12
- package/LICENSE +6 -6
- package/README.md +15 -15
- package/app/about/page.tsx +298 -298
- package/app/adRequest/page.tsx +549 -549
- package/app/analytics/page.tsx +346 -346
- package/app/api/about/route.ts +306 -306
- package/app/api/adRequest/route.ts +567 -567
- package/app/api/analytics/[reportType]/route.ts +337 -337
- package/app/api/bio/route.ts +313 -313
- package/app/api/blog/route.ts +306 -306
- package/app/api/chat/route.ts +14 -14
- package/app/api/contact/route.ts +409 -409
- package/app/api/contacts/route.ts +224 -224
- package/app/api/files/route.ts +429 -429
- package/app/api/gallery-data/route.ts +735 -735
- package/app/api/schedule/route.ts +455 -455
- package/app/api/sync-user/route.ts +131 -131
- package/app/api/trial-request/route.ts +297 -297
- package/app/blog/[id]/page.tsx +288 -288
- package/app/blog/page.tsx +216 -216
- package/app/contact/page.tsx +284 -284
- package/app/faq/page.tsx +191 -191
- package/app/gallery/page.tsx +315 -315
- package/app/globals.css +58 -58
- package/app/layout.tsx +110 -110
- package/app/not-found.tsx +20 -20
- package/app/page.tsx +338 -338
- package/app/schedule/page.tsx +660 -660
- package/bin/init.js +219 -219
- package/components/addOns/functional/BioEditor.tsx +446 -446
- package/components/addOns/functional/CalendlyWidget.tsx +107 -107
- package/components/addOns/functional/ClassList.tsx +145 -145
- package/components/addOns/functional/ClassPopup.tsx +398 -398
- package/components/addOns/functional/ContactForm.tsx +284 -284
- package/components/addOns/functional/FileUploader.tsx +294 -294
- package/components/addOns/functional/ImageDescCarousel.tsx +730 -730
- package/components/addOns/functional/NewUserAnalytics.tsx +100 -100
- package/components/addOns/functional/ScheduleCarousel.tsx +171 -171
- package/components/addOns/functional/aboutSections/AboutSection.tsx +544 -544
- package/components/addOns/functional/aboutSections/constants/aboutSection.ts +65 -65
- package/components/addOns/functional/blogSections/BlogDashboard.tsx +184 -184
- package/components/addOns/functional/blogSections/BlogFormPopUp.tsx +554 -554
- package/components/addOns/functional/blogSections/BlogList.tsx +148 -148
- package/components/addOns/functional/blogSections/BlogSidebar.tsx +58 -58
- package/components/addOns/functional/blogSections/constants/blogDashboard.ts +28 -28
- package/components/addOns/functional/blogSections/constants/blogFormPopUp.ts +97 -97
- package/components/addOns/functional/blogSections/constants/blogList.ts +22 -22
- package/components/addOns/functional/blogSections/constants/blogSidebar.ts +15 -15
- package/components/addOns/functional/contactsDashboard/ContactsDashboard.tsx +366 -366
- package/components/addOns/functional/contactsDashboard/constants/contactsDashboard.ts +70 -70
- package/components/addOns/functional/galleries/GalleryComplex.tsx +836 -836
- package/components/addOns/functional/galleries/GallerySimple.tsx +509 -509
- package/components/addOns/functional/galleries/constants/galleryComplex.ts +106 -106
- package/components/addOns/functional/galleries/constants/gallerySimple.ts +76 -76
- package/components/addOns/functional/schedules/ScheduleGridOne.tsx +262 -262
- package/components/addOns/functional/schedules/ScheduleGridTwo.tsx +294 -294
- package/components/addOns/functional/schedules/ScheduleGridTwoBasic.tsx +288 -288
- package/components/addOns/functional/schedules/SchedulerForm.tsx +428 -428
- package/components/addOns/functional/schedules/constants/ScheduleGridTwo.ts +40 -40
- package/components/addOns/functional/schedules/constants/ScheduleGridTwoBasic.ts +40 -40
- package/components/addOns/functional/schedules/constants/SchedulerForm.ts +65 -65
- package/components/addOns/functional/schedules/constants/scheduleGridOne.ts +54 -54
- package/components/addOns/non-functional/AnnouncementBanner.tsx +46 -46
- package/components/addOns/non-functional/FeaturesSection.tsx +62 -62
- package/components/addOns/non-functional/Heros/HeroSection.tsx +142 -142
- package/components/addOns/non-functional/IconBubble.tsx +49 -49
- package/components/addOns/non-functional/SampleCarousel.tsx +204 -204
- package/components/addOns/non-functional/Testimonials.tsx +334 -334
- package/components/addOns/non-functional/ThreeSetGallery.tsx +63 -63
- package/components/addOns/non-functional/aboutSections/AboutSection.tsx +62 -62
- package/components/addOns/non-functional/aboutSections/constants/aboutSection.ts +24 -24
- package/components/addOns/non-functional/imageCarousels/ProductSlider.tsx +117 -117
- package/components/addOns/non-functional/imageCarousels/ProgramCarousel.tsx +232 -232
- package/components/addOns/non-functional/imageCarousels/constants/programCarousel.ts +39 -39
- package/components/addOns/non-functional/imageCarousels/constants/programSlider.ts +36 -36
- package/components/addOns/non-functional/spinner.tsx +21 -21
- package/components/footers/footer.tsx +453 -453
- package/components/navBars/navbar.tsx +310 -310
- package/components/other/accordion.tsx +58 -58
- package/components/other/admin-menu.tsx +68 -68
- package/components/other/alert-dialog.tsx +141 -141
- package/components/other/alert.tsx +59 -59
- package/components/other/aspect-ratio.tsx +7 -7
- package/components/other/avatar.tsx +50 -50
- package/components/other/badge.tsx +36 -36
- package/components/other/breadcrumb.tsx +115 -115
- package/components/other/button.tsx +738 -738
- package/components/other/calendar.tsx +66 -66
- package/components/other/card.tsx +86 -86
- package/components/other/carousel.tsx +274 -274
- package/components/other/chart.tsx +363 -363
- package/components/other/checkbox.tsx +30 -30
- package/components/other/collapsible.tsx +11 -11
- package/components/other/command.tsx +155 -155
- package/components/other/context-menu.tsx +200 -200
- package/components/other/dialog.tsx +122 -122
- package/components/other/drawer.tsx +118 -118
- package/components/other/dropdown-menu.tsx +200 -200
- package/components/other/form.tsx +179 -179
- package/components/other/hover-card.tsx +29 -29
- package/components/other/input-otp.tsx +71 -71
- package/components/other/input.tsx +25 -25
- package/components/other/label.tsx +26 -26
- package/components/other/menubar.tsx +236 -236
- package/components/other/mobile-icon.tsx +21 -21
- package/components/other/navigation-menu.tsx +128 -128
- package/components/other/pagination.tsx +117 -117
- package/components/other/popover.tsx +31 -31
- package/components/other/progress.tsx +28 -28
- package/components/other/radio-group.tsx +44 -44
- package/components/other/resizable.tsx +45 -45
- package/components/other/scroll-area.tsx +48 -48
- package/components/other/select.tsx +160 -160
- package/components/other/separator.tsx +31 -31
- package/components/other/sheet.tsx +140 -140
- package/components/other/skeleton.tsx +15 -15
- package/components/other/slider.tsx +28 -28
- package/components/other/social-icons.tsx +39 -39
- package/components/other/sonner.tsx +31 -31
- package/components/other/switch.tsx +29 -29
- package/components/other/table.tsx +117 -117
- package/components/other/tabs.tsx +55 -55
- package/components/other/textarea.tsx +24 -24
- package/components/other/toast.tsx +122 -122
- package/components/other/toaster.tsx +35 -35
- package/components/other/toggle-group.tsx +61 -61
- package/components/other/toggle.tsx +45 -45
- package/components/other/tooltip.tsx +30 -30
- package/components/theme-provider.tsx +8 -8
- package/components/types.ts +49 -49
- package/hooks/use-toast.ts +188 -188
- package/lib/auth-context.tsx +130 -130
- package/lib/constants/about.ts +34 -34
- package/lib/constants/adRequest.ts +113 -113
- package/lib/constants/contact.ts +40 -40
- package/lib/constants/faq.ts +34 -34
- package/lib/constants/gallery.ts +42 -42
- package/lib/constants/page.ts +69 -69
- package/lib/constants/schedule.ts +71 -71
- package/lib/google-analytics.tsx +97 -97
- package/lib/verify-user.ts +117 -117
- package/middleware.ts +42 -42
- package/netlify.toml +5 -5
- package/next.config.js +10 -10
- package/package.json +115 -115
- package/tailwind.config.ts +89 -89
- package/tsconfig.json +23 -23
- package/dist/.next/types/app/api/about/route.js +0 -52
- package/dist/.next/types/app/api/blog/route.js +0 -52
- package/dist/.next/types/app/api/files/route.js +0 -52
- package/dist/.next/types/app/api/schedule/route.js +0 -52
- package/dist/.next/types/app/api/sync-user/route.js +0 -52
- package/dist/.next/types/app/layout.js +0 -22
- package/dist/.next/types/app/page.js +0 -22
- package/dist/app/about/page.jsx +0 -258
- package/dist/app/adRequest/page.jsx +0 -531
- package/dist/app/analytics/page.jsx +0 -298
- package/dist/app/api/about/route.js +0 -285
- package/dist/app/api/adRequest/route.js +0 -440
- package/dist/app/api/analytics/[reportType]/route.js +0 -357
- package/dist/app/api/bio/route.js +0 -293
- package/dist/app/api/blog/route.js +0 -366
- package/dist/app/api/chat/route.js +0 -58
- package/dist/app/api/contact/route.js +0 -163
- package/dist/app/api/contacts/route.js +0 -234
- package/dist/app/api/files/route.js +0 -444
- package/dist/app/api/gallery-data/route.js +0 -719
- package/dist/app/api/schedule/route.js +0 -461
- package/dist/app/api/sync-user/route.js +0 -186
- package/dist/app/api/trial-request/route.js +0 -165
- package/dist/app/blog/[id]/page.jsx +0 -312
- package/dist/app/blog/page.jsx +0 -210
- package/dist/app/constants/about.js +0 -32
- package/dist/app/constants/adRequest.js +0 -113
- package/dist/app/constants/contact.js +0 -40
- package/dist/app/constants/faq.js +0 -36
- package/dist/app/constants/gallery.js +0 -42
- package/dist/app/constants/page.js +0 -69
- package/dist/app/constants/schedule.js +0 -71
- package/dist/app/contact/page.jsx +0 -119
- package/dist/app/faq/page.jsx +0 -97
- package/dist/app/gallery/page.jsx +0 -281
- package/dist/app/layout.jsx +0 -45
- package/dist/app/not-found.jsx +0 -14
- package/dist/app/page.jsx +0 -324
- package/dist/app/schedule/page.jsx +0 -500
- package/dist/components/addOns/functional/BioEditor.jsx +0 -187
- package/dist/components/addOns/functional/CalendlyWidget.jsx +0 -61
- package/dist/components/addOns/functional/ClassList.jsx +0 -158
- package/dist/components/addOns/functional/ClassPopup.jsx +0 -300
- package/dist/components/addOns/functional/ContactForm.jsx +0 -219
- package/dist/components/addOns/functional/FileUploader.jsx +0 -222
- package/dist/components/addOns/functional/ImageDescCarousel.jsx +0 -491
- package/dist/components/addOns/functional/NewUserAnalytics.jsx +0 -71
- package/dist/components/addOns/functional/ScheduleCarousel.jsx +0 -68
- package/dist/components/addOns/functional/aboutSections/AboutSection.jsx +0 -372
- package/dist/components/addOns/functional/aboutSections/constants/aboutSection.js +0 -65
- package/dist/components/addOns/functional/blogSections/BlogDashboard.jsx +0 -111
- package/dist/components/addOns/functional/blogSections/BlogFormPopUp.jsx +0 -465
- package/dist/components/addOns/functional/blogSections/BlogList.jsx +0 -170
- package/dist/components/addOns/functional/blogSections/BlogSidebar.jsx +0 -35
- package/dist/components/addOns/functional/blogSections/constants/blogDashboard.js +0 -28
- package/dist/components/addOns/functional/blogSections/constants/blogFormPopUp.js +0 -97
- package/dist/components/addOns/functional/blogSections/constants/blogList.js +0 -22
- package/dist/components/addOns/functional/blogSections/constants/blogSidebar.js +0 -15
- package/dist/components/addOns/functional/contactsDashboard/ContactsDashboard.jsx +0 -355
- package/dist/components/addOns/functional/contactsDashboard/constants/contactsDashboard.js +0 -70
- package/dist/components/addOns/functional/galleries/GalleryComplex.jsx +0 -605
- package/dist/components/addOns/functional/galleries/GallerySimple.jsx +0 -363
- package/dist/components/addOns/functional/galleries/constants/galleryComplex.js +0 -106
- package/dist/components/addOns/functional/galleries/constants/gallerySimple.js +0 -76
- package/dist/components/addOns/functional/schedules/ScheduleGridOne.jsx +0 -167
- package/dist/components/addOns/functional/schedules/ScheduleGridTwo.jsx +0 -100
- package/dist/components/addOns/functional/schedules/ScheduleGridTwoBasic.jsx +0 -97
- package/dist/components/addOns/functional/schedules/SchedulerForm.jsx +0 -188
- package/dist/components/addOns/functional/schedules/constants/ScheduleGridTwo.js +0 -40
- package/dist/components/addOns/functional/schedules/constants/ScheduleGridTwoBasic.js +0 -40
- package/dist/components/addOns/functional/schedules/constants/SchedulerForm.js +0 -65
- package/dist/components/addOns/functional/schedules/constants/scheduleGridOne.js +0 -54
- package/dist/components/addOns/non-functional/AnnouncementBanner.jsx +0 -24
- package/dist/components/addOns/non-functional/FeaturesSection.jsx +0 -38
- package/dist/components/addOns/non-functional/HeroSection.jsx +0 -71
- package/dist/components/addOns/non-functional/Heros/HeroSection.jsx +0 -71
- package/dist/components/addOns/non-functional/IconBubble.jsx +0 -36
- package/dist/components/addOns/non-functional/SampleCarousel.jsx +0 -114
- package/dist/components/addOns/non-functional/Testimonials.jsx +0 -177
- package/dist/components/addOns/non-functional/ThreeSetGallery.jsx +0 -40
- package/dist/components/addOns/non-functional/aboutSections/AboutSection.jsx +0 -35
- package/dist/components/addOns/non-functional/aboutSections/constants/aboutSection.js +0 -24
- package/dist/components/addOns/non-functional/imageCarousels/ProductSlider.jsx +0 -80
- package/dist/components/addOns/non-functional/imageCarousels/ProgramCarousel.jsx +0 -155
- package/dist/components/addOns/non-functional/imageCarousels/constants/programCarousel.js +0 -39
- package/dist/components/addOns/non-functional/imageCarousels/constants/programSlider.js +0 -36
- package/dist/components/addOns/non-functional/spinner.jsx +0 -13
- package/dist/components/footers/footer.jsx +0 -217
- package/dist/components/navBars/navbar.jsx +0 -159
- package/dist/components/other/accordion.jsx +0 -40
- package/dist/components/other/admin-menu.jsx +0 -34
- package/dist/components/other/alert-dialog.jsx +0 -64
- package/dist/components/other/alert.jsx +0 -41
- package/dist/components/other/aspect-ratio.jsx +0 -4
- package/dist/components/other/avatar.jsx +0 -31
- package/dist/components/other/badge.jsx +0 -32
- package/dist/components/other/breadcrumb.jsx +0 -57
- package/dist/components/other/button.jsx +0 -322
- package/dist/components/other/calendar.jsx +0 -43
- package/dist/components/other/card.jsx +0 -44
- package/dist/components/other/carousel.jsx +0 -140
- package/dist/components/other/chart.jsx +0 -182
- package/dist/components/other/checkbox.jsx +0 -26
- package/dist/components/other/collapsible.jsx +0 -6
- package/dist/components/other/command.jsx +0 -68
- package/dist/components/other/context-menu.jsx +0 -88
- package/dist/components/other/dialog.jsx +0 -60
- package/dist/components/other/drawer.jsx +0 -60
- package/dist/components/other/dropdown-menu.jsx +0 -90
- package/dist/components/other/form.jsx +0 -89
- package/dist/components/other/hover-card.jsx +0 -23
- package/dist/components/other/input-otp.jsx +0 -46
- package/dist/components/other/input.jsx +0 -19
- package/dist/components/other/label.jsx +0 -23
- package/dist/components/other/login-popup.jsx +0 -1
- package/dist/components/other/menubar.jsx +0 -96
- package/dist/components/other/mobile-icon.jsx +0 -11
- package/dist/components/other/navigation-menu.jsx +0 -62
- package/dist/components/other/pagination.jsx +0 -63
- package/dist/components/other/popover.jsx +0 -25
- package/dist/components/other/progress.jsx +0 -23
- package/dist/components/other/radio-group.jsx +0 -31
- package/dist/components/other/resizable.jsx +0 -29
- package/dist/components/other/scroll-area.jsx +0 -36
- package/dist/components/other/select.jsx +0 -83
- package/dist/components/other/separator.jsx +0 -21
- package/dist/components/other/sheet.jsx +0 -74
- package/dist/components/other/signup-popup.jsx +0 -1
- package/dist/components/other/skeleton.jsx +0 -17
- package/dist/components/other/slider.jsx +0 -26
- package/dist/components/other/social-icons.jsx +0 -15
- package/dist/components/other/sonner.jsx +0 -27
- package/dist/components/other/switch.jsx +0 -23
- package/dist/components/other/table.jsx +0 -56
- package/dist/components/other/tabs.jsx +0 -32
- package/dist/components/other/textarea.jsx +0 -19
- package/dist/components/other/toast.jsx +0 -58
- package/dist/components/other/toaster.jsx +0 -31
- package/dist/components/other/toggle-group.jsx +0 -41
- package/dist/components/other/toggle.jsx +0 -39
- package/dist/components/other/tooltip.jsx +0 -24
- package/dist/components/theme-provider.jsx +0 -18
- package/dist/components/types.js +0 -1
- package/dist/hooks/use-toast.js +0 -135
- package/dist/lib/auth-context.jsx +0 -144
- package/dist/lib/constants/about.js +0 -32
- package/dist/lib/constants/adRequest.js +0 -113
- package/dist/lib/constants/contact.js +0 -40
- package/dist/lib/constants/faq.js +0 -36
- package/dist/lib/constants/gallery.js +0 -42
- package/dist/lib/constants/page.js +0 -69
- package/dist/lib/constants/schedule.js +0 -71
- package/dist/lib/google-analytics.jsx +0 -148
- package/dist/lib/utils.js +0 -9
- package/dist/lib/verify-user.js +0 -142
- package/dist/middleware.js +0 -37
- package/dist/tailwind.config.js +0 -86
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -1,731 +1,731 @@
|
|
|
1
|
-
// src/components/addOns/functional/ImageDescCarousel.tsx
|
|
2
|
-
"use client";
|
|
3
|
-
|
|
4
|
-
import { useState, useEffect, useRef } from "react";
|
|
5
|
-
import {
|
|
6
|
-
ActionButton,
|
|
7
|
-
EditIconButton,
|
|
8
|
-
TrashIconButton,
|
|
9
|
-
CloseButton,
|
|
10
|
-
SubmitButton,
|
|
11
|
-
CancelButton,
|
|
12
|
-
NextButton,
|
|
13
|
-
PrevButton,
|
|
14
|
-
FilterButton,
|
|
15
|
-
DeleteButton,
|
|
16
|
-
} from "@/components/other/button";
|
|
17
|
-
import { Card } from "@/components/other/card";
|
|
18
|
-
import Image from "next/image";
|
|
19
|
-
import { Upload, ChevronRight, X, ChevronLeft } from "lucide-react";
|
|
20
|
-
import { motion, useScroll, useTransform } from "framer-motion";
|
|
21
|
-
import { useAuth, useUser } from "@clerk/nextjs";
|
|
22
|
-
import { StrapiUser, UploadedImage, Category } from "@/components/types";
|
|
23
|
-
|
|
24
|
-
interface ImageDescCarouselProps {
|
|
25
|
-
user: StrapiUser | null;
|
|
26
|
-
uploadedImages: UploadedImage[];
|
|
27
|
-
setUploadedImages: (images: UploadedImage[]) => void;
|
|
28
|
-
error: string | null;
|
|
29
|
-
setError: (error: string | null) => void;
|
|
30
|
-
isLoading: boolean;
|
|
31
|
-
setIsLoading: (isLoading: boolean) => void;
|
|
32
|
-
handleImageUpload: (
|
|
33
|
-
e: React.FormEvent<HTMLFormElement>,
|
|
34
|
-
file: File | null,
|
|
35
|
-
title: string,
|
|
36
|
-
description: string,
|
|
37
|
-
category: Category
|
|
38
|
-
) => Promise<void>;
|
|
39
|
-
handleDeleteImage: (documentId: string) => Promise<void>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const Slideshow = ({
|
|
43
|
-
images,
|
|
44
|
-
altPrefix,
|
|
45
|
-
currentSlide,
|
|
46
|
-
setCurrentSlide,
|
|
47
|
-
isAdmin,
|
|
48
|
-
handleImageClick,
|
|
49
|
-
handleEditImage,
|
|
50
|
-
handleDeleteImage,
|
|
51
|
-
}: {
|
|
52
|
-
images: UploadedImage[];
|
|
53
|
-
altPrefix: string;
|
|
54
|
-
currentSlide: number;
|
|
55
|
-
setCurrentSlide: React.Dispatch<React.SetStateAction<number>>;
|
|
56
|
-
isAdmin: boolean;
|
|
57
|
-
handleImageClick: (image: UploadedImage) => void;
|
|
58
|
-
handleEditImage: (image: UploadedImage) => void;
|
|
59
|
-
handleDeleteImage: (documentId: string) => void;
|
|
60
|
-
}) => {
|
|
61
|
-
const slideshowRef = useRef<HTMLDivElement>(null);
|
|
62
|
-
|
|
63
|
-
const goToPrev = () => setCurrentSlide((prev) => (prev - 1 + images.length) % images.length);
|
|
64
|
-
const goToNext = () => setCurrentSlide((prev) => (prev + 1) % images.length);
|
|
65
|
-
|
|
66
|
-
if (!images.length || !images[currentSlide]) {
|
|
67
|
-
return (
|
|
68
|
-
<Card
|
|
69
|
-
className="relative h-[40vh] min-h-[400px] sm:h-[50vh] lg:h-[600px] rounded-3xl overflow-hidden bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl w-full mx-auto max-w-3xl md:max-w-none md:mx-0 flex items-center justify-center supports-[not(backdrop-filter:blur(10px))]:bg-white/20"
|
|
70
|
-
ref={slideshowRef}
|
|
71
|
-
>
|
|
72
|
-
<p className="text-gray-600 text-lg">No images available</p>
|
|
73
|
-
</Card>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div
|
|
79
|
-
className="relative h-[40vh] min-h-[400px] sm:h-[50vh] lg:h-[600px] rounded-3xl overflow-hidden bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl w-full mx-auto max-w-3xl md:max-w-none md:mx-0 supports-[not(backdrop-filter:blur(10px))]:bg-white/20"
|
|
80
|
-
ref={slideshowRef}
|
|
81
|
-
>
|
|
82
|
-
<div
|
|
83
|
-
className="relative w-full h-full cursor-pointer"
|
|
84
|
-
onClick={() => handleImageClick(images[currentSlide])}
|
|
85
|
-
>
|
|
86
|
-
<Image
|
|
87
|
-
src={images[currentSlide].url}
|
|
88
|
-
alt={
|
|
89
|
-
images[currentSlide].title?.trim() &&
|
|
90
|
-
!images[currentSlide].title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
91
|
-
? images[currentSlide].title
|
|
92
|
-
: altPrefix
|
|
93
|
-
}
|
|
94
|
-
fill
|
|
95
|
-
className="object-cover rounded-2xl transition-transform duration-700 hover:scale-110"
|
|
96
|
-
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
97
|
-
loading="lazy"
|
|
98
|
-
quality={85}
|
|
99
|
-
/>
|
|
100
|
-
{isAdmin && (
|
|
101
|
-
<div className="absolute top-2 right-2 flex space-x-2 opacity-0 hover:opacity-100 transition-opacity duration-500">
|
|
102
|
-
<EditIconButton
|
|
103
|
-
onClick={(e) => {
|
|
104
|
-
e.stopPropagation();
|
|
105
|
-
handleEditImage(images[currentSlide]);
|
|
106
|
-
}}
|
|
107
|
-
/>
|
|
108
|
-
<TrashIconButton
|
|
109
|
-
onClick={(e) => {
|
|
110
|
-
e.stopPropagation();
|
|
111
|
-
handleDeleteImage(images[currentSlide].documentId);
|
|
112
|
-
}}
|
|
113
|
-
/>
|
|
114
|
-
</div>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
{images.length > 1 && (
|
|
118
|
-
<>
|
|
119
|
-
<PrevButton
|
|
120
|
-
onClick={(e) => {
|
|
121
|
-
e.stopPropagation();
|
|
122
|
-
goToPrev();
|
|
123
|
-
}}
|
|
124
|
-
/>
|
|
125
|
-
<NextButton
|
|
126
|
-
onClick={(e) => {
|
|
127
|
-
e.stopPropagation();
|
|
128
|
-
goToNext();
|
|
129
|
-
}}
|
|
130
|
-
/>
|
|
131
|
-
</>
|
|
132
|
-
)}
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export function ImageDescCarousel({
|
|
138
|
-
user,
|
|
139
|
-
uploadedImages,
|
|
140
|
-
setUploadedImages,
|
|
141
|
-
error,
|
|
142
|
-
setError,
|
|
143
|
-
isLoading,
|
|
144
|
-
setIsLoading,
|
|
145
|
-
handleImageUpload,
|
|
146
|
-
handleDeleteImage,
|
|
147
|
-
}: ImageDescCarouselProps) {
|
|
148
|
-
const { isSignedIn } = useUser();
|
|
149
|
-
const { getToken } = useAuth();
|
|
150
|
-
const [activeTab, setActiveTab] = useState<Category>("indoor");
|
|
151
|
-
const [currentSlide, setCurrentSlide] = useState(0);
|
|
152
|
-
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
|
153
|
-
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
154
|
-
const [isConfirmDeleteOpen, setIsConfirmDeleteOpen] = useState(false);
|
|
155
|
-
const [documentIdToDelete, setDocumentIdToDelete] = useState<string | null>(null);
|
|
156
|
-
const [selectedImage, setSelectedImage] = useState<UploadedImage | null>(null);
|
|
157
|
-
const [uploadForm, setUploadForm] = useState<{
|
|
158
|
-
file: File | null;
|
|
159
|
-
title: string;
|
|
160
|
-
description: string;
|
|
161
|
-
category: Category;
|
|
162
|
-
}>({ file: null, title: "", description: "", category: "indoor" });
|
|
163
|
-
const [editForm, setEditForm] = useState<{
|
|
164
|
-
id: number;
|
|
165
|
-
documentId: string;
|
|
166
|
-
title: string;
|
|
167
|
-
description: string;
|
|
168
|
-
category: Category;
|
|
169
|
-
file: File | null;
|
|
170
|
-
}>({ id: 0, documentId: "", title: "", description: "", category: "indoor", file: null });
|
|
171
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
172
|
-
|
|
173
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
174
|
-
const { scrollYProgress } = useScroll({
|
|
175
|
-
target: containerRef,
|
|
176
|
-
offset: ["start end", "end start"],
|
|
177
|
-
});
|
|
178
|
-
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -50]);
|
|
179
|
-
|
|
180
|
-
const isAdmin = isSignedIn && !!user?.businessAdminId || false;
|
|
181
|
-
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
const isAnyModalOpen = isUploadModalOpen || isEditModalOpen || isConfirmDeleteOpen || !!selectedImage;
|
|
184
|
-
if (isAnyModalOpen) {
|
|
185
|
-
document.body.style.overflow = "hidden";
|
|
186
|
-
} else {
|
|
187
|
-
document.body.style.overflow = "";
|
|
188
|
-
}
|
|
189
|
-
window.dispatchEvent(
|
|
190
|
-
new CustomEvent("modalStateChange", { detail: { isOpen: isAnyModalOpen } })
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
return () => {
|
|
194
|
-
document.body.style.overflow = "";
|
|
195
|
-
};
|
|
196
|
-
}, [isUploadModalOpen, isEditModalOpen, isConfirmDeleteOpen, selectedImage]);
|
|
197
|
-
|
|
198
|
-
const filteredImages = uploadedImages.filter(
|
|
199
|
-
(img) => (img.category || "none") === activeTab
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
const handleImageClick = (image: UploadedImage) => {
|
|
203
|
-
setSelectedImage(image);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const handleCloseModal = () => {
|
|
207
|
-
setSelectedImage(null);
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const openEditModal = (image: UploadedImage) => {
|
|
211
|
-
if (!isAdmin) {
|
|
212
|
-
setError("Unauthorized: Only admins can edit images");
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
setEditForm({
|
|
216
|
-
id: image.id,
|
|
217
|
-
documentId: image.documentId,
|
|
218
|
-
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 : "",
|
|
219
|
-
description: image.description || "",
|
|
220
|
-
category: image.category && image.category !== "none" ? image.category : "indoor",
|
|
221
|
-
file: null,
|
|
222
|
-
});
|
|
223
|
-
setIsEditModalOpen(true);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const handleCloseEditModal = () => {
|
|
227
|
-
setIsEditModalOpen(false);
|
|
228
|
-
setEditForm({ id: 0, documentId: "", title: "", description: "", category: "indoor", file: null });
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const handleCloseUploadModal = () => {
|
|
232
|
-
setIsUploadModalOpen(false);
|
|
233
|
-
setUploadForm({ file: null, title: "", description: "", category: "indoor" });
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const openConfirmDelete = (documentId: string) => {
|
|
237
|
-
if (!isAdmin) {
|
|
238
|
-
setError("Unauthorized: Only admins can delete images");
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
setDocumentIdToDelete(documentId);
|
|
242
|
-
setIsConfirmDeleteOpen(true);
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const handleConfirmDelete = async () => {
|
|
246
|
-
if (!isAdmin) {
|
|
247
|
-
setError("Unauthorized: Only admins can delete images");
|
|
248
|
-
setIsConfirmDeleteOpen(false);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
if (documentIdToDelete !== null) {
|
|
252
|
-
setIsLoading(true);
|
|
253
|
-
try {
|
|
254
|
-
await handleDeleteImage(documentIdToDelete);
|
|
255
|
-
} finally {
|
|
256
|
-
setIsLoading(false);
|
|
257
|
-
setIsConfirmDeleteOpen(false);
|
|
258
|
-
setDocumentIdToDelete(null);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const handleCancelDelete = () => {
|
|
264
|
-
setIsConfirmDeleteOpen(false);
|
|
265
|
-
setDocumentIdToDelete(null);
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const handleEditImage = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
269
|
-
e.preventDefault();
|
|
270
|
-
if (!isAdmin) {
|
|
271
|
-
console.error("ImageDescCarousel: Unauthorized edit attempt", {
|
|
272
|
-
isSignedIn,
|
|
273
|
-
businessAdminId: user?.businessAdminId,
|
|
274
|
-
});
|
|
275
|
-
setError("Unauthorized: Only admins can edit images");
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
setIsLoading(true);
|
|
281
|
-
setIsSubmitting(true);
|
|
282
|
-
const token = await getToken();
|
|
283
|
-
if (!token) {
|
|
284
|
-
console.error("ImageDescCarousel: No authentication token available");
|
|
285
|
-
setError("Authentication error: Please log in again");
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const formData = new FormData();
|
|
290
|
-
formData.append("documentId", editForm.documentId);
|
|
291
|
-
formData.append("title", editForm.title || `Image ${new Date().toISOString()}`);
|
|
292
|
-
formData.append("description", editForm.description || "");
|
|
293
|
-
formData.append("category", editForm.category);
|
|
294
|
-
if (editForm.file) {
|
|
295
|
-
formData.append("file", editForm.file);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const response = await fetch("/api/gallery-data", {
|
|
299
|
-
method: "PUT",
|
|
300
|
-
headers: {
|
|
301
|
-
Authorization: `Bearer ${token}`,
|
|
302
|
-
},
|
|
303
|
-
body: formData,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
if (!response.ok) {
|
|
307
|
-
const errorData = await response.json();
|
|
308
|
-
console.error("ImageDescCarousel: Edit failed", { status: response.status, errorData });
|
|
309
|
-
if (response.status === 401) {
|
|
310
|
-
setError("Authentication error: Please log in again");
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
throw new Error(errorData.error || `Failed to edit image (Status: ${response.status})`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const { data } = await response.json();
|
|
317
|
-
setUploadedImages(data || []);
|
|
318
|
-
setError(null);
|
|
319
|
-
setIsEditModalOpen(false);
|
|
320
|
-
setEditForm({ id: 0, documentId: "", title: "", description: "", category: "indoor", file: null });
|
|
321
|
-
} catch (err) {
|
|
322
|
-
console.error("ImageDescCarousel: Edit Error", err);
|
|
323
|
-
setError(err instanceof Error ? err.message : "Failed to edit image");
|
|
324
|
-
} finally {
|
|
325
|
-
setIsLoading(false);
|
|
326
|
-
setIsSubmitting(false);
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const sectionVariants = {
|
|
331
|
-
hidden: { opacity: 0 },
|
|
332
|
-
visible: {
|
|
333
|
-
opacity: 1,
|
|
334
|
-
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1], staggerChildren: 0.1 },
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const modalVariants = {
|
|
339
|
-
hidden: { opacity: 0, y: "100vh" },
|
|
340
|
-
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] } },
|
|
341
|
-
exit: { opacity: 0, y: "100vh", transition: { duration: 0.3, ease: "easeIn" } },
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
return (
|
|
345
|
-
<div className="w-full">
|
|
346
|
-
{/* Gallery Section */}
|
|
347
|
-
<motion.section
|
|
348
|
-
variants={sectionVariants}
|
|
349
|
-
initial="hidden"
|
|
350
|
-
whileInView="visible"
|
|
351
|
-
viewport={{ once: true }}
|
|
352
|
-
className="relative py-12 sm:py-16 lg:pb-24 w-full bg-gray-50/50 backdrop-blur-sm"
|
|
353
|
-
ref={containerRef}
|
|
354
|
-
style={{ y: parallaxY }}
|
|
355
|
-
>
|
|
356
|
-
<div className="relative z-10 w-full px-4 sm:px-6 lg:px-8">
|
|
357
|
-
{error && <p className="text-red-600 text-lg text-center mb-8">{error}</p>}
|
|
358
|
-
{user && !isAdmin && (
|
|
359
|
-
<p className="text-yellow-600 text-lg text-center mb-8">
|
|
360
|
-
You are logged in but do not have admin privileges.
|
|
361
|
-
</p>
|
|
362
|
-
)}
|
|
363
|
-
{isAdmin && (
|
|
364
|
-
<div className="flex justify-center mb-12">
|
|
365
|
-
<ActionButton
|
|
366
|
-
onClick={() => setIsUploadModalOpen(true)}
|
|
367
|
-
className="flex items-center"
|
|
368
|
-
disabled={isLoading || isSubmitting}
|
|
369
|
-
>
|
|
370
|
-
<Upload className="mr-2 h-4 w-4" />
|
|
371
|
-
Upload New Image
|
|
372
|
-
</ActionButton>
|
|
373
|
-
</div>
|
|
374
|
-
)}
|
|
375
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 sm:gap-8 lg:gap-12 items-start">
|
|
376
|
-
<div className="h-auto min-h-[600px] bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl p-6 sm:p-8 lg:p-10 rounded-3xl flex flex-col space-y-4 sm:space-y-6 order-2 max-w-3xl mx-auto md:max-w-none md:mx-0 md:order-1 supports-[not(backdrop-filter:blur(10px))]:bg-white/20">
|
|
377
|
-
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-gray-900">
|
|
378
|
-
{filteredImages[currentSlide]?.title?.trim() &&
|
|
379
|
-
!filteredImages[currentSlide]?.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
380
|
-
? filteredImages[currentSlide].title
|
|
381
|
-
: "Image Gallery"}
|
|
382
|
-
</h2>
|
|
383
|
-
<div className="text-sm sm:text-base lg:text-lg text-gray-700">
|
|
384
|
-
{filteredImages.length > 0 && currentSlide >= 0 && currentSlide < filteredImages.length ? (
|
|
385
|
-
(filteredImages[currentSlide].description || "")
|
|
386
|
-
.split("\n")
|
|
387
|
-
.filter((paragraph) => paragraph.trim() !== "")
|
|
388
|
-
.map((paragraph, index) => (
|
|
389
|
-
<p key={index} className="mb-4">
|
|
390
|
-
{paragraph}
|
|
391
|
-
</p>
|
|
392
|
-
)) || <p className="mb-4">No description available.</p>
|
|
393
|
-
) : (
|
|
394
|
-
<p className="mb-4">No description available.</p>
|
|
395
|
-
)}
|
|
396
|
-
</div>
|
|
397
|
-
</div>
|
|
398
|
-
<div className="order-1 md:order-2">
|
|
399
|
-
<Slideshow
|
|
400
|
-
images={filteredImages}
|
|
401
|
-
altPrefix="Image Gallery"
|
|
402
|
-
currentSlide={currentSlide}
|
|
403
|
-
setCurrentSlide={setCurrentSlide}
|
|
404
|
-
isAdmin={isAdmin}
|
|
405
|
-
handleImageClick={handleImageClick}
|
|
406
|
-
handleEditImage={openEditModal}
|
|
407
|
-
handleDeleteImage={openConfirmDelete}
|
|
408
|
-
/>
|
|
409
|
-
<div className="flex flex-wrap justify-center gap-2 sm:gap-4 mt-6 z-20">
|
|
410
|
-
{["indoor", "outdoor", "commercial"].map((tab) => (
|
|
411
|
-
<FilterButton
|
|
412
|
-
key={tab}
|
|
413
|
-
isActive={activeTab === tab}
|
|
414
|
-
onClick={() => {
|
|
415
|
-
setActiveTab(tab as Category);
|
|
416
|
-
setCurrentSlide(0);
|
|
417
|
-
}}
|
|
418
|
-
>
|
|
419
|
-
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
420
|
-
</FilterButton>
|
|
421
|
-
))}
|
|
422
|
-
</div>
|
|
423
|
-
</div>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
</motion.section>
|
|
427
|
-
|
|
428
|
-
{/* Upload Modal */}
|
|
429
|
-
{isAdmin && isUploadModalOpen && (
|
|
430
|
-
<motion.div
|
|
431
|
-
variants={modalVariants}
|
|
432
|
-
initial="hidden"
|
|
433
|
-
animate="visible"
|
|
434
|
-
exit="exit"
|
|
435
|
-
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10000] p-4 isolate"
|
|
436
|
-
onClick={handleCloseUploadModal}
|
|
437
|
-
aria-modal="true"
|
|
438
|
-
role="dialog"
|
|
439
|
-
>
|
|
440
|
-
<div
|
|
441
|
-
className="relative max-w-5xl w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-h-[90vh] overflow-y-auto"
|
|
442
|
-
onClick={(e) => e.stopPropagation()}
|
|
443
|
-
>
|
|
444
|
-
<CloseButton onClick={handleCloseUploadModal}>
|
|
445
|
-
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
446
|
-
</CloseButton>
|
|
447
|
-
<h3 className="text-xl font-bold text-white mb-4">Upload Image</h3>
|
|
448
|
-
<form
|
|
449
|
-
onSubmit={(e) => {
|
|
450
|
-
setIsSubmitting(true);
|
|
451
|
-
handleImageUpload(e, uploadForm.file, uploadForm.title, uploadForm.description, uploadForm.category).then(() => {
|
|
452
|
-
handleCloseUploadModal();
|
|
453
|
-
setIsSubmitting(false);
|
|
454
|
-
});
|
|
455
|
-
}}
|
|
456
|
-
className="space-y-4"
|
|
457
|
-
>
|
|
458
|
-
<div>
|
|
459
|
-
<label htmlFor="image-upload" className="block text-sm font-medium text-gray-300 mb-1">
|
|
460
|
-
Choose Image
|
|
461
|
-
</label>
|
|
462
|
-
<input
|
|
463
|
-
type="file"
|
|
464
|
-
accept="image/jpeg,image/png,image/gif"
|
|
465
|
-
onChange={(e) => setUploadForm({ ...uploadForm, file: e.target.files?.[0] || null })}
|
|
466
|
-
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
467
|
-
id="image-upload"
|
|
468
|
-
disabled={isSubmitting}
|
|
469
|
-
/>
|
|
470
|
-
{uploadForm.file && (
|
|
471
|
-
<p className="mt-2 text-gray-300 text-sm max-[320px]:text-xs">Selected: {uploadForm.file.name}</p>
|
|
472
|
-
)}
|
|
473
|
-
</div>
|
|
474
|
-
<div>
|
|
475
|
-
<label htmlFor="image-title" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
476
|
-
Image Title (optional)
|
|
477
|
-
</label>
|
|
478
|
-
<input
|
|
479
|
-
id="image-title"
|
|
480
|
-
value={uploadForm.title}
|
|
481
|
-
onChange={(e) => setUploadForm({ ...uploadForm, title: e.target.value })}
|
|
482
|
-
placeholder="Enter image title"
|
|
483
|
-
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
484
|
-
disabled={isSubmitting}
|
|
485
|
-
/>
|
|
486
|
-
</div>
|
|
487
|
-
<div>
|
|
488
|
-
<label htmlFor="image-description" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
489
|
-
Image Description (optional)
|
|
490
|
-
</label>
|
|
491
|
-
<textarea
|
|
492
|
-
id="image-description"
|
|
493
|
-
value={uploadForm.description}
|
|
494
|
-
onChange={(e) => setUploadForm({ ...uploadForm, description: e.target.value })}
|
|
495
|
-
placeholder="Enter image description"
|
|
496
|
-
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm min-h-[80px] max-[768px]:text-sm max-[768px]:min-h-[80px] max-[320px]:text-xs max-[320px]:p-2"
|
|
497
|
-
disabled={isSubmitting}
|
|
498
|
-
/>
|
|
499
|
-
</div>
|
|
500
|
-
<div>
|
|
501
|
-
<label htmlFor="image-category" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
502
|
-
Category
|
|
503
|
-
</label>
|
|
504
|
-
<select
|
|
505
|
-
id="image-category"
|
|
506
|
-
value={uploadForm.category}
|
|
507
|
-
onChange={(e) =>
|
|
508
|
-
setUploadForm({
|
|
509
|
-
...uploadForm,
|
|
510
|
-
category: e.target.value as Category,
|
|
511
|
-
})
|
|
512
|
-
}
|
|
513
|
-
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
514
|
-
disabled={isSubmitting}
|
|
515
|
-
>
|
|
516
|
-
<option value="indoor">Indoor</option>
|
|
517
|
-
<option value="outdoor">Outdoor</option>
|
|
518
|
-
<option value="commercial">Commercial</option>
|
|
519
|
-
{/* Add project-specific categories, e.g.:
|
|
520
|
-
<option value="landscape-boulders">Landscape Boulders</option>
|
|
521
|
-
*/}
|
|
522
|
-
</select>
|
|
523
|
-
</div>
|
|
524
|
-
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
|
|
525
|
-
<SubmitButton type="submit" disabled={isSubmitting || !uploadForm.file}>
|
|
526
|
-
{isSubmitting ? "Uploading..." : "Upload"}
|
|
527
|
-
</SubmitButton>
|
|
528
|
-
<CancelButton onClick={handleCloseUploadModal} disabled={isSubmitting} />
|
|
529
|
-
</div>
|
|
530
|
-
{error && <p className="text-red-400 text-sm font-medium max-[320px]:text-xs">{error}</p>}
|
|
531
|
-
</form>
|
|
532
|
-
</div>
|
|
533
|
-
</motion.div>
|
|
534
|
-
)}
|
|
535
|
-
|
|
536
|
-
{/* Edit Modal */}
|
|
537
|
-
{isAdmin && isEditModalOpen && (
|
|
538
|
-
<motion.div
|
|
539
|
-
variants={modalVariants}
|
|
540
|
-
initial="hidden"
|
|
541
|
-
animate="visible"
|
|
542
|
-
exit="exit"
|
|
543
|
-
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10000] p-4 isolate"
|
|
544
|
-
onClick={handleCloseEditModal}
|
|
545
|
-
aria-modal="true"
|
|
546
|
-
role="dialog"
|
|
547
|
-
>
|
|
548
|
-
<div
|
|
549
|
-
className="relative max-w-5xl w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-h-[90vh] overflow-y-auto"
|
|
550
|
-
onClick={(e) => e.stopPropagation()}
|
|
551
|
-
>
|
|
552
|
-
<CloseButton onClick={handleCloseEditModal}>
|
|
553
|
-
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
554
|
-
</CloseButton>
|
|
555
|
-
<h3 className="text-xl font-bold text-white mb-4">Edit Image</h3>
|
|
556
|
-
<form
|
|
557
|
-
onSubmit={handleEditImage}
|
|
558
|
-
className="space-y-4"
|
|
559
|
-
>
|
|
560
|
-
<div>
|
|
561
|
-
<label htmlFor="edit-image" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
562
|
-
Replace Image (optional)
|
|
563
|
-
</label>
|
|
564
|
-
<input
|
|
565
|
-
id="edit-image"
|
|
566
|
-
type="file"
|
|
567
|
-
accept="image/jpeg,image/png,image/gif"
|
|
568
|
-
onChange={(e) => setEditForm({ ...editForm, file: e.target.files?.[0] || null })}
|
|
569
|
-
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
570
|
-
disabled={isSubmitting}
|
|
571
|
-
/>
|
|
572
|
-
{editForm.file && (
|
|
573
|
-
<p className="mt-2 text-gray-300 text-sm max-[320px]:text-xs">Selected: {editForm.file.name}</p>
|
|
574
|
-
)}
|
|
575
|
-
</div>
|
|
576
|
-
<div>
|
|
577
|
-
<label htmlFor="edit-title" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
578
|
-
Image Title (optional)
|
|
579
|
-
</label>
|
|
580
|
-
<input
|
|
581
|
-
id="edit-title"
|
|
582
|
-
value={editForm.title}
|
|
583
|
-
onChange={(e) => setEditForm({ ...editForm, title: e.target.value })}
|
|
584
|
-
placeholder="Enter image title"
|
|
585
|
-
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
586
|
-
disabled={isSubmitting}
|
|
587
|
-
/>
|
|
588
|
-
</div>
|
|
589
|
-
<div>
|
|
590
|
-
<label htmlFor="edit-description" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
591
|
-
Image Description (optional)
|
|
592
|
-
</label>
|
|
593
|
-
<textarea
|
|
594
|
-
id="edit-description"
|
|
595
|
-
value={editForm.description}
|
|
596
|
-
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
|
597
|
-
placeholder="Enter image description"
|
|
598
|
-
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm min-h-[80px] max-[768px]:text-sm max-[768px]:min-h-[80px] max-[320px]:text-xs max-[320px]:p-2"
|
|
599
|
-
disabled={isSubmitting}
|
|
600
|
-
/>
|
|
601
|
-
</div>
|
|
602
|
-
<div>
|
|
603
|
-
<label htmlFor="edit-category" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
604
|
-
Category
|
|
605
|
-
</label>
|
|
606
|
-
<select
|
|
607
|
-
id="edit-category"
|
|
608
|
-
value={editForm.category}
|
|
609
|
-
onChange={(e) =>
|
|
610
|
-
setEditForm({
|
|
611
|
-
...editForm,
|
|
612
|
-
category: e.target.value as Category,
|
|
613
|
-
})
|
|
614
|
-
}
|
|
615
|
-
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
616
|
-
disabled={isSubmitting}
|
|
617
|
-
>
|
|
618
|
-
<option value="indoor">Indoor</option>
|
|
619
|
-
<option value="outdoor">Outdoor</option>
|
|
620
|
-
<option value="commercial">Commercial</option>
|
|
621
|
-
{/* Add project-specific categories, e.g.:
|
|
622
|
-
<option value="landscape-boulders">Landscape Boulders</option>
|
|
623
|
-
*/}
|
|
624
|
-
</select>
|
|
625
|
-
</div>
|
|
626
|
-
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
|
|
627
|
-
<SubmitButton type="submit" disabled={isSubmitting}>
|
|
628
|
-
{isSubmitting ? "Saving..." : "Save"}
|
|
629
|
-
</SubmitButton>
|
|
630
|
-
<CancelButton onClick={handleCloseEditModal} disabled={isSubmitting} />
|
|
631
|
-
</div>
|
|
632
|
-
{error && <p className="text-red-400 text-sm font-medium max-[320px]:text-xs">{error}</p>}
|
|
633
|
-
</form>
|
|
634
|
-
</div>
|
|
635
|
-
</motion.div>
|
|
636
|
-
)}
|
|
637
|
-
|
|
638
|
-
{/* Delete Confirmation Modal */}
|
|
639
|
-
{isAdmin && isConfirmDeleteOpen && (
|
|
640
|
-
<motion.div
|
|
641
|
-
variants={modalVariants}
|
|
642
|
-
initial="hidden"
|
|
643
|
-
animate="visible"
|
|
644
|
-
exit="exit"
|
|
645
|
-
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10000] p-4 isolate"
|
|
646
|
-
onClick={handleCancelDelete}
|
|
647
|
-
aria-modal="true"
|
|
648
|
-
role="dialog"
|
|
649
|
-
>
|
|
650
|
-
<div
|
|
651
|
-
className="relative max-w-sm w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-h-[90vh] overflow-y-auto"
|
|
652
|
-
onClick={(e) => e.stopPropagation()}
|
|
653
|
-
>
|
|
654
|
-
<CloseButton onClick={handleCancelDelete}>
|
|
655
|
-
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
656
|
-
</CloseButton>
|
|
657
|
-
<h3 className="text-xl font-bold text-white mb-4">Confirm Deletion</h3>
|
|
658
|
-
<p className="text-gray-300 mb-6">Are you sure you want to delete this image?</p>
|
|
659
|
-
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
|
|
660
|
-
<DeleteButton
|
|
661
|
-
onClick={handleConfirmDelete}
|
|
662
|
-
disabled={isSubmitting}
|
|
663
|
-
>
|
|
664
|
-
{isSubmitting ? "Deleting..." : "Delete"}
|
|
665
|
-
</DeleteButton>
|
|
666
|
-
<CancelButton onClick={handleCancelDelete} disabled={isSubmitting} />
|
|
667
|
-
</div>
|
|
668
|
-
</div>
|
|
669
|
-
</motion.div>
|
|
670
|
-
)}
|
|
671
|
-
|
|
672
|
-
{/* View Image Modal */}
|
|
673
|
-
{selectedImage && (
|
|
674
|
-
<motion.div
|
|
675
|
-
variants={modalVariants}
|
|
676
|
-
initial="hidden"
|
|
677
|
-
animate="visible"
|
|
678
|
-
exit="exit"
|
|
679
|
-
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10001] p-2 sm:p-4 isolate"
|
|
680
|
-
onClick={handleCloseModal}
|
|
681
|
-
role="dialog"
|
|
682
|
-
aria-modal="true"
|
|
683
|
-
aria-labelledby="modal-image"
|
|
684
|
-
>
|
|
685
|
-
<div
|
|
686
|
-
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"
|
|
687
|
-
onClick={(e) => e.stopPropagation()}
|
|
688
|
-
>
|
|
689
|
-
<CloseButton
|
|
690
|
-
variant="close-form"
|
|
691
|
-
onClick={handleCloseModal}
|
|
692
|
-
>
|
|
693
|
-
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
694
|
-
</CloseButton>
|
|
695
|
-
<div className="relative w-full h-[70vh] sm:h-[80vh]">
|
|
696
|
-
<Image
|
|
697
|
-
src={selectedImage.url}
|
|
698
|
-
alt={
|
|
699
|
-
selectedImage.title?.trim() &&
|
|
700
|
-
!selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
701
|
-
? selectedImage.title
|
|
702
|
-
: "Gallery image"
|
|
703
|
-
}
|
|
704
|
-
fill
|
|
705
|
-
className="object-contain rounded-2xl"
|
|
706
|
-
quality={85}
|
|
707
|
-
/>
|
|
708
|
-
{(selectedImage.title?.trim() &&
|
|
709
|
-
!selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) ||
|
|
710
|
-
selectedImage.description) && (
|
|
711
|
-
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-3 sm:p-4">
|
|
712
|
-
{selectedImage.title?.trim() &&
|
|
713
|
-
!selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) && (
|
|
714
|
-
<h3 className="text-lg sm:text-xl md:text-2xl font-bold text-white drop-shadow-lg">
|
|
715
|
-
{selectedImage.title}
|
|
716
|
-
</h3>
|
|
717
|
-
)}
|
|
718
|
-
{selectedImage.description && (
|
|
719
|
-
<p className="text-xs sm:text-sm md:text-base text-gray-200 drop-shadow-md line-clamp-2">
|
|
720
|
-
{selectedImage.description}
|
|
721
|
-
</p>
|
|
722
|
-
)}
|
|
723
|
-
</div>
|
|
724
|
-
)}
|
|
725
|
-
</div>
|
|
726
|
-
</div>
|
|
727
|
-
</motion.div>
|
|
728
|
-
)}
|
|
729
|
-
</div>
|
|
730
|
-
);
|
|
1
|
+
// src/components/addOns/functional/ImageDescCarousel.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useRef } from "react";
|
|
5
|
+
import {
|
|
6
|
+
ActionButton,
|
|
7
|
+
EditIconButton,
|
|
8
|
+
TrashIconButton,
|
|
9
|
+
CloseButton,
|
|
10
|
+
SubmitButton,
|
|
11
|
+
CancelButton,
|
|
12
|
+
NextButton,
|
|
13
|
+
PrevButton,
|
|
14
|
+
FilterButton,
|
|
15
|
+
DeleteButton,
|
|
16
|
+
} from "@/components/other/button";
|
|
17
|
+
import { Card } from "@/components/other/card";
|
|
18
|
+
import Image from "next/image";
|
|
19
|
+
import { Upload, ChevronRight, X, ChevronLeft } from "lucide-react";
|
|
20
|
+
import { motion, useScroll, useTransform } from "framer-motion";
|
|
21
|
+
import { useAuth, useUser } from "@clerk/nextjs";
|
|
22
|
+
import { StrapiUser, UploadedImage, Category } from "@/components/types";
|
|
23
|
+
|
|
24
|
+
interface ImageDescCarouselProps {
|
|
25
|
+
user: StrapiUser | null;
|
|
26
|
+
uploadedImages: UploadedImage[];
|
|
27
|
+
setUploadedImages: (images: UploadedImage[]) => void;
|
|
28
|
+
error: string | null;
|
|
29
|
+
setError: (error: string | null) => void;
|
|
30
|
+
isLoading: boolean;
|
|
31
|
+
setIsLoading: (isLoading: boolean) => void;
|
|
32
|
+
handleImageUpload: (
|
|
33
|
+
e: React.FormEvent<HTMLFormElement>,
|
|
34
|
+
file: File | null,
|
|
35
|
+
title: string,
|
|
36
|
+
description: string,
|
|
37
|
+
category: Category
|
|
38
|
+
) => Promise<void>;
|
|
39
|
+
handleDeleteImage: (documentId: string) => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const Slideshow = ({
|
|
43
|
+
images,
|
|
44
|
+
altPrefix,
|
|
45
|
+
currentSlide,
|
|
46
|
+
setCurrentSlide,
|
|
47
|
+
isAdmin,
|
|
48
|
+
handleImageClick,
|
|
49
|
+
handleEditImage,
|
|
50
|
+
handleDeleteImage,
|
|
51
|
+
}: {
|
|
52
|
+
images: UploadedImage[];
|
|
53
|
+
altPrefix: string;
|
|
54
|
+
currentSlide: number;
|
|
55
|
+
setCurrentSlide: React.Dispatch<React.SetStateAction<number>>;
|
|
56
|
+
isAdmin: boolean;
|
|
57
|
+
handleImageClick: (image: UploadedImage) => void;
|
|
58
|
+
handleEditImage: (image: UploadedImage) => void;
|
|
59
|
+
handleDeleteImage: (documentId: string) => void;
|
|
60
|
+
}) => {
|
|
61
|
+
const slideshowRef = useRef<HTMLDivElement>(null);
|
|
62
|
+
|
|
63
|
+
const goToPrev = () => setCurrentSlide((prev) => (prev - 1 + images.length) % images.length);
|
|
64
|
+
const goToNext = () => setCurrentSlide((prev) => (prev + 1) % images.length);
|
|
65
|
+
|
|
66
|
+
if (!images.length || !images[currentSlide]) {
|
|
67
|
+
return (
|
|
68
|
+
<Card
|
|
69
|
+
className="relative h-[40vh] min-h-[400px] sm:h-[50vh] lg:h-[600px] rounded-3xl overflow-hidden bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl w-full mx-auto max-w-3xl md:max-w-none md:mx-0 flex items-center justify-center supports-[not(backdrop-filter:blur(10px))]:bg-white/20"
|
|
70
|
+
ref={slideshowRef}
|
|
71
|
+
>
|
|
72
|
+
<p className="text-gray-600 text-lg">No images available</p>
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className="relative h-[40vh] min-h-[400px] sm:h-[50vh] lg:h-[600px] rounded-3xl overflow-hidden bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl w-full mx-auto max-w-3xl md:max-w-none md:mx-0 supports-[not(backdrop-filter:blur(10px))]:bg-white/20"
|
|
80
|
+
ref={slideshowRef}
|
|
81
|
+
>
|
|
82
|
+
<div
|
|
83
|
+
className="relative w-full h-full cursor-pointer"
|
|
84
|
+
onClick={() => handleImageClick(images[currentSlide])}
|
|
85
|
+
>
|
|
86
|
+
<Image
|
|
87
|
+
src={images[currentSlide].url}
|
|
88
|
+
alt={
|
|
89
|
+
images[currentSlide].title?.trim() &&
|
|
90
|
+
!images[currentSlide].title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
91
|
+
? images[currentSlide].title
|
|
92
|
+
: altPrefix
|
|
93
|
+
}
|
|
94
|
+
fill
|
|
95
|
+
className="object-cover rounded-2xl transition-transform duration-700 hover:scale-110"
|
|
96
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
97
|
+
loading="lazy"
|
|
98
|
+
quality={85}
|
|
99
|
+
/>
|
|
100
|
+
{isAdmin && (
|
|
101
|
+
<div className="absolute top-2 right-2 flex space-x-2 opacity-0 hover:opacity-100 transition-opacity duration-500">
|
|
102
|
+
<EditIconButton
|
|
103
|
+
onClick={(e) => {
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
handleEditImage(images[currentSlide]);
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
<TrashIconButton
|
|
109
|
+
onClick={(e) => {
|
|
110
|
+
e.stopPropagation();
|
|
111
|
+
handleDeleteImage(images[currentSlide].documentId);
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
{images.length > 1 && (
|
|
118
|
+
<>
|
|
119
|
+
<PrevButton
|
|
120
|
+
onClick={(e) => {
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
goToPrev();
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
<NextButton
|
|
126
|
+
onClick={(e) => {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
goToNext();
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export function ImageDescCarousel({
|
|
138
|
+
user,
|
|
139
|
+
uploadedImages,
|
|
140
|
+
setUploadedImages,
|
|
141
|
+
error,
|
|
142
|
+
setError,
|
|
143
|
+
isLoading,
|
|
144
|
+
setIsLoading,
|
|
145
|
+
handleImageUpload,
|
|
146
|
+
handleDeleteImage,
|
|
147
|
+
}: ImageDescCarouselProps) {
|
|
148
|
+
const { isSignedIn } = useUser();
|
|
149
|
+
const { getToken } = useAuth();
|
|
150
|
+
const [activeTab, setActiveTab] = useState<Category>("indoor");
|
|
151
|
+
const [currentSlide, setCurrentSlide] = useState(0);
|
|
152
|
+
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
|
153
|
+
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
154
|
+
const [isConfirmDeleteOpen, setIsConfirmDeleteOpen] = useState(false);
|
|
155
|
+
const [documentIdToDelete, setDocumentIdToDelete] = useState<string | null>(null);
|
|
156
|
+
const [selectedImage, setSelectedImage] = useState<UploadedImage | null>(null);
|
|
157
|
+
const [uploadForm, setUploadForm] = useState<{
|
|
158
|
+
file: File | null;
|
|
159
|
+
title: string;
|
|
160
|
+
description: string;
|
|
161
|
+
category: Category;
|
|
162
|
+
}>({ file: null, title: "", description: "", category: "indoor" });
|
|
163
|
+
const [editForm, setEditForm] = useState<{
|
|
164
|
+
id: number;
|
|
165
|
+
documentId: string;
|
|
166
|
+
title: string;
|
|
167
|
+
description: string;
|
|
168
|
+
category: Category;
|
|
169
|
+
file: File | null;
|
|
170
|
+
}>({ id: 0, documentId: "", title: "", description: "", category: "indoor", file: null });
|
|
171
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
172
|
+
|
|
173
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
174
|
+
const { scrollYProgress } = useScroll({
|
|
175
|
+
target: containerRef,
|
|
176
|
+
offset: ["start end", "end start"],
|
|
177
|
+
});
|
|
178
|
+
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -50]);
|
|
179
|
+
|
|
180
|
+
const isAdmin = isSignedIn && !!user?.businessAdminId || false;
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const isAnyModalOpen = isUploadModalOpen || isEditModalOpen || isConfirmDeleteOpen || !!selectedImage;
|
|
184
|
+
if (isAnyModalOpen) {
|
|
185
|
+
document.body.style.overflow = "hidden";
|
|
186
|
+
} else {
|
|
187
|
+
document.body.style.overflow = "";
|
|
188
|
+
}
|
|
189
|
+
window.dispatchEvent(
|
|
190
|
+
new CustomEvent("modalStateChange", { detail: { isOpen: isAnyModalOpen } })
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return () => {
|
|
194
|
+
document.body.style.overflow = "";
|
|
195
|
+
};
|
|
196
|
+
}, [isUploadModalOpen, isEditModalOpen, isConfirmDeleteOpen, selectedImage]);
|
|
197
|
+
|
|
198
|
+
const filteredImages = uploadedImages.filter(
|
|
199
|
+
(img) => (img.category || "none") === activeTab
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const handleImageClick = (image: UploadedImage) => {
|
|
203
|
+
setSelectedImage(image);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleCloseModal = () => {
|
|
207
|
+
setSelectedImage(null);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const openEditModal = (image: UploadedImage) => {
|
|
211
|
+
if (!isAdmin) {
|
|
212
|
+
setError("Unauthorized: Only admins can edit images");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
setEditForm({
|
|
216
|
+
id: image.id,
|
|
217
|
+
documentId: image.documentId,
|
|
218
|
+
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 : "",
|
|
219
|
+
description: image.description || "",
|
|
220
|
+
category: image.category && image.category !== "none" ? image.category : "indoor",
|
|
221
|
+
file: null,
|
|
222
|
+
});
|
|
223
|
+
setIsEditModalOpen(true);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const handleCloseEditModal = () => {
|
|
227
|
+
setIsEditModalOpen(false);
|
|
228
|
+
setEditForm({ id: 0, documentId: "", title: "", description: "", category: "indoor", file: null });
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const handleCloseUploadModal = () => {
|
|
232
|
+
setIsUploadModalOpen(false);
|
|
233
|
+
setUploadForm({ file: null, title: "", description: "", category: "indoor" });
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const openConfirmDelete = (documentId: string) => {
|
|
237
|
+
if (!isAdmin) {
|
|
238
|
+
setError("Unauthorized: Only admins can delete images");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
setDocumentIdToDelete(documentId);
|
|
242
|
+
setIsConfirmDeleteOpen(true);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handleConfirmDelete = async () => {
|
|
246
|
+
if (!isAdmin) {
|
|
247
|
+
setError("Unauthorized: Only admins can delete images");
|
|
248
|
+
setIsConfirmDeleteOpen(false);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (documentIdToDelete !== null) {
|
|
252
|
+
setIsLoading(true);
|
|
253
|
+
try {
|
|
254
|
+
await handleDeleteImage(documentIdToDelete);
|
|
255
|
+
} finally {
|
|
256
|
+
setIsLoading(false);
|
|
257
|
+
setIsConfirmDeleteOpen(false);
|
|
258
|
+
setDocumentIdToDelete(null);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const handleCancelDelete = () => {
|
|
264
|
+
setIsConfirmDeleteOpen(false);
|
|
265
|
+
setDocumentIdToDelete(null);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const handleEditImage = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
if (!isAdmin) {
|
|
271
|
+
console.error("ImageDescCarousel: Unauthorized edit attempt", {
|
|
272
|
+
isSignedIn,
|
|
273
|
+
businessAdminId: user?.businessAdminId,
|
|
274
|
+
});
|
|
275
|
+
setError("Unauthorized: Only admins can edit images");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
setIsLoading(true);
|
|
281
|
+
setIsSubmitting(true);
|
|
282
|
+
const token = await getToken();
|
|
283
|
+
if (!token) {
|
|
284
|
+
console.error("ImageDescCarousel: No authentication token available");
|
|
285
|
+
setError("Authentication error: Please log in again");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const formData = new FormData();
|
|
290
|
+
formData.append("documentId", editForm.documentId);
|
|
291
|
+
formData.append("title", editForm.title || `Image ${new Date().toISOString()}`);
|
|
292
|
+
formData.append("description", editForm.description || "");
|
|
293
|
+
formData.append("category", editForm.category);
|
|
294
|
+
if (editForm.file) {
|
|
295
|
+
formData.append("file", editForm.file);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const response = await fetch("/api/gallery-data", {
|
|
299
|
+
method: "PUT",
|
|
300
|
+
headers: {
|
|
301
|
+
Authorization: `Bearer ${token}`,
|
|
302
|
+
},
|
|
303
|
+
body: formData,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
const errorData = await response.json();
|
|
308
|
+
console.error("ImageDescCarousel: Edit failed", { status: response.status, errorData });
|
|
309
|
+
if (response.status === 401) {
|
|
310
|
+
setError("Authentication error: Please log in again");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
throw new Error(errorData.error || `Failed to edit image (Status: ${response.status})`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const { data } = await response.json();
|
|
317
|
+
setUploadedImages(data || []);
|
|
318
|
+
setError(null);
|
|
319
|
+
setIsEditModalOpen(false);
|
|
320
|
+
setEditForm({ id: 0, documentId: "", title: "", description: "", category: "indoor", file: null });
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error("ImageDescCarousel: Edit Error", err);
|
|
323
|
+
setError(err instanceof Error ? err.message : "Failed to edit image");
|
|
324
|
+
} finally {
|
|
325
|
+
setIsLoading(false);
|
|
326
|
+
setIsSubmitting(false);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const sectionVariants = {
|
|
331
|
+
hidden: { opacity: 0 },
|
|
332
|
+
visible: {
|
|
333
|
+
opacity: 1,
|
|
334
|
+
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1], staggerChildren: 0.1 },
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const modalVariants = {
|
|
339
|
+
hidden: { opacity: 0, y: "100vh" },
|
|
340
|
+
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] } },
|
|
341
|
+
exit: { opacity: 0, y: "100vh", transition: { duration: 0.3, ease: "easeIn" } },
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className="w-full">
|
|
346
|
+
{/* Gallery Section */}
|
|
347
|
+
<motion.section
|
|
348
|
+
variants={sectionVariants}
|
|
349
|
+
initial="hidden"
|
|
350
|
+
whileInView="visible"
|
|
351
|
+
viewport={{ once: true }}
|
|
352
|
+
className="relative py-12 sm:py-16 lg:pb-24 w-full bg-gray-50/50 backdrop-blur-sm"
|
|
353
|
+
ref={containerRef}
|
|
354
|
+
style={{ y: parallaxY }}
|
|
355
|
+
>
|
|
356
|
+
<div className="relative z-10 w-full px-4 sm:px-6 lg:px-8">
|
|
357
|
+
{error && <p className="text-red-600 text-lg text-center mb-8">{error}</p>}
|
|
358
|
+
{user && !isAdmin && (
|
|
359
|
+
<p className="text-yellow-600 text-lg text-center mb-8">
|
|
360
|
+
You are logged in but do not have admin privileges.
|
|
361
|
+
</p>
|
|
362
|
+
)}
|
|
363
|
+
{isAdmin && (
|
|
364
|
+
<div className="flex justify-center mb-12">
|
|
365
|
+
<ActionButton
|
|
366
|
+
onClick={() => setIsUploadModalOpen(true)}
|
|
367
|
+
className="flex items-center"
|
|
368
|
+
disabled={isLoading || isSubmitting}
|
|
369
|
+
>
|
|
370
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
371
|
+
Upload New Image
|
|
372
|
+
</ActionButton>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 sm:gap-8 lg:gap-12 items-start">
|
|
376
|
+
<div className="h-auto min-h-[600px] bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl p-6 sm:p-8 lg:p-10 rounded-3xl flex flex-col space-y-4 sm:space-y-6 order-2 max-w-3xl mx-auto md:max-w-none md:mx-0 md:order-1 supports-[not(backdrop-filter:blur(10px))]:bg-white/20">
|
|
377
|
+
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-gray-900">
|
|
378
|
+
{filteredImages[currentSlide]?.title?.trim() &&
|
|
379
|
+
!filteredImages[currentSlide]?.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
380
|
+
? filteredImages[currentSlide].title
|
|
381
|
+
: "Image Gallery"}
|
|
382
|
+
</h2>
|
|
383
|
+
<div className="text-sm sm:text-base lg:text-lg text-gray-700">
|
|
384
|
+
{filteredImages.length > 0 && currentSlide >= 0 && currentSlide < filteredImages.length ? (
|
|
385
|
+
(filteredImages[currentSlide].description || "")
|
|
386
|
+
.split("\n")
|
|
387
|
+
.filter((paragraph) => paragraph.trim() !== "")
|
|
388
|
+
.map((paragraph, index) => (
|
|
389
|
+
<p key={index} className="mb-4">
|
|
390
|
+
{paragraph}
|
|
391
|
+
</p>
|
|
392
|
+
)) || <p className="mb-4">No description available.</p>
|
|
393
|
+
) : (
|
|
394
|
+
<p className="mb-4">No description available.</p>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
<div className="order-1 md:order-2">
|
|
399
|
+
<Slideshow
|
|
400
|
+
images={filteredImages}
|
|
401
|
+
altPrefix="Image Gallery"
|
|
402
|
+
currentSlide={currentSlide}
|
|
403
|
+
setCurrentSlide={setCurrentSlide}
|
|
404
|
+
isAdmin={isAdmin}
|
|
405
|
+
handleImageClick={handleImageClick}
|
|
406
|
+
handleEditImage={openEditModal}
|
|
407
|
+
handleDeleteImage={openConfirmDelete}
|
|
408
|
+
/>
|
|
409
|
+
<div className="flex flex-wrap justify-center gap-2 sm:gap-4 mt-6 z-20">
|
|
410
|
+
{["indoor", "outdoor", "commercial"].map((tab) => (
|
|
411
|
+
<FilterButton
|
|
412
|
+
key={tab}
|
|
413
|
+
isActive={activeTab === tab}
|
|
414
|
+
onClick={() => {
|
|
415
|
+
setActiveTab(tab as Category);
|
|
416
|
+
setCurrentSlide(0);
|
|
417
|
+
}}
|
|
418
|
+
>
|
|
419
|
+
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
420
|
+
</FilterButton>
|
|
421
|
+
))}
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</motion.section>
|
|
427
|
+
|
|
428
|
+
{/* Upload Modal */}
|
|
429
|
+
{isAdmin && isUploadModalOpen && (
|
|
430
|
+
<motion.div
|
|
431
|
+
variants={modalVariants}
|
|
432
|
+
initial="hidden"
|
|
433
|
+
animate="visible"
|
|
434
|
+
exit="exit"
|
|
435
|
+
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10000] p-4 isolate"
|
|
436
|
+
onClick={handleCloseUploadModal}
|
|
437
|
+
aria-modal="true"
|
|
438
|
+
role="dialog"
|
|
439
|
+
>
|
|
440
|
+
<div
|
|
441
|
+
className="relative max-w-5xl w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-h-[90vh] overflow-y-auto"
|
|
442
|
+
onClick={(e) => e.stopPropagation()}
|
|
443
|
+
>
|
|
444
|
+
<CloseButton onClick={handleCloseUploadModal}>
|
|
445
|
+
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
446
|
+
</CloseButton>
|
|
447
|
+
<h3 className="text-xl font-bold text-white mb-4">Upload Image</h3>
|
|
448
|
+
<form
|
|
449
|
+
onSubmit={(e) => {
|
|
450
|
+
setIsSubmitting(true);
|
|
451
|
+
handleImageUpload(e, uploadForm.file, uploadForm.title, uploadForm.description, uploadForm.category).then(() => {
|
|
452
|
+
handleCloseUploadModal();
|
|
453
|
+
setIsSubmitting(false);
|
|
454
|
+
});
|
|
455
|
+
}}
|
|
456
|
+
className="space-y-4"
|
|
457
|
+
>
|
|
458
|
+
<div>
|
|
459
|
+
<label htmlFor="image-upload" className="block text-sm font-medium text-gray-300 mb-1">
|
|
460
|
+
Choose Image
|
|
461
|
+
</label>
|
|
462
|
+
<input
|
|
463
|
+
type="file"
|
|
464
|
+
accept="image/jpeg,image/png,image/gif"
|
|
465
|
+
onChange={(e) => setUploadForm({ ...uploadForm, file: e.target.files?.[0] || null })}
|
|
466
|
+
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
467
|
+
id="image-upload"
|
|
468
|
+
disabled={isSubmitting}
|
|
469
|
+
/>
|
|
470
|
+
{uploadForm.file && (
|
|
471
|
+
<p className="mt-2 text-gray-300 text-sm max-[320px]:text-xs">Selected: {uploadForm.file.name}</p>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
474
|
+
<div>
|
|
475
|
+
<label htmlFor="image-title" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
476
|
+
Image Title (optional)
|
|
477
|
+
</label>
|
|
478
|
+
<input
|
|
479
|
+
id="image-title"
|
|
480
|
+
value={uploadForm.title}
|
|
481
|
+
onChange={(e) => setUploadForm({ ...uploadForm, title: e.target.value })}
|
|
482
|
+
placeholder="Enter image title"
|
|
483
|
+
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
484
|
+
disabled={isSubmitting}
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
<div>
|
|
488
|
+
<label htmlFor="image-description" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
489
|
+
Image Description (optional)
|
|
490
|
+
</label>
|
|
491
|
+
<textarea
|
|
492
|
+
id="image-description"
|
|
493
|
+
value={uploadForm.description}
|
|
494
|
+
onChange={(e) => setUploadForm({ ...uploadForm, description: e.target.value })}
|
|
495
|
+
placeholder="Enter image description"
|
|
496
|
+
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm min-h-[80px] max-[768px]:text-sm max-[768px]:min-h-[80px] max-[320px]:text-xs max-[320px]:p-2"
|
|
497
|
+
disabled={isSubmitting}
|
|
498
|
+
/>
|
|
499
|
+
</div>
|
|
500
|
+
<div>
|
|
501
|
+
<label htmlFor="image-category" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
502
|
+
Category
|
|
503
|
+
</label>
|
|
504
|
+
<select
|
|
505
|
+
id="image-category"
|
|
506
|
+
value={uploadForm.category}
|
|
507
|
+
onChange={(e) =>
|
|
508
|
+
setUploadForm({
|
|
509
|
+
...uploadForm,
|
|
510
|
+
category: e.target.value as Category,
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
514
|
+
disabled={isSubmitting}
|
|
515
|
+
>
|
|
516
|
+
<option value="indoor">Indoor</option>
|
|
517
|
+
<option value="outdoor">Outdoor</option>
|
|
518
|
+
<option value="commercial">Commercial</option>
|
|
519
|
+
{/* Add project-specific categories, e.g.:
|
|
520
|
+
<option value="landscape-boulders">Landscape Boulders</option>
|
|
521
|
+
*/}
|
|
522
|
+
</select>
|
|
523
|
+
</div>
|
|
524
|
+
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
|
|
525
|
+
<SubmitButton type="submit" disabled={isSubmitting || !uploadForm.file}>
|
|
526
|
+
{isSubmitting ? "Uploading..." : "Upload"}
|
|
527
|
+
</SubmitButton>
|
|
528
|
+
<CancelButton onClick={handleCloseUploadModal} disabled={isSubmitting} />
|
|
529
|
+
</div>
|
|
530
|
+
{error && <p className="text-red-400 text-sm font-medium max-[320px]:text-xs">{error}</p>}
|
|
531
|
+
</form>
|
|
532
|
+
</div>
|
|
533
|
+
</motion.div>
|
|
534
|
+
)}
|
|
535
|
+
|
|
536
|
+
{/* Edit Modal */}
|
|
537
|
+
{isAdmin && isEditModalOpen && (
|
|
538
|
+
<motion.div
|
|
539
|
+
variants={modalVariants}
|
|
540
|
+
initial="hidden"
|
|
541
|
+
animate="visible"
|
|
542
|
+
exit="exit"
|
|
543
|
+
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10000] p-4 isolate"
|
|
544
|
+
onClick={handleCloseEditModal}
|
|
545
|
+
aria-modal="true"
|
|
546
|
+
role="dialog"
|
|
547
|
+
>
|
|
548
|
+
<div
|
|
549
|
+
className="relative max-w-5xl w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-h-[90vh] overflow-y-auto"
|
|
550
|
+
onClick={(e) => e.stopPropagation()}
|
|
551
|
+
>
|
|
552
|
+
<CloseButton onClick={handleCloseEditModal}>
|
|
553
|
+
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
554
|
+
</CloseButton>
|
|
555
|
+
<h3 className="text-xl font-bold text-white mb-4">Edit Image</h3>
|
|
556
|
+
<form
|
|
557
|
+
onSubmit={handleEditImage}
|
|
558
|
+
className="space-y-4"
|
|
559
|
+
>
|
|
560
|
+
<div>
|
|
561
|
+
<label htmlFor="edit-image" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
562
|
+
Replace Image (optional)
|
|
563
|
+
</label>
|
|
564
|
+
<input
|
|
565
|
+
id="edit-image"
|
|
566
|
+
type="file"
|
|
567
|
+
accept="image/jpeg,image/png,image/gif"
|
|
568
|
+
onChange={(e) => setEditForm({ ...editForm, file: e.target.files?.[0] || null })}
|
|
569
|
+
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
570
|
+
disabled={isSubmitting}
|
|
571
|
+
/>
|
|
572
|
+
{editForm.file && (
|
|
573
|
+
<p className="mt-2 text-gray-300 text-sm max-[320px]:text-xs">Selected: {editForm.file.name}</p>
|
|
574
|
+
)}
|
|
575
|
+
</div>
|
|
576
|
+
<div>
|
|
577
|
+
<label htmlFor="edit-title" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
578
|
+
Image Title (optional)
|
|
579
|
+
</label>
|
|
580
|
+
<input
|
|
581
|
+
id="edit-title"
|
|
582
|
+
value={editForm.title}
|
|
583
|
+
onChange={(e) => setEditForm({ ...editForm, title: e.target.value })}
|
|
584
|
+
placeholder="Enter image title"
|
|
585
|
+
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
586
|
+
disabled={isSubmitting}
|
|
587
|
+
/>
|
|
588
|
+
</div>
|
|
589
|
+
<div>
|
|
590
|
+
<label htmlFor="edit-description" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
591
|
+
Image Description (optional)
|
|
592
|
+
</label>
|
|
593
|
+
<textarea
|
|
594
|
+
id="edit-description"
|
|
595
|
+
value={editForm.description}
|
|
596
|
+
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
|
597
|
+
placeholder="Enter image description"
|
|
598
|
+
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm min-h-[80px] max-[768px]:text-sm max-[768px]:min-h-[80px] max-[320px]:text-xs max-[320px]:p-2"
|
|
599
|
+
disabled={isSubmitting}
|
|
600
|
+
/>
|
|
601
|
+
</div>
|
|
602
|
+
<div>
|
|
603
|
+
<label htmlFor="edit-category" className="block text-sm font-medium text-gray-300 mb-1 max-[320px]:text-xs">
|
|
604
|
+
Category
|
|
605
|
+
</label>
|
|
606
|
+
<select
|
|
607
|
+
id="edit-category"
|
|
608
|
+
value={editForm.category}
|
|
609
|
+
onChange={(e) =>
|
|
610
|
+
setEditForm({
|
|
611
|
+
...editForm,
|
|
612
|
+
category: e.target.value as Category,
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
className="w-full p-2 rounded-md bg-gray-700 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm max-[768px]:text-sm max-[320px]:text-xs max-[320px]:p-2"
|
|
616
|
+
disabled={isSubmitting}
|
|
617
|
+
>
|
|
618
|
+
<option value="indoor">Indoor</option>
|
|
619
|
+
<option value="outdoor">Outdoor</option>
|
|
620
|
+
<option value="commercial">Commercial</option>
|
|
621
|
+
{/* Add project-specific categories, e.g.:
|
|
622
|
+
<option value="landscape-boulders">Landscape Boulders</option>
|
|
623
|
+
*/}
|
|
624
|
+
</select>
|
|
625
|
+
</div>
|
|
626
|
+
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
|
|
627
|
+
<SubmitButton type="submit" disabled={isSubmitting}>
|
|
628
|
+
{isSubmitting ? "Saving..." : "Save"}
|
|
629
|
+
</SubmitButton>
|
|
630
|
+
<CancelButton onClick={handleCloseEditModal} disabled={isSubmitting} />
|
|
631
|
+
</div>
|
|
632
|
+
{error && <p className="text-red-400 text-sm font-medium max-[320px]:text-xs">{error}</p>}
|
|
633
|
+
</form>
|
|
634
|
+
</div>
|
|
635
|
+
</motion.div>
|
|
636
|
+
)}
|
|
637
|
+
|
|
638
|
+
{/* Delete Confirmation Modal */}
|
|
639
|
+
{isAdmin && isConfirmDeleteOpen && (
|
|
640
|
+
<motion.div
|
|
641
|
+
variants={modalVariants}
|
|
642
|
+
initial="hidden"
|
|
643
|
+
animate="visible"
|
|
644
|
+
exit="exit"
|
|
645
|
+
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10000] p-4 isolate"
|
|
646
|
+
onClick={handleCancelDelete}
|
|
647
|
+
aria-modal="true"
|
|
648
|
+
role="dialog"
|
|
649
|
+
>
|
|
650
|
+
<div
|
|
651
|
+
className="relative max-w-sm w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg max-h-[90vh] overflow-y-auto"
|
|
652
|
+
onClick={(e) => e.stopPropagation()}
|
|
653
|
+
>
|
|
654
|
+
<CloseButton onClick={handleCancelDelete}>
|
|
655
|
+
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
656
|
+
</CloseButton>
|
|
657
|
+
<h3 className="text-xl font-bold text-white mb-4">Confirm Deletion</h3>
|
|
658
|
+
<p className="text-gray-300 mb-6">Are you sure you want to delete this image?</p>
|
|
659
|
+
<div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
|
|
660
|
+
<DeleteButton
|
|
661
|
+
onClick={handleConfirmDelete}
|
|
662
|
+
disabled={isSubmitting}
|
|
663
|
+
>
|
|
664
|
+
{isSubmitting ? "Deleting..." : "Delete"}
|
|
665
|
+
</DeleteButton>
|
|
666
|
+
<CancelButton onClick={handleCancelDelete} disabled={isSubmitting} />
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
</motion.div>
|
|
670
|
+
)}
|
|
671
|
+
|
|
672
|
+
{/* View Image Modal */}
|
|
673
|
+
{selectedImage && (
|
|
674
|
+
<motion.div
|
|
675
|
+
variants={modalVariants}
|
|
676
|
+
initial="hidden"
|
|
677
|
+
animate="visible"
|
|
678
|
+
exit="exit"
|
|
679
|
+
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10001] p-2 sm:p-4 isolate"
|
|
680
|
+
onClick={handleCloseModal}
|
|
681
|
+
role="dialog"
|
|
682
|
+
aria-modal="true"
|
|
683
|
+
aria-labelledby="modal-image"
|
|
684
|
+
>
|
|
685
|
+
<div
|
|
686
|
+
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"
|
|
687
|
+
onClick={(e) => e.stopPropagation()}
|
|
688
|
+
>
|
|
689
|
+
<CloseButton
|
|
690
|
+
variant="close-form"
|
|
691
|
+
onClick={handleCloseModal}
|
|
692
|
+
>
|
|
693
|
+
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
694
|
+
</CloseButton>
|
|
695
|
+
<div className="relative w-full h-[70vh] sm:h-[80vh]">
|
|
696
|
+
<Image
|
|
697
|
+
src={selectedImage.url}
|
|
698
|
+
alt={
|
|
699
|
+
selectedImage.title?.trim() &&
|
|
700
|
+
!selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
701
|
+
? selectedImage.title
|
|
702
|
+
: "Gallery image"
|
|
703
|
+
}
|
|
704
|
+
fill
|
|
705
|
+
className="object-contain rounded-2xl"
|
|
706
|
+
quality={85}
|
|
707
|
+
/>
|
|
708
|
+
{(selectedImage.title?.trim() &&
|
|
709
|
+
!selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) ||
|
|
710
|
+
selectedImage.description) && (
|
|
711
|
+
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-3 sm:p-4">
|
|
712
|
+
{selectedImage.title?.trim() &&
|
|
713
|
+
!selectedImage.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) && (
|
|
714
|
+
<h3 className="text-lg sm:text-xl md:text-2xl font-bold text-white drop-shadow-lg">
|
|
715
|
+
{selectedImage.title}
|
|
716
|
+
</h3>
|
|
717
|
+
)}
|
|
718
|
+
{selectedImage.description && (
|
|
719
|
+
<p className="text-xs sm:text-sm md:text-base text-gray-200 drop-shadow-md line-clamp-2">
|
|
720
|
+
{selectedImage.description}
|
|
721
|
+
</p>
|
|
722
|
+
)}
|
|
723
|
+
</div>
|
|
724
|
+
)}
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
</motion.div>
|
|
728
|
+
)}
|
|
729
|
+
</div>
|
|
730
|
+
);
|
|
731
731
|
}
|