@devvistatech/devvista-kit 0.0.1

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