@devvistatech/devvista-kit 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/app/ClientLayout.tsx +66 -0
- package/app/about/page.tsx +11 -248
- package/app/adRequest/page.tsx +101 -25
- package/app/admin-profile/page.tsx +123 -0
- package/app/analytics/page.tsx +41 -5
- package/app/api/about/route.ts +2 -18
- package/app/api/adRequest/route.ts +7 -27
- package/app/api/analytics/[reportType]/route.ts +1 -64
- package/app/api/bio/route.ts +1 -17
- package/app/api/blog/route.ts +1 -19
- package/app/api/contacts/route.ts +1 -46
- package/app/api/files/route.ts +1 -15
- package/app/api/gallery-data/route.ts +53 -61
- package/app/api/schedule/route.ts +5 -21
- package/app/api/signup/route.ts +129 -0
- package/app/api/sync-user/route.ts +268 -94
- package/app/api/verify-admin/route.ts +46 -0
- package/app/blog/[id]/page.tsx +71 -52
- package/app/blog/page.tsx +43 -10
- package/app/favicon.ico +0 -0
- package/app/gallery/page.tsx +27 -6
- package/app/layout.tsx +31 -82
- package/app/page.tsx +20 -311
- package/app/products/constants/product.ts +27 -0
- package/app/products/page.tsx +296 -0
- package/app/products/productOne/page.tsx +266 -0
- package/app/products/productTwo/page.tsx +272 -0
- package/app/schedule/page.tsx +78 -40
- package/bin/init.js +0 -12
- package/components/addOns/functional/ClassList.tsx +21 -17
- package/components/addOns/functional/ProductList.tsx +1027 -0
- package/components/addOns/functional/aboutSections/AboutSection.tsx +107 -70
- package/components/addOns/functional/aboutSections/constants/aboutSection.ts +9 -4
- package/components/addOns/functional/banner/Banner.tsx +150 -0
- package/components/addOns/functional/banner/BannerDashboard.tsx +283 -0
- package/components/addOns/functional/bioSections/BioEditor.tsx +471 -0
- package/components/addOns/functional/bioSections/constants/bioEditor.ts +36 -0
- package/components/addOns/functional/blogSections/BlogDashboard.tsx +1 -1
- package/components/addOns/functional/blogSections/BlogFormPopUp.tsx +2 -1
- package/components/addOns/functional/{ImageDescCarousel.tsx → carousels/ImageDescCarousel.tsx} +166 -57
- package/components/addOns/functional/carousels/ProductDescCarousel.tsx +1129 -0
- package/components/addOns/functional/{ScheduleCarousel.tsx → carousels/ScheduleCarousel.tsx} +110 -50
- package/components/addOns/functional/carousels/constants.ts/productDescCarousel.ts +197 -0
- package/components/addOns/functional/carousels/constants.ts/scheduleCarousel.ts +20 -0
- package/components/addOns/functional/contactsDashboard/ContactsDashboard.tsx +1 -1
- package/components/addOns/functional/fileUploaders/FileUploader.tsx +437 -0
- package/components/addOns/functional/fileUploaders/constants/fileUploader.ts +45 -0
- package/components/addOns/functional/galleries/GalleryComplex.tsx +468 -267
- package/components/addOns/functional/galleries/GallerySimple.tsx +78 -50
- package/components/addOns/functional/galleries/ThreeSetGallery.tsx +260 -0
- package/components/addOns/functional/schedules/ScheduleGridOne.tsx +22 -8
- package/components/addOns/functional/schedules/ScheduleGridTwo.tsx +12 -7
- package/components/addOns/functional/schedules/ScheduleGridTwoBasic.tsx +12 -7
- package/components/addOns/non-functional/SampleCarousel.tsx +3 -3
- package/components/addOns/non-functional/ThreeSetGallery.tsx +3 -3
- package/components/addOns/non-functional/featureSections/FeaturesSection.tsx +74 -0
- package/components/addOns/non-functional/featureSections/constants/featuresSection.ts +30 -0
- package/components/addOns/non-functional/{Heros/HeroSection.tsx → heros/HomeHero.tsx} +17 -15
- package/components/addOns/non-functional/heros/ProductHero.tsx +111 -0
- package/components/addOns/non-functional/heros/constants/hero.ts +62 -0
- package/components/addOns/non-functional/imageCarousels/ProductSlider.tsx +6 -6
- package/components/addOns/non-functional/imageCarousels/ProgramCarousel.tsx +10 -10
- package/components/footers/footer.tsx +161 -198
- package/components/other/admin-menu.tsx +1 -1
- package/lib/auth/auth-context.tsx +225 -0
- package/lib/auth/auth-utils.tsx +30 -0
- package/lib/constants/adRequest.ts +199 -56
- package/lib/constants/admin-profile.ts +12 -0
- package/lib/constants/page.ts +15 -15
- package/lib/google/google-analytics-tracking.tsx +44 -0
- package/lib/types.ts +235 -0
- package/lib/utils/compressImage.tsx +32 -0
- package/middleware.ts +9 -5
- package/next.config.js +1 -1
- package/package.json +3 -2
- package/public/images/test.png +0 -0
- package/components/addOns/functional/BioEditor.tsx +0 -447
- package/components/addOns/functional/FileUploader.tsx +0 -295
- package/components/addOns/non-functional/FeaturesSection.tsx +0 -63
- package/components/types.ts +0 -50
- package/lib/auth-context.tsx +0 -131
- package/lib/verify-user.ts +0 -118
- /package/lib/{google-analytics.tsx → google/google-analytics.tsx} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useRef, useEffect
|
|
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 =
|
|
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 [
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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",
|
|
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
|
-
|
|
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>{
|
|
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 || !
|
|
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 "@/
|
|
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
|
-
|
|
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
|
-
|
|
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 "@/
|
|
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
|
-
|
|
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
|
-
|
|
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 "@/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/>
|