@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
@@ -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, useUser } from "@clerk/nextjs";
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
- authLoading: boolean;
27
+ isSignedIn: boolean | undefined;
32
28
  }
33
29
 
34
- export function AboutSection({ user, authLoading }: AboutSectionProps) {
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
- throw new Error(ABOUT_SECTION.ERRORS.NO_DATA_FOUND);
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 || !user?.businessAdminId) {
167
- console.error("No user or businessAdminId:", {
168
- user,
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
- if (!["image/jpeg", "image/png", "image/gif"].includes(formImage.type)) {
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", formImage);
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
- {imageUrl ? (
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 DevVista Kit"
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
- {title && (
401
- <motion.h2
402
- variants={itemVariants}
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
- {paragraphs
411
- .slice(0, isExpanded ? paragraphs.length : 3)
412
- .map((paragraph, index) => (
413
- <motion.p
414
- key={index}
415
- variants={itemVariants}
416
- className="text-gray-800 text-base sm:text-lg md:text-xl leading-relaxed font-medium"
417
- >
418
- {paragraph}
419
- </motion.p>
420
- ))}
421
- {paragraphs.length > 3 && (
422
- <motion.div variants={itemVariants} className="mt-4">
423
- <ToggleButton
424
- variant="toggle-bio"
425
- onClick={() => setIsExpanded(!isExpanded)}
426
- >
427
- {isExpanded ? ABOUT_SECTION.BUTTONS.READ_LESS_BUTTON : ABOUT_SECTION.BUTTONS.READ_MORE_BUTTON}
428
- </ToggleButton>
429
- </motion.div>
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 max-h-[90vh] overflow-y-auto modal-form"
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 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]"
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: "“DevVista Kit is a modern, well-equipped facility with a friendly and vibrant atmosphere.”",
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: "Update Description",
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
+ }