@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
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
import { motion } from "framer-motion";
|
|
4
4
|
import Image from "next/image";
|
|
5
5
|
import { useState, useEffect, useRef } from "react";
|
|
6
|
-
import { EditIconButton, CloseButton, UpdateButton, CancelButton, ToggleButton } from "@/components/other/button";
|
|
6
|
+
import { EditIconButton, CloseButton, UpdateButton, CancelButton, ToggleButton, TrashIconButton } from "@/components/other/button";
|
|
7
7
|
import Spinner from "@/components/addOns/non-functional/spinner";
|
|
8
8
|
import { X } from "lucide-react";
|
|
9
|
-
import { useAuth
|
|
9
|
+
import { useAuth } from "@clerk/nextjs";
|
|
10
10
|
import { ABOUT_SECTION } from "./constants/aboutSection";
|
|
11
|
+
import { compressImage } from "@/lib/utils/compressImage";
|
|
12
|
+
import { StrapiUser } from "@/lib/types";
|
|
13
|
+
import { isAdminUser } from "@/lib/auth/auth-utils";
|
|
11
14
|
|
|
12
15
|
interface AboutContent {
|
|
13
16
|
id: number;
|
|
@@ -19,21 +22,13 @@ interface AboutContent {
|
|
|
19
22
|
createdAt: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
interface StrapiUser {
|
|
23
|
-
id: number;
|
|
24
|
-
username: string;
|
|
25
|
-
email: string;
|
|
26
|
-
businessAdminId?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
interface AboutSectionProps {
|
|
30
26
|
user: StrapiUser | null;
|
|
31
|
-
|
|
27
|
+
isSignedIn: boolean | undefined;
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
export function AboutSection({ user,
|
|
30
|
+
export function AboutSection({ user, isSignedIn }: AboutSectionProps) {
|
|
35
31
|
const { getToken } = useAuth();
|
|
36
|
-
const { isSignedIn } = useUser();
|
|
37
32
|
const [isMobile, setIsMobile] = useState(false);
|
|
38
33
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
39
34
|
const [formTitle, setFormTitle] = useState<string>("");
|
|
@@ -46,6 +41,7 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
46
41
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
47
42
|
const [error, setError] = useState<string | null>(null);
|
|
48
43
|
const [isLoading, setIsLoading] = useState(true);
|
|
44
|
+
const [isAdmin, setIsAdmin] = useState(false);
|
|
49
45
|
const hasFetched = useRef(false);
|
|
50
46
|
|
|
51
47
|
useEffect(() => {
|
|
@@ -71,7 +67,9 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
71
67
|
setFormTitle(aboutData.title || "");
|
|
72
68
|
setFormDescription(aboutData.description || "");
|
|
73
69
|
} else {
|
|
74
|
-
|
|
70
|
+
setTitle(ABOUT_SECTION.UI.FALLBACK_TITLE);
|
|
71
|
+
setDescription(ABOUT_SECTION.UI.FALLBACK_DESCRIPTION);
|
|
72
|
+
setImageUrl(null);
|
|
75
73
|
}
|
|
76
74
|
} else {
|
|
77
75
|
const errorData = await response.json();
|
|
@@ -80,6 +78,9 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
80
78
|
} catch (err) {
|
|
81
79
|
console.error("Fetch About Error:", err);
|
|
82
80
|
setError(err instanceof Error ? err.message : ABOUT_SECTION.ERRORS.FETCH_ERROR);
|
|
81
|
+
setTitle(ABOUT_SECTION.UI.FALLBACK_TITLE);
|
|
82
|
+
setDescription(ABOUT_SECTION.UI.FALLBACK_DESCRIPTION);
|
|
83
|
+
setImageUrl(null);
|
|
83
84
|
} finally {
|
|
84
85
|
setIsLoading(false);
|
|
85
86
|
}
|
|
@@ -87,6 +88,27 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
87
88
|
fetchAboutData();
|
|
88
89
|
}, []);
|
|
89
90
|
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
let isMounted = true;
|
|
93
|
+
const checkAdmin = async () => {
|
|
94
|
+
if (!isSignedIn || !user?.authId) {
|
|
95
|
+
|
|
96
|
+
if (isMounted) setIsAdmin(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const adminStatus = await isAdminUser(isSignedIn, user);
|
|
101
|
+
if (isMounted) {
|
|
102
|
+
|
|
103
|
+
setIsAdmin(adminStatus);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
checkAdmin();
|
|
107
|
+
return () => {
|
|
108
|
+
isMounted = false;
|
|
109
|
+
};
|
|
110
|
+
}, [isSignedIn, user]);
|
|
111
|
+
|
|
90
112
|
useEffect(() => {
|
|
91
113
|
const debounce = (fn: () => void, delay: number) => {
|
|
92
114
|
let timeout: NodeJS.Timeout;
|
|
@@ -163,10 +185,13 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
163
185
|
onSuccess: () => void
|
|
164
186
|
) => {
|
|
165
187
|
e.preventDefault();
|
|
166
|
-
if (!isSignedIn
|
|
167
|
-
console.error("
|
|
168
|
-
|
|
188
|
+
if (!(await isAdminUser(isSignedIn, user))) {
|
|
189
|
+
console.error("Unauthorized: User is not an admin", {
|
|
190
|
+
isSignedIn,
|
|
191
|
+
authId: user?.authId,
|
|
169
192
|
businessAdminId: user?.businessAdminId,
|
|
193
|
+
userRole: user?.userRole,
|
|
194
|
+
businessOwner: user?.businessOwner,
|
|
170
195
|
});
|
|
171
196
|
setError(ABOUT_SECTION.ERRORS.UNAUTHORIZED_UPDATE);
|
|
172
197
|
return;
|
|
@@ -181,10 +206,11 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
181
206
|
formData.append("description", formDescription);
|
|
182
207
|
}
|
|
183
208
|
if (formImage) {
|
|
184
|
-
|
|
209
|
+
const compressedImage = await compressImage(formImage);
|
|
210
|
+
if (!["image/jpeg", "image/png", "image/gif"].includes(compressedImage.type)) {
|
|
185
211
|
throw new Error(ABOUT_SECTION.ERRORS.INVALID_IMAGE_TYPE);
|
|
186
212
|
}
|
|
187
|
-
formData.append("image",
|
|
213
|
+
formData.append("image", compressedImage);
|
|
188
214
|
}
|
|
189
215
|
|
|
190
216
|
if (!formData.has("title") && !formData.has("description") && !formData.has("image")) {
|
|
@@ -229,8 +255,6 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
229
255
|
}
|
|
230
256
|
};
|
|
231
257
|
|
|
232
|
-
const isAdmin = isSignedIn && !!user?.businessAdminId;
|
|
233
|
-
|
|
234
258
|
const sectionVariants = {
|
|
235
259
|
hidden: { opacity: 0, y: 20 },
|
|
236
260
|
visible: {
|
|
@@ -364,11 +388,15 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
364
388
|
variants={itemVariants}
|
|
365
389
|
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"
|
|
366
390
|
>
|
|
367
|
-
{
|
|
391
|
+
{isLoading ? (
|
|
392
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
393
|
+
<Spinner />
|
|
394
|
+
</div>
|
|
395
|
+
) : imageUrl ? (
|
|
368
396
|
<div className="relative w-full h-full">
|
|
369
397
|
<Image
|
|
370
398
|
src={imageUrl}
|
|
371
|
-
alt="About
|
|
399
|
+
alt="About Milton Supply Co"
|
|
372
400
|
fill
|
|
373
401
|
className="object-cover rounded-[1rem] transition-transform duration-700 hover:scale-110"
|
|
374
402
|
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
@@ -384,6 +412,11 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
384
412
|
openEditModal();
|
|
385
413
|
}}
|
|
386
414
|
/>
|
|
415
|
+
<TrashIconButton
|
|
416
|
+
disabled
|
|
417
|
+
className="disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400"
|
|
418
|
+
title="Cannot delete this image"
|
|
419
|
+
/>
|
|
387
420
|
</div>
|
|
388
421
|
)}
|
|
389
422
|
</div>
|
|
@@ -397,55 +430,59 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
397
430
|
variants={itemVariants}
|
|
398
431
|
className="space-y-6 min-h-[fit-content] h-auto w-full relative"
|
|
399
432
|
>
|
|
400
|
-
{
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
className="text-2xl sm:text-3xl font-bold text-gray-800"
|
|
404
|
-
>
|
|
405
|
-
{title}
|
|
406
|
-
</motion.h2>
|
|
407
|
-
)}
|
|
408
|
-
{paragraphs.length > 0 ? (
|
|
433
|
+
{isLoading ? (
|
|
434
|
+
<Spinner />
|
|
435
|
+
) : (
|
|
409
436
|
<>
|
|
410
|
-
{
|
|
411
|
-
.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
437
|
+
{title && (
|
|
438
|
+
<motion.h2
|
|
439
|
+
variants={itemVariants}
|
|
440
|
+
className="text-2xl sm:text-3xl font-bold text-gray-800"
|
|
441
|
+
>
|
|
442
|
+
{title}
|
|
443
|
+
</motion.h2>
|
|
444
|
+
)}
|
|
445
|
+
{paragraphs.length > 0 ? (
|
|
446
|
+
<>
|
|
447
|
+
{paragraphs
|
|
448
|
+
.slice(0, isExpanded ? paragraphs.length : 3)
|
|
449
|
+
.map((paragraph, index) => (
|
|
450
|
+
<motion.p
|
|
451
|
+
key={index}
|
|
452
|
+
variants={itemVariants}
|
|
453
|
+
className="text-gray-800 text-base sm:text-lg md:text-xl leading-relaxed font-medium"
|
|
454
|
+
>
|
|
455
|
+
{paragraph}
|
|
456
|
+
</motion.p>
|
|
457
|
+
))}
|
|
458
|
+
{paragraphs.length > 3 && (
|
|
459
|
+
<motion.div variants={itemVariants} className="mt-4">
|
|
460
|
+
<ToggleButton
|
|
461
|
+
variant="toggle-bio"
|
|
462
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
463
|
+
>
|
|
464
|
+
{isExpanded ? ABOUT_SECTION.BUTTONS.READ_LESS_BUTTON : ABOUT_SECTION.BUTTONS.READ_MORE_BUTTON}
|
|
465
|
+
</ToggleButton>
|
|
466
|
+
</motion.div>
|
|
467
|
+
)}
|
|
468
|
+
</>
|
|
469
|
+
) : (
|
|
470
|
+
<motion.p
|
|
471
|
+
variants={itemVariants}
|
|
472
|
+
className="text-gray-600 text-base sm:text-lg md:text-xl leading-relaxed font-medium"
|
|
473
|
+
>
|
|
474
|
+
{ABOUT_SECTION.UI.NO_DESCRIPTION_AVAILABLE}
|
|
475
|
+
</motion.p>
|
|
476
|
+
)}
|
|
477
|
+
{error && (
|
|
478
|
+
<p className="text-red-400 text-base sm:text-lg md:text-xl text-center mt-4">{error}</p>
|
|
479
|
+
)}
|
|
480
|
+
{isAdmin && (
|
|
481
|
+
<p className="text-gray-400 text-sm font-medium">
|
|
482
|
+
{ABOUT_SECTION.UI.ADMIN_LOGGED_IN_MESSAGE}
|
|
483
|
+
</p>
|
|
430
484
|
)}
|
|
431
485
|
</>
|
|
432
|
-
) : (
|
|
433
|
-
<motion.p
|
|
434
|
-
variants={itemVariants}
|
|
435
|
-
className="text-gray-600 text-base sm:text-lg md:text-xl leading-relaxed font-medium"
|
|
436
|
-
>
|
|
437
|
-
{ABOUT_SECTION.UI.NO_DESCRIPTION_AVAILABLE}
|
|
438
|
-
</motion.p>
|
|
439
|
-
)}
|
|
440
|
-
{authLoading || isLoading ? (
|
|
441
|
-
<Spinner />
|
|
442
|
-
) : isAdmin ? (
|
|
443
|
-
<p className="text-gray-400 text-sm font-medium">
|
|
444
|
-
{ABOUT_SECTION.UI.ADMIN_LOGGED_IN_MESSAGE}
|
|
445
|
-
</p>
|
|
446
|
-
) : null}
|
|
447
|
-
{error && (
|
|
448
|
-
<p className="text-red-400 text-base sm:text-lg md:text-xl text-center mt-4">{error}</p>
|
|
449
486
|
)}
|
|
450
487
|
</motion.div>
|
|
451
488
|
</div>
|
|
@@ -462,7 +499,7 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
462
499
|
role="dialog"
|
|
463
500
|
>
|
|
464
501
|
<div
|
|
465
|
-
className="relative max-w-5xl w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg
|
|
502
|
+
className="relative max-w-5xl w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg modal-form"
|
|
466
503
|
onClick={(e) => e.stopPropagation()}
|
|
467
504
|
>
|
|
468
505
|
<CloseButton onClick={handleCancelEdit}>
|
|
@@ -523,7 +560,7 @@ export function AboutSection({ user, authLoading }: AboutSectionProps) {
|
|
|
523
560
|
id="aboutDescription"
|
|
524
561
|
value={formDescription}
|
|
525
562
|
onChange={(e) => setFormDescription(e.target.value)}
|
|
526
|
-
className="w-full
|
|
563
|
+
className="modal-form w-full"
|
|
527
564
|
placeholder={ABOUT_SECTION.UI.DESCRIPTION_PLACEHOLDER}
|
|
528
565
|
/>
|
|
529
566
|
</div>
|
|
@@ -5,15 +5,20 @@ export const ABOUT_SECTION = {
|
|
|
5
5
|
// UI Text for AboutSection
|
|
6
6
|
UI: {
|
|
7
7
|
// Blockquote text
|
|
8
|
-
BLOCKQUOTE_TEXT: "“
|
|
8
|
+
BLOCKQUOTE_TEXT: "“Family Owned & Operated Since 1954.”",
|
|
9
9
|
// Placeholder text when no image is available
|
|
10
10
|
NO_IMAGE_AVAILABLE: "No image available",
|
|
11
11
|
// Text when no description is available
|
|
12
12
|
NO_DESCRIPTION_AVAILABLE: "No about description available.",
|
|
13
|
+
// Fallback title when no data is available
|
|
14
|
+
FALLBACK_TITLE: "About Milton Supply Co",
|
|
15
|
+
// Fallback description when no data is available
|
|
16
|
+
FALLBACK_DESCRIPTION:
|
|
17
|
+
"Welcome to Milton Supply Co, your one-stop source for masonry supplies near Syracuse, NY.",
|
|
13
18
|
// Message when logged in as admin
|
|
14
19
|
ADMIN_LOGGED_IN_MESSAGE: "You are logged in as an admin.",
|
|
15
|
-
// Modal heading
|
|
16
|
-
MODAL_HEADING: "Edit About",
|
|
20
|
+
// Modal heading for edit form
|
|
21
|
+
MODAL_HEADING: "Edit About Section",
|
|
17
22
|
// Form label for title input
|
|
18
23
|
TITLE_LABEL: "About Title",
|
|
19
24
|
// Placeholder for title input
|
|
@@ -23,7 +28,7 @@ export const ABOUT_SECTION = {
|
|
|
23
28
|
// Text showing selected image name
|
|
24
29
|
SELECTED_IMAGE_TEXT: "Selected: ${formImage.name}",
|
|
25
30
|
// Form label for description textarea
|
|
26
|
-
DESCRIPTION_LABEL: "
|
|
31
|
+
DESCRIPTION_LABEL: "About Description",
|
|
27
32
|
// Placeholder for description textarea
|
|
28
33
|
DESCRIPTION_PLACEHOLDER: "Enter the about description",
|
|
29
34
|
},
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { motion } from "framer-motion";
|
|
5
|
+
import { X } from "lucide-react";
|
|
6
|
+
import { UploadedImage } from "@/lib/types";
|
|
7
|
+
import Spinner from "@/components/addOns/non-functional/spinner";
|
|
8
|
+
|
|
9
|
+
export default function Banner() {
|
|
10
|
+
const [bannerData, setBannerData] = useState<UploadedImage | null>(null);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
13
|
+
const bannerRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const fetchBanner = async () => {
|
|
17
|
+
setIsLoading(true);
|
|
18
|
+
console.log("Fetching banner from /api/gallery-data...");
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(`/api/gallery-data?t=${Date.now()}`, {
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
cache: "no-store",
|
|
23
|
+
});
|
|
24
|
+
console.log("API Response Status:", response.status);
|
|
25
|
+
if (response.ok) {
|
|
26
|
+
const result = await response.json();
|
|
27
|
+
console.log("API Response Data:", result);
|
|
28
|
+
const images = Array.isArray(result.data)
|
|
29
|
+
? result.data.map((item: any) => ({
|
|
30
|
+
id: Number(item.id) || 0,
|
|
31
|
+
documentId: item.documentId || "",
|
|
32
|
+
title: item.title || "Untitled",
|
|
33
|
+
description: item.description || "",
|
|
34
|
+
url: item.image?.url
|
|
35
|
+
? item.image.url.startsWith("http")
|
|
36
|
+
? item.image.url
|
|
37
|
+
: `${process.env.STRAPI_API_URL}${item.image.url}`
|
|
38
|
+
: "",
|
|
39
|
+
createdAt: item.createdAt || new Date().toISOString(),
|
|
40
|
+
category: item.category || "none",
|
|
41
|
+
subCategory: item.subCategory || "",
|
|
42
|
+
favorite: item.favorite || false,
|
|
43
|
+
banner: item.banner || false,
|
|
44
|
+
startDate: item.startDate || undefined,
|
|
45
|
+
endDate: item.endDate || undefined,
|
|
46
|
+
}))
|
|
47
|
+
: [];
|
|
48
|
+
console.log("Mapped Images:", images);
|
|
49
|
+
|
|
50
|
+
const currentDate = new Date();
|
|
51
|
+
const banner = images.find((image: UploadedImage) => {
|
|
52
|
+
if (!image.banner) return false;
|
|
53
|
+
if (!image.startDate || !image.endDate) return false;
|
|
54
|
+
const start = new Date(image.startDate);
|
|
55
|
+
const end = new Date(image.endDate);
|
|
56
|
+
return start <= currentDate && currentDate <= end;
|
|
57
|
+
});
|
|
58
|
+
console.log("Selected Banner Data:", banner);
|
|
59
|
+
setBannerData(banner || null);
|
|
60
|
+
} else {
|
|
61
|
+
console.error("Banner: Gallery API Error", {
|
|
62
|
+
status: response.status,
|
|
63
|
+
statusText: response.statusText,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error("Banner: Fetch Error", err);
|
|
68
|
+
} finally {
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
fetchBanner();
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
// Handle click outside to close the banner
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
78
|
+
if (
|
|
79
|
+
bannerRef.current &&
|
|
80
|
+
!bannerRef.current.contains(event.target as Node) &&
|
|
81
|
+
isVisible
|
|
82
|
+
) {
|
|
83
|
+
setIsVisible(false);
|
|
84
|
+
console.log("Banner: Closed by clicking outside");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (isVisible) {
|
|
89
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
90
|
+
} else {
|
|
91
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
96
|
+
};
|
|
97
|
+
}, [isVisible]);
|
|
98
|
+
|
|
99
|
+
if (isLoading) {
|
|
100
|
+
console.log("Banner: Showing Spinner");
|
|
101
|
+
return <Spinner />;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!isVisible || !bannerData) {
|
|
105
|
+
console.log("Banner: Not rendering", { isVisible, bannerData });
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log("Banner: Rendering with data", bannerData);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<motion.div
|
|
113
|
+
ref={bannerRef}
|
|
114
|
+
className="fixed top-0 left-0 right-0 bg-blue-500/80 backdrop-blur-sm text-white py-3 sm:py-6 z-[1200] shadow-md"
|
|
115
|
+
style={{
|
|
116
|
+
maskImage: "linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%)",
|
|
117
|
+
WebkitMaskImage: "linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%)",
|
|
118
|
+
}}
|
|
119
|
+
initial={{ y: -100, opacity: 0 }}
|
|
120
|
+
animate={{ y: 0, opacity: 1 }}
|
|
121
|
+
exit={{ y: -100, opacity: 0 }}
|
|
122
|
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
123
|
+
>
|
|
124
|
+
<div className="max-w-7xl mx-auto flex items-center justify-between relative pr-10 sm:pr-14">
|
|
125
|
+
<div className="p-4 sm:p-6">
|
|
126
|
+
<h3 className="text-sm sm:text-base md:text-lg font-bold text-center">
|
|
127
|
+
{bannerData.title?.trim() &&
|
|
128
|
+
!bannerData.title.match(/Image \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
129
|
+
? bannerData.title
|
|
130
|
+
: "Banner"}
|
|
131
|
+
</h3>
|
|
132
|
+
{bannerData.description && (
|
|
133
|
+
<p className="text-xs sm:text-sm text-center mt-2">
|
|
134
|
+
{bannerData.description.length > 100
|
|
135
|
+
? bannerData.description.slice(0, 100) + "..."
|
|
136
|
+
: bannerData.description}
|
|
137
|
+
</p>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
<button
|
|
141
|
+
onClick={() => setIsVisible(false)}
|
|
142
|
+
className="absolute right-2 sm:right-4 bg-blue-600/50 hover:bg-blue-700/70 text-white rounded-full p-1.5 sm:p-2 focus:outline-none focus:ring-2 focus:ring-blue-300 transition-colors"
|
|
143
|
+
aria-label="Close banner"
|
|
144
|
+
>
|
|
145
|
+
<X className="w-5 h-5 sm:w-8 sm:h-8" />
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
</motion.div>
|
|
149
|
+
);
|
|
150
|
+
}
|