@devvistatech/devvista-kit 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +40 -0
  2. package/app/ClientLayout.tsx +66 -0
  3. package/app/about/page.tsx +11 -248
  4. package/app/adRequest/page.tsx +101 -25
  5. package/app/admin-profile/page.tsx +123 -0
  6. package/app/analytics/page.tsx +41 -5
  7. package/app/api/about/route.ts +2 -18
  8. package/app/api/adRequest/route.ts +7 -27
  9. package/app/api/analytics/[reportType]/route.ts +1 -64
  10. package/app/api/bio/route.ts +1 -17
  11. package/app/api/blog/route.ts +1 -19
  12. package/app/api/contacts/route.ts +1 -46
  13. package/app/api/files/route.ts +1 -15
  14. package/app/api/gallery-data/route.ts +53 -61
  15. package/app/api/schedule/route.ts +5 -21
  16. package/app/api/signup/route.ts +129 -0
  17. package/app/api/sync-user/route.ts +268 -94
  18. package/app/api/verify-admin/route.ts +46 -0
  19. package/app/blog/[id]/page.tsx +71 -52
  20. package/app/blog/page.tsx +43 -10
  21. package/app/favicon.ico +0 -0
  22. package/app/gallery/page.tsx +27 -6
  23. package/app/layout.tsx +31 -82
  24. package/app/page.tsx +20 -311
  25. package/app/products/constants/product.ts +27 -0
  26. package/app/products/page.tsx +296 -0
  27. package/app/products/productOne/page.tsx +266 -0
  28. package/app/products/productTwo/page.tsx +272 -0
  29. package/app/schedule/page.tsx +78 -40
  30. package/bin/init.js +0 -12
  31. package/components/addOns/functional/ClassList.tsx +21 -17
  32. package/components/addOns/functional/ProductList.tsx +1027 -0
  33. package/components/addOns/functional/aboutSections/AboutSection.tsx +107 -70
  34. package/components/addOns/functional/aboutSections/constants/aboutSection.ts +9 -4
  35. package/components/addOns/functional/banner/Banner.tsx +150 -0
  36. package/components/addOns/functional/banner/BannerDashboard.tsx +283 -0
  37. package/components/addOns/functional/bioSections/BioEditor.tsx +471 -0
  38. package/components/addOns/functional/bioSections/constants/bioEditor.ts +36 -0
  39. package/components/addOns/functional/blogSections/BlogDashboard.tsx +1 -1
  40. package/components/addOns/functional/blogSections/BlogFormPopUp.tsx +2 -1
  41. package/components/addOns/functional/{ImageDescCarousel.tsx → carousels/ImageDescCarousel.tsx} +166 -57
  42. package/components/addOns/functional/carousels/ProductDescCarousel.tsx +1129 -0
  43. package/components/addOns/functional/{ScheduleCarousel.tsx → carousels/ScheduleCarousel.tsx} +110 -50
  44. package/components/addOns/functional/carousels/constants.ts/productDescCarousel.ts +197 -0
  45. package/components/addOns/functional/carousels/constants.ts/scheduleCarousel.ts +20 -0
  46. package/components/addOns/functional/contactsDashboard/ContactsDashboard.tsx +1 -1
  47. package/components/addOns/functional/fileUploaders/FileUploader.tsx +437 -0
  48. package/components/addOns/functional/fileUploaders/constants/fileUploader.ts +45 -0
  49. package/components/addOns/functional/galleries/GalleryComplex.tsx +468 -267
  50. package/components/addOns/functional/galleries/GallerySimple.tsx +78 -50
  51. package/components/addOns/functional/galleries/ThreeSetGallery.tsx +260 -0
  52. package/components/addOns/functional/schedules/ScheduleGridOne.tsx +22 -8
  53. package/components/addOns/functional/schedules/ScheduleGridTwo.tsx +12 -7
  54. package/components/addOns/functional/schedules/ScheduleGridTwoBasic.tsx +12 -7
  55. package/components/addOns/non-functional/SampleCarousel.tsx +3 -3
  56. package/components/addOns/non-functional/ThreeSetGallery.tsx +3 -3
  57. package/components/addOns/non-functional/featureSections/FeaturesSection.tsx +74 -0
  58. package/components/addOns/non-functional/featureSections/constants/featuresSection.ts +30 -0
  59. package/components/addOns/non-functional/{Heros/HeroSection.tsx → heros/HomeHero.tsx} +17 -15
  60. package/components/addOns/non-functional/heros/ProductHero.tsx +111 -0
  61. package/components/addOns/non-functional/heros/constants/hero.ts +62 -0
  62. package/components/addOns/non-functional/imageCarousels/ProductSlider.tsx +6 -6
  63. package/components/addOns/non-functional/imageCarousels/ProgramCarousel.tsx +10 -10
  64. package/components/footers/footer.tsx +161 -198
  65. package/components/other/admin-menu.tsx +1 -1
  66. package/lib/auth/auth-context.tsx +225 -0
  67. package/lib/auth/auth-utils.tsx +30 -0
  68. package/lib/constants/adRequest.ts +199 -56
  69. package/lib/constants/admin-profile.ts +12 -0
  70. package/lib/constants/page.ts +15 -15
  71. package/lib/google/google-analytics-tracking.tsx +44 -0
  72. package/lib/types.ts +235 -0
  73. package/lib/utils/compressImage.tsx +32 -0
  74. package/middleware.ts +9 -5
  75. package/next.config.js +1 -1
  76. package/package.json +3 -2
  77. package/public/images/test.png +0 -0
  78. package/components/addOns/functional/BioEditor.tsx +0 -447
  79. package/components/addOns/functional/FileUploader.tsx +0 -295
  80. package/components/addOns/non-functional/FeaturesSection.tsx +0 -63
  81. package/components/types.ts +0 -50
  82. package/lib/auth-context.tsx +0 -131
  83. package/lib/verify-user.ts +0 -118
  84. /package/lib/{google-analytics.tsx → google/google-analytics.tsx} +0 -0
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useRef, useEffect, Dispatch, SetStateAction } from "react";
3
+ import { useState, useRef, useEffect } from "react";
4
4
  import { Card } from "@/components/other/card";
5
5
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/other/tabs";
6
6
  import { motion, useScroll, useTransform } from "framer-motion";
@@ -8,45 +8,10 @@ import Image from "next/image";
8
8
  import { ActionButton, TrashIconButton, CloseButton, SubmitButton, CancelButton, DeleteButton } from "@/components/other/button";
9
9
  import { Upload, X } from "lucide-react";
10
10
  import { useUser } from "@clerk/nextjs";
11
- import { useStrapiAuth } from "@/lib/auth-context";
11
+ import { useStrapiAuth } from "@/lib/auth/auth-context";
12
+ import { isAdminUser } from "@/lib/auth/auth-utils";
12
13
  import { GALLERY_SIMPLE } from "./constants/gallerySimple";
13
-
14
- interface StrapiUser {
15
- id: number;
16
- username: string;
17
- email: string;
18
- businessAdminId?: string;
19
- }
20
-
21
- interface UploadedImage {
22
- id: number;
23
- documentId: string;
24
- title?: string;
25
- description?: string;
26
- url: string;
27
- createdAt: string;
28
- category?: "none" | "indoor" | "outdoor" | "commercial";
29
- }
30
-
31
- interface GallerySectionProps {
32
- user: StrapiUser | null;
33
- authLoading: boolean;
34
- uploadedImages: UploadedImage[];
35
- setUploadedImages: (images: UploadedImage[]) => void;
36
- error: string | null;
37
- setError: (error: string | null) => void;
38
- isLoading: boolean;
39
- setIsLoading: Dispatch<SetStateAction<boolean>>;
40
- setUser: Dispatch<SetStateAction<StrapiUser | null>>;
41
- handleImageUpload: (
42
- e: React.FormEvent<HTMLFormElement>,
43
- file: File | null,
44
- title: string,
45
- description: string,
46
- category: "none" | "indoor" | "outdoor" | "commercial"
47
- ) => Promise<void>;
48
- handleDeleteImage: (documentId: string) => Promise<void>;
49
- }
14
+ import { GallerySectionProps, UploadedImage, UploadFormState } from "@/lib/types";
50
15
 
51
16
  export function GallerySection({
52
17
  user,
@@ -63,7 +28,7 @@ export function GallerySection({
63
28
  }: GallerySectionProps) {
64
29
  const { isSignedIn } = useUser();
65
30
  const { checkSession } = useStrapiAuth();
66
- const isAdmin = isSignedIn && !!user?.businessAdminId;
31
+ const [isAdmin, setIsAdmin] = useState(false);
67
32
  const [activeTab, setActiveTab] = useState("photos");
68
33
  const [selectedImage, setSelectedImage] = useState<{ url: string } | null>(null);
69
34
  const [isMobile, setIsMobile] = useState(false);
@@ -71,7 +36,17 @@ export function GallerySection({
71
36
  const [isConfirmDeleteOpen, setIsConfirmDeleteOpen] = useState(false);
72
37
  const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
73
38
  const [documentIdToDelete, setDocumentIdToDelete] = useState<string | null>(null);
74
- const [selectedFile, setSelectedFile] = useState<File | null>(null);
39
+ const [uploadForm, setUploadForm] = useState<UploadFormState>({
40
+ file: null,
41
+ title: "",
42
+ description: "",
43
+ category: "none",
44
+ subCategory: "",
45
+ favorite: false,
46
+ banner: false,
47
+ startDate: "",
48
+ endDate: "",
49
+ });
75
50
  const fileInputRef = useRef<HTMLInputElement>(null);
76
51
  const containerRef = useRef<HTMLDivElement>(null);
77
52
  const { scrollYProgress } = useScroll({
@@ -80,6 +55,24 @@ export function GallerySection({
80
55
  });
81
56
  const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -50]);
82
57
 
58
+ useEffect(() => {
59
+ let isMounted = true;
60
+ const checkAdmin = async () => {
61
+ if (!isSignedIn || !user?.authId) {
62
+ if (isMounted) setIsAdmin(false);
63
+ return;
64
+ }
65
+ const adminStatus = await isAdminUser(isSignedIn, user);
66
+ if (isMounted) {
67
+ setIsAdmin(adminStatus);
68
+ }
69
+ };
70
+ checkAdmin();
71
+ return () => {
72
+ isMounted = false;
73
+ };
74
+ }, [isSignedIn, user]);
75
+
83
76
  useEffect(() => {
84
77
  const handleLogin = () => checkSession();
85
78
  const handleLogout = () => checkSession();
@@ -174,7 +167,17 @@ export function GallerySection({
174
167
 
175
168
  const handleCloseUploadModal = () => {
176
169
  setIsUploadModalOpen(false);
177
- setSelectedFile(null);
170
+ setUploadForm({
171
+ file: null,
172
+ title: "",
173
+ description: "",
174
+ category: "none",
175
+ subCategory: "",
176
+ favorite: false,
177
+ banner: false,
178
+ startDate: "",
179
+ endDate: "",
180
+ });
178
181
  if (fileInputRef.current) {
179
182
  fileInputRef.current.value = "";
180
183
  }
@@ -185,25 +188,50 @@ export function GallerySection({
185
188
  if (!isAdmin) {
186
189
  console.error("GallerySimple: Unauthorized upload attempt", {
187
190
  isSignedIn,
191
+ authId: user?.authId,
188
192
  businessAdminId: user?.businessAdminId,
193
+ userRole: user?.userRole,
194
+ businessOwner: user?.businessOwner ?? null,
195
+ timestamp: new Date().toISOString(),
189
196
  });
190
197
  setError(GALLERY_SIMPLE.ERRORS.UNAUTHORIZED_UPLOAD);
191
198
  return;
192
199
  }
193
- if (!selectedFile || selectedFile.size === 0) {
200
+ if (!uploadForm.file || uploadForm.file.size === 0) {
194
201
  setError(GALLERY_SIMPLE.ERRORS.INVALID_FILE);
195
202
  return;
196
203
  }
197
204
  setIsLoading(true);
198
205
  try {
199
- await handleImageUpload(e, selectedFile, "", "", "none");
206
+ await handleImageUpload(
207
+ e,
208
+ uploadForm.file,
209
+ uploadForm.title,
210
+ uploadForm.description,
211
+ uploadForm.category,
212
+ uploadForm.subCategory || "",
213
+ uploadForm.favorite
214
+ );
215
+ setUploadForm({
216
+ file: null,
217
+ title: "",
218
+ description: "",
219
+ category: "none",
220
+ subCategory: "",
221
+ favorite: false,
222
+ banner: false,
223
+ startDate: "",
224
+ endDate: "",
225
+ });
226
+ setIsUploadModalOpen(false);
200
227
  } catch (err) {
201
- console.error("GallerySimple: Upload Error", err);
228
+ console.error("GallerySimple: Upload Error", {
229
+ error: err instanceof Error ? err.message : "Unknown error",
230
+ timestamp: new Date().toISOString(),
231
+ });
202
232
  setError(err instanceof Error ? err.message : GALLERY_SIMPLE.ERRORS.UPLOAD_FAILED);
203
233
  } finally {
204
234
  setIsLoading(false);
205
- setIsUploadModalOpen(false);
206
- setSelectedFile(null);
207
235
  if (fileInputRef.current) {
208
236
  fileInputRef.current.value = "";
209
237
  }
@@ -476,7 +504,7 @@ export function GallerySection({
476
504
  type="file"
477
505
  accept="image/jpeg,image/png,image/gif"
478
506
  onChange={(e) => {
479
- setSelectedFile(e.target.files?.[0] || null);
507
+ setUploadForm({ ...uploadForm, file: e.target.files?.[0] || null });
480
508
  }}
481
509
  disabled={isLoading}
482
510
  className="hidden"
@@ -485,7 +513,7 @@ export function GallerySection({
485
513
  />
486
514
  <label htmlFor="image-upload" className="cursor-pointer">
487
515
  <div className="flex items-center justify-between bg-gray-700 text-white px-4 py-2 rounded-lg">
488
- <span>{selectedFile ? selectedFile.name : GALLERY_SIMPLE.UI.CHOOSE_IMAGE_TEXT}</span>
516
+ <span>{uploadForm.file ? uploadForm.file.name : GALLERY_SIMPLE.UI.CHOOSE_IMAGE_TEXT}</span>
489
517
  <Upload className="h-4 w-4" />
490
518
  </div>
491
519
  </label>
@@ -493,7 +521,7 @@ export function GallerySection({
493
521
  <div className="flex space-x-3">
494
522
  <SubmitButton
495
523
  type="submit"
496
- disabled={isLoading || !selectedFile}
524
+ disabled={isLoading || !uploadForm.file}
497
525
  >
498
526
  {isLoading ? GALLERY_SIMPLE.BUTTONS.UPLOADING_BUTTON : GALLERY_SIMPLE.BUTTONS.UPLOAD_BUTTON}
499
527
  </SubmitButton>
@@ -0,0 +1,260 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef } from "react";
4
+ import Image from "next/image";
5
+ import { UploadedImage } from "@/lib/types";
6
+ import Spinner from "@/components/addOns/non-functional/spinner";
7
+ import { ChevronLeft, ChevronRight } from "lucide-react";
8
+ import { motion } from "framer-motion";
9
+
10
+ interface ThreeSetGalleryProps {
11
+ columns?: "1" | "2" | "3" | "4";
12
+ }
13
+
14
+ const ThreeSetGallery = ({ columns = "3" }: ThreeSetGalleryProps) => {
15
+ const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
16
+ const [isLoading, setIsLoading] = useState(false);
17
+ const [currentSlide, setCurrentSlide] = useState(0);
18
+ const slideshowRef = useRef<HTMLDivElement>(null);
19
+
20
+ useEffect(() => {
21
+ const fetchImages = async () => {
22
+ setIsLoading(true);
23
+ try {
24
+ const response = await fetch(`/api/gallery-data?t=${Date.now()}`, {
25
+ headers: { "Content-Type": "application/json" },
26
+ cache: "no-store",
27
+ });
28
+ if (response.ok) {
29
+ const result = await response.json();
30
+ setUploadedImages(
31
+ result.data.map((image: any) => ({
32
+ id: image.id,
33
+ documentId: image.documentId,
34
+ title: image.title || "Untitled",
35
+ description: image.description || "",
36
+ url: image.url,
37
+ createdAt: image.createdAt,
38
+ category: image.category || "none",
39
+ favorite: image.favorite || false,
40
+ })) || []
41
+ );
42
+ } else {
43
+ console.error("ThreeSetGallery: Gallery API Error", {
44
+ status: response.status,
45
+ statusText: response.statusText,
46
+ });
47
+ }
48
+ } catch (err) {
49
+ console.error("ThreeSetGallery: Fetch Error", err);
50
+ } finally {
51
+ setIsLoading(false);
52
+ }
53
+ };
54
+ fetchImages();
55
+ }, []);
56
+
57
+ // Filter favorite images
58
+ const favoriteImages = uploadedImages.filter((image) => image.favorite === true);
59
+
60
+ const truncateDescription = (description: string | undefined): string => {
61
+ if (!description) return "No description available".slice(0, 250) + "...";
62
+ return description.length > 250
63
+ ? description.slice(0, 250) + "..."
64
+ : description + "...";
65
+ };
66
+
67
+ const goToPrev = () => setCurrentSlide((prev) => (prev - 1 + (favoriteImages.length === 2 ? 1 : favoriteImages.length)) % (favoriteImages.length === 2 ? 1 : favoriteImages.length));
68
+ const goToNext = () => setCurrentSlide((prev) => (prev + 1) % (favoriteImages.length === 2 ? 1 : favoriteImages.length));
69
+
70
+ if (isLoading) {
71
+ return <Spinner />;
72
+ }
73
+
74
+ return (
75
+ <section className="w-full" data-testid="three-set-gallery">
76
+ <div className="w-full px-4 sm:px-6">
77
+ {favoriteImages.length === 0 ? (
78
+ <p className="text-center text-gray-700 font-medium text-base sm:text-lg mb-4">
79
+ No content available
80
+ </p>
81
+ ) : (
82
+ <>
83
+ {/* Desktop: Grid Layout */}
84
+ <div
85
+ className={`hidden md:grid grid-cols-${columns} gap-4 sm:gap-6`}
86
+ data-testid="desktop-grid"
87
+ >
88
+ {favoriteImages.map((image, index) => (
89
+ <div
90
+ key={image.documentId || `image-${index}`}
91
+ className="relative overflow-hidden group w-full aspect-[4/3] rounded-3xl bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl"
92
+ data-testid={`gallery-image-${index}`}
93
+ >
94
+ <Image
95
+ src={image.url}
96
+ alt={
97
+ image.title?.trim() &&
98
+ !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
99
+ ? image.title
100
+ : `Services Image ${index + 1}`
101
+ }
102
+ fill
103
+ className="object-cover rounded-2xl transition-transform duration-700 group-hover:scale-110"
104
+ quality={85}
105
+ priority={index === 0}
106
+ loading={index === 0 ? "eager" : "lazy"}
107
+ sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
108
+ onError={() => console.error(`ThreeSetGallery: Image failed to load: ${image.url}`)}
109
+ />
110
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-black/50 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300 p-4 sm:p-8">
111
+ <h3 className="text-lg sm:text-xl font-bold">
112
+ {image.title?.trim() &&
113
+ !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
114
+ ? image.title
115
+ : `Image ${index + 1}`}
116
+ </h3>
117
+ <p className="text-sm sm:text-base">{truncateDescription(image.description)}</p>
118
+ </div>
119
+ </div>
120
+ ))}
121
+ </div>
122
+
123
+ {/* Mobile: Carousel Layout */}
124
+ <div
125
+ className={`md:hidden relative h-[40vh] min-h-[400px] sm:h-[50vh] rounded-3xl overflow-hidden bg-white/10 backdrop-blur-lg border-2 border-transparent shadow-xl w-full ${favoriteImages.length === 2 ? '' : 'mx-auto max-w-3xl'} supports-[not(backdrop-filter:blur(10px))]:bg-white/20`}
126
+ data-testid="mobile-carousel"
127
+ ref={slideshowRef}
128
+ >
129
+ {favoriteImages.length === 1 ? (
130
+ <div
131
+ className="relative w-full h-full"
132
+ data-testid="carousel-image-0"
133
+ >
134
+ <Image
135
+ src={favoriteImages[0].url}
136
+ alt={
137
+ favoriteImages[0].title?.trim() &&
138
+ !favoriteImages[0].title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
139
+ ? favoriteImages[0].title
140
+ : "Services Image 1"
141
+ }
142
+ fill
143
+ className="object-cover rounded-2xl transition-transform duration-700 hover:scale-110"
144
+ quality={85}
145
+ priority
146
+ sizes="100vw"
147
+ onError={() => console.error(`ThreeSetGallery: Image failed to load: ${favoriteImages[0].url}`)}
148
+ />
149
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-black/50 text-white opacity-0 hover:opacity-100 transition-opacity duration-300 p-4">
150
+ <h3 className="text-lg font-bold">
151
+ {favoriteImages[0].title?.trim() &&
152
+ !favoriteImages[0].title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
153
+ ? favoriteImages[0].title
154
+ : "Image 1"}
155
+ </h3>
156
+ <p className="text-sm">{truncateDescription(favoriteImages[0].description)}</p>
157
+ </div>
158
+ </div>
159
+ ) : favoriteImages.length === 2 ? (
160
+ <div className="relative w-full h-full flex" data-testid="carousel-two-images">
161
+ {favoriteImages.map((image, index) => (
162
+ <div
163
+ key={image.documentId || `image-${index}`}
164
+ className="relative w-1/2 h-full"
165
+ data-testid={`carousel-image-${index}`}
166
+ >
167
+ <Image
168
+ src={image.url}
169
+ alt={
170
+ image.title?.trim() &&
171
+ !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
172
+ ? image.title
173
+ : `Services Image ${index + 1}`
174
+ }
175
+ fill
176
+ className="object-cover rounded-2xl transition-transform duration-700 hover:scale-110"
177
+ quality={85}
178
+ priority={index === 0}
179
+ sizes="50vw"
180
+ onError={() => console.error(`ThreeSetGallery: Image failed to load: ${image.url}`)}
181
+ />
182
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-black/50 text-white opacity-0 hover:opacity-100 transition-opacity duration-300 p-2">
183
+ <h3 className="text-sm font-bold">
184
+ {image.title?.trim() &&
185
+ !image.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
186
+ ? image.title
187
+ : `Image ${index + 1}`}
188
+ </h3>
189
+ <p className="text-xs">{truncateDescription(image.description)}</p>
190
+ </div>
191
+ </div>
192
+ ))}
193
+ </div>
194
+ ) : (
195
+ <>
196
+ <motion.div
197
+ key={currentSlide}
198
+ initial={{ opacity: 0, x: 50 }}
199
+ animate={{ opacity: 1, x: 0 }}
200
+ exit={{ opacity: 0, x: -50 }}
201
+ transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
202
+ className="relative w-full h-full"
203
+ data-testid={`carousel-image-${currentSlide}`}
204
+ >
205
+ <Image
206
+ src={favoriteImages[currentSlide].url}
207
+ alt={
208
+ favoriteImages[currentSlide].title?.trim() &&
209
+ !favoriteImages[currentSlide].title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
210
+ ? favoriteImages[currentSlide].title
211
+ : `Services Image ${currentSlide + 1}`
212
+ }
213
+ fill
214
+ className="object-cover rounded-2xl transition-transform duration-700 hover:scale-110"
215
+ quality={85}
216
+ priority={currentSlide === 0}
217
+ sizes="100vw"
218
+ onError={() => console.error(`ThreeSetGallery: Image failed to load: ${favoriteImages[currentSlide].url}`)}
219
+ />
220
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-black/50 text-white opacity-0 hover:opacity-100 transition-opacity duration-300 p-4">
221
+ <h3 className="text-lg font-bold">
222
+ {favoriteImages[currentSlide].title?.trim() &&
223
+ !favoriteImages[currentSlide].title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
224
+ ? favoriteImages[currentSlide].title
225
+ : `Image ${currentSlide + 1}`}
226
+ </h3>
227
+ <p className="text-sm">{truncateDescription(favoriteImages[currentSlide].description)}</p>
228
+ </div>
229
+ </motion.div>
230
+ <button
231
+ onClick={(e) => {
232
+ e.stopPropagation();
233
+ goToPrev();
234
+ }}
235
+ className="absolute left-2 top-1/2 transform -translate-y-1/2 bg-gray-800/60 hover:bg-gray-800/80 text-white p-2 rounded-full transition-colors duration-300"
236
+ aria-label="Previous slide"
237
+ >
238
+ <ChevronLeft className="h-6 w-6" />
239
+ </button>
240
+ <button
241
+ onClick={(e) => {
242
+ e.stopPropagation();
243
+ goToNext();
244
+ }}
245
+ className="absolute right-2 top-1/2 transform -translate-y-1/2 bg-gray-800/60 hover:bg-gray-800/80 text-white p-2 rounded-full transition-colors duration-300"
246
+ aria-label="Next slide"
247
+ >
248
+ <ChevronRight className="h-6 w-6" />
249
+ </button>
250
+ </>
251
+ )}
252
+ </div>
253
+ </>
254
+ )}
255
+ </div>
256
+ </section>
257
+ );
258
+ };
259
+
260
+ export default ThreeSetGallery;
@@ -1,9 +1,8 @@
1
- // src/pages/ScheduleGridOne.tsx
2
1
  "use client";
3
2
 
4
3
  import React, { useState, useMemo } from "react";
5
4
  import { AddButton, SubmitButton, DeleteIconButton } from "@/components/other/button";
6
- import { ScheduleClass, WeeklySchedule } from "@/components/types";
5
+ import { ScheduleClass, WeeklySchedule } from "@/lib/types";
7
6
  import { ClassPopup } from "../ClassPopup";
8
7
  import { SCHEDULE_GRID_ONE } from "./constants/scheduleGridOne";
9
8
 
@@ -35,7 +34,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
35
34
  ageGroup: string;
36
35
  } | null>(null);
37
36
 
38
- const daysOfWeek = SCHEDULE_GRID_ONE.UI.DAYS_OF_WEEK;
37
+ const daysOfWeek = SCHEDULE_GRID_ONE.UI.DAYS_OF_WEEK as (keyof WeeklySchedule)[];
39
38
 
40
39
  const timeRanges = [
41
40
  SCHEDULE_GRID_ONE.UI.MORNING_OPTION,
@@ -56,9 +55,18 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
56
55
  };
57
56
 
58
57
  const filteredSchedule = useMemo(() => {
59
- const filtered: WeeklySchedule = {};
58
+ const filtered: WeeklySchedule = {
59
+ Monday: [],
60
+ Tuesday: [],
61
+ Wednesday: [],
62
+ Thursday: [],
63
+ Friday: [],
64
+ Saturday: [],
65
+ Sunday: [],
66
+ };
60
67
  Object.entries(schedule).forEach(([day, classes]) => {
61
- filtered[day] = classes.filter((cls) => {
68
+ const dayKey = day as keyof WeeklySchedule;
69
+ filtered[dayKey] = classes.filter((cls: { name: string; startTime: string; }) => {
62
70
  if (searchQuery && !cls.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
63
71
  if (timeFilter) {
64
72
  const startHour = parseTimeTo24Hour(formatTime(cls.startTime));
@@ -78,6 +86,10 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
78
86
  };
79
87
 
80
88
  const handleClassClick = (cls: ScheduleClass, day: string) => {
89
+ if (!cls.documentId || !cls.id) {
90
+ console.error("Missing documentId or id for class:", cls);
91
+ return;
92
+ }
81
93
  if (!isAdmin) {
82
94
  setSelectedClass({
83
95
  id: cls.documentId,
@@ -168,7 +180,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
168
180
  <div className="bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl p-6 shadow-[0_8px_32px_rgba(0,0,0,0.2)]">
169
181
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
170
182
  {daysOfWeek.map((day, index) => {
171
- const classes = filteredSchedule[day] || [];
183
+ const classes: ScheduleClass[] = filteredSchedule[day] || [];
172
184
  return (
173
185
  <div key={day} className="p-4 flex flex-col min-w-0">
174
186
  <h3
@@ -187,7 +199,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
187
199
  </h3>
188
200
  <ul className="space-y-3 font-medium flex-1">
189
201
  {classes.length > 0 ? (
190
- classes.map((cls) => (
202
+ classes.map((cls: ScheduleClass) => (
191
203
  <li
192
204
  key={`${cls.startTime}-${cls.endTime}-${cls.id}`}
193
205
  className={`bg-white/12 backdrop-blur-lg border border-white/20 rounded-lg p-3 shadow-[0_4px_20px_rgba(0,0,0,0.15)] relative cursor-pointer hover:bg-blue-600/20`}
@@ -214,7 +226,9 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
214
226
  <DeleteIconButton
215
227
  onClick={(e) => {
216
228
  e.stopPropagation();
217
- openConfirmDelete(cls.id, cls.documentId, day);
229
+ if (cls.id && cls.documentId) {
230
+ openConfirmDelete(cls.id, cls.documentId, day);
231
+ }
218
232
  }}
219
233
  aria-label={SCHEDULE_GRID_ONE.UI.DELETE_BUTTON_ARIA.replace("${cls.name}", cls.name)}
220
234
  />
@@ -1,9 +1,8 @@
1
- // src/pages/ScheduleGridTwo.tsx
2
1
  "use client";
3
2
 
4
3
  import React, { useState } from "react";
5
4
  import { AddButton, DeleteIconButton } from "@/components/other/button";
6
- import { ScheduleClass, WeeklySchedule } from "@/components/types";
5
+ import { ScheduleClass, WeeklySchedule } from "@/lib/types";
7
6
  import { ClassPopup } from "../ClassPopup";
8
7
  import { SCHEDULE_GRID_TWO } from "./constants/ScheduleGridTwo";
9
8
  import { useUser } from "@clerk/nextjs";
@@ -36,9 +35,13 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
36
35
 
37
36
  const { isSignedIn } = useUser();
38
37
 
39
- const daysOfWeek = SCHEDULE_GRID_TWO.UI.DAYS_OF_WEEK;
38
+ const daysOfWeek = SCHEDULE_GRID_TWO.UI.DAYS_OF_WEEK as (keyof WeeklySchedule)[];
40
39
 
41
40
  const handleClassClick = (cls: ScheduleClass, day: string) => {
41
+ if (!cls.documentId || !cls.id) {
42
+ console.error("Missing documentId or id for class:", cls);
43
+ return;
44
+ }
42
45
  if (!isAdmin) {
43
46
  setSelectedClass({
44
47
  id: cls.documentId,
@@ -112,7 +115,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
112
115
  .schedule-card {
113
116
  background: rgba(255, 255, 255, 0.12);
114
117
  backdrop-filter: blur(10px);
115
- hanging-punctuation: 1px solid rgba(255, 255, 255, 0.2);
118
+ border: 1px solid rgba(255, 255, 255, 0.2);
116
119
  border-radius: 1rem;
117
120
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
118
121
  min-width: 0;
@@ -201,7 +204,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
201
204
  <div className="schedule-container rounded-[2rem] overflow-hidden p-6">
202
205
  <div className="grid grid-cols-1 gap-4">
203
206
  {daysOfWeek.map((day, index) => {
204
- const classes = schedule[day] || [];
207
+ const classes: ScheduleClass[] = schedule[day] || [];
205
208
  return (
206
209
  <div key={day} className="p-4 flex flex-col min-w-0">
207
210
  <h3
@@ -221,7 +224,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
221
224
 
222
225
  <ul className="space-y-3 font-medium flex-1">
223
226
  {classes.length > 0 ? (
224
- classes.map((cls) => (
227
+ classes.map((cls: ScheduleClass) => (
225
228
  <li
226
229
  key={`${cls.startTime}-${cls.endTime}-${cls.id}`}
227
230
  className={`schedule-card p-3 border-none relative ${
@@ -248,7 +251,9 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
248
251
  <DeleteIconButton
249
252
  onClick={(e) => {
250
253
  e.stopPropagation();
251
- openConfirmDelete(cls.id, cls.documentId, day);
254
+ if (cls.id && cls.documentId) {
255
+ openConfirmDelete(cls.id, cls.documentId, day);
256
+ }
252
257
  }}
253
258
  aria-label={SCHEDULE_GRID_TWO.UI.DELETE_BUTTON_ARIA.replace("${cls.name}", cls.name)}
254
259
  />
@@ -1,9 +1,8 @@
1
- // src/pages/ScheduleGridTwoBasic.tsx
2
1
  "use client";
3
2
 
4
3
  import React, { useState } from "react";
5
4
  import { AddButton, DeleteIconButton } from "@/components/other/button";
6
- import { ScheduleClass, WeeklySchedule } from "@/components/types";
5
+ import { ScheduleClass, WeeklySchedule } from "@/lib/types";
7
6
  import { useUser } from "@clerk/nextjs";
8
7
  import { SCHEDULE_GRID_TWO_BASIC } from "./constants/ScheduleGridTwoBasic";
9
8
 
@@ -35,9 +34,13 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
35
34
 
36
35
  const { isSignedIn } = useUser();
37
36
 
38
- const daysOfWeek = SCHEDULE_GRID_TWO_BASIC.UI.DAYS_OF_WEEK;
37
+ const daysOfWeek = SCHEDULE_GRID_TWO_BASIC.UI.DAYS_OF_WEEK as (keyof WeeklySchedule)[];
39
38
 
40
39
  const handleClassClick = (cls: ScheduleClass, day: string) => {
40
+ if (!cls.documentId || !cls.id) {
41
+ console.error("Missing documentId or id for class:", cls);
42
+ return;
43
+ }
41
44
  if (!isAdmin) {
42
45
  setSelectedClass({
43
46
  id: cls.documentId,
@@ -111,7 +114,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
111
114
  .schedule-card {
112
115
  background: rgba(255, 255, 255, 0.12);
113
116
  backdrop-filter: blur(10px);
114
- hanging-punctuation: 1px solid rgba(255, 255, 255, 0.2);
117
+ border: 1px solid rgba(255, 255, 255, 0.2);
115
118
  border-radius: 1rem;
116
119
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
117
120
  min-width: 0;
@@ -204,7 +207,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
204
207
  <div className="schedule-container rounded-[2rem] overflow-hidden p-6">
205
208
  <div className="grid grid-cols-1 gap-4">
206
209
  {daysOfWeek.map((day, index) => {
207
- const classes = schedule[day] || [];
210
+ const classes: ScheduleClass[] = schedule[day] || [];
208
211
  return (
209
212
  <div key={day} className="p-4 flex flex-col min-w-0">
210
213
  <h3
@@ -224,7 +227,7 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
224
227
 
225
228
  <ul className="space-y-3 font-medium flex-1">
226
229
  {classes.length > 0 ? (
227
- classes.map((cls) => (
230
+ classes.map((cls: ScheduleClass) => (
228
231
  <li
229
232
  key={`${cls.startTime}-${cls.endTime}-${cls.id}`}
230
233
  className={`schedule-card p-3 border-none relative ${isAdmin ? "cursor-pointer hover:bg-blue-600/20" : "cursor-default"}`}
@@ -251,7 +254,9 @@ const ScheduleGrid: React.FC<ScheduleGridProps> = ({
251
254
  <DeleteIconButton
252
255
  onClick={(e) => {
253
256
  e.stopPropagation();
254
- openConfirmDelete(cls.id, cls.documentId, day);
257
+ if (cls.id && cls.documentId) {
258
+ openConfirmDelete(cls.id, cls.documentId, day);
259
+ }
255
260
  }}
256
261
  aria-label={SCHEDULE_GRID_TWO_BASIC.UI.DELETE_BUTTON_ARIA.replace("${cls.name}", cls.name)}
257
262
  />