@devvistatech/devvista-kit 0.0.10 → 0.0.13

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