@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
@@ -0,0 +1,471 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import Image from "next/image";
5
+ import { useState, useEffect, useRef } from "react";
6
+ import { EditIconButton, ToggleButton, UpdateButton, CancelButton, CloseButton, TrashIconButton } from "@/components/other/button";
7
+ import Spinner from "@/components/addOns/non-functional/spinner";
8
+ import { X } from "lucide-react";
9
+ import { useAuth } from "@clerk/nextjs";
10
+ import { BIO_SECTION } from "./constants/bioEditor";
11
+ import { compressImage } from "@/lib/utils/compressImage";
12
+ import { StrapiUser } from "@/lib/types";
13
+ import { isAdminUser } from "@/lib/auth/auth-utils";
14
+
15
+ interface BioContent {
16
+ id: number;
17
+ documentId: string;
18
+ title: string;
19
+ description: string;
20
+ bio: boolean;
21
+ about: boolean;
22
+ image?: { url: string; id: number };
23
+ createdAt: string;
24
+ }
25
+
26
+ interface BioSectionProps {
27
+ user: StrapiUser | null;
28
+ isSignedIn: boolean | undefined;
29
+ }
30
+
31
+ export function BioSection({ user, isSignedIn }: BioSectionProps) {
32
+ const { getToken } = useAuth();
33
+ const [isMobile, setIsMobile] = useState(false);
34
+ const [isEditModalOpen, setIsEditModalOpen] = useState(false);
35
+ const [formTitle, setFormTitle] = useState<string>("");
36
+ const [formDescription, setFormDescription] = useState<string>("");
37
+ const [formImage, setFormImage] = useState<File | null>(null);
38
+ const [isSubmitting, setIsSubmitting] = useState(false);
39
+ const [isExpanded, setIsExpanded] = useState(false);
40
+ const [title, setTitle] = useState<string | null>(null);
41
+ const [description, setDescription] = useState<string | null>(null);
42
+ const [imageUrl, setImageUrl] = useState<string | null>(null);
43
+ const [error, setError] = useState<string | null>(null);
44
+ const [isLoading, setIsLoading] = useState(true);
45
+ const [isAdmin, setIsAdmin] = useState(false);
46
+ const hasFetched = useRef(false);
47
+
48
+ useEffect(() => {
49
+ if (hasFetched.current) return;
50
+ hasFetched.current = true;
51
+
52
+ const fetchBioData = async () => {
53
+ setIsLoading(true);
54
+ try {
55
+ const response = await fetch(`/api/bio?t=${Date.now()}`, {
56
+ headers: { "Content-Type": "application/json" },
57
+ cache: "no-store",
58
+ });
59
+ if (response.ok) {
60
+ const result = await response.json();
61
+ if (result.data) {
62
+ const bioData: BioContent = Array.isArray(result.data) ? result.data[0] : result.data;
63
+ setTitle(bioData.title || null);
64
+ setDescription(bioData.description || null);
65
+ setImageUrl(bioData.image?.url || null);
66
+ setFormTitle(bioData.title || "");
67
+ setFormDescription(bioData.description || "");
68
+ } else {
69
+ setTitle(BIO_SECTION.UI.FALLBACK_TITLE);
70
+ setDescription(BIO_SECTION.UI.FALLBACK_DESCRIPTION);
71
+ setImageUrl(null);
72
+ }
73
+ } else {
74
+ const errorData = await response.json();
75
+ throw new Error(errorData.error || BIO_SECTION.ERRORS.FETCH_FAILED.replace("${response.status}", response.status.toString()));
76
+ }
77
+ } catch (err) {
78
+ console.error("Fetch Bio Error:", err);
79
+ setError(err instanceof Error ? err.message : BIO_SECTION.ERRORS.FETCH_ERROR);
80
+ setTitle(BIO_SECTION.UI.FALLBACK_TITLE);
81
+ setDescription(BIO_SECTION.UI.FALLBACK_DESCRIPTION);
82
+ setImageUrl(null);
83
+ } finally {
84
+ setIsLoading(false);
85
+ }
86
+ };
87
+ fetchBioData();
88
+ }, []);
89
+
90
+ useEffect(() => {
91
+ let isMounted = true;
92
+ const checkAdmin = async () => {
93
+ if (!isSignedIn || !user?.authId) {
94
+ if (isMounted) setIsAdmin(false);
95
+ return;
96
+ }
97
+ const adminStatus = await isAdminUser(isSignedIn, user);
98
+ if (isMounted) setIsAdmin(adminStatus);
99
+ };
100
+ checkAdmin();
101
+ return () => {
102
+ isMounted = false;
103
+ };
104
+ }, [isSignedIn, user]);
105
+
106
+ useEffect(() => {
107
+ const debounce = (fn: () => void, delay: number) => {
108
+ let timeout: NodeJS.Timeout;
109
+ return () => {
110
+ clearTimeout(timeout);
111
+ timeout = setTimeout(fn, delay);
112
+ };
113
+ };
114
+ const checkMobile = debounce(() => {
115
+ setIsMobile(window.innerWidth < 768);
116
+ }, 100);
117
+ checkMobile();
118
+ window.addEventListener("resize", checkMobile);
119
+ return () => window.removeEventListener("resize", checkMobile);
120
+ }, []);
121
+
122
+ useEffect(() => {
123
+ const event = new CustomEvent("modalStateChange", { detail: { isOpen: isEditModalOpen } });
124
+ window.dispatchEvent(event);
125
+ }, [isEditModalOpen]);
126
+
127
+ useEffect(() => {
128
+ if (isEditModalOpen) {
129
+ const scrollY = window.scrollY;
130
+ document.body.style.position = "fixed";
131
+ document.body.style.top = `-${scrollY}px`;
132
+ document.body.style.width = "100%";
133
+ document.body.classList.add("overflow-hidden");
134
+ return () => {
135
+ const scrollYRestored = document.body.style.top ? parseInt(document.body.style.top, 10) * -1 : 0;
136
+ document.body.style.position = "";
137
+ document.body.style.top = "";
138
+ document.body.style.width = "";
139
+ document.body.classList.remove("overflow-hidden");
140
+ window.scrollTo(0, scrollYRestored);
141
+ };
142
+ }
143
+ }, [isEditModalOpen]);
144
+
145
+ useEffect(() => {
146
+ const handleEsc = (e: KeyboardEvent) => {
147
+ if (e.key === "Escape" && isEditModalOpen) {
148
+ handleCancelEdit();
149
+ }
150
+ };
151
+ window.addEventListener("keydown", handleEsc);
152
+ return () => window.removeEventListener("keydown", handleEsc);
153
+ }, [isEditModalOpen]);
154
+
155
+ const openEditModal = () => {
156
+ setFormTitle(title || "");
157
+ setFormDescription(description || "");
158
+ setFormImage(null);
159
+ setIsEditModalOpen(true);
160
+ };
161
+
162
+ const handleCancelEdit = () => {
163
+ setIsEditModalOpen(false);
164
+ setFormTitle(title || "");
165
+ setFormDescription(description || "");
166
+ setFormImage(null);
167
+ };
168
+
169
+ const handlePatchSubmit = async (
170
+ e: React.FormEvent,
171
+ formTitle: string,
172
+ formDescription: string,
173
+ formImage: File | null,
174
+ onSuccess: () => void
175
+ ) => {
176
+ e.preventDefault();
177
+ if (!(await isAdminUser(isSignedIn, user))) {
178
+ console.error("Unauthorized: User is not an admin", {
179
+ isSignedIn,
180
+ authId: user?.authId,
181
+ businessAdminId: user?.businessAdminId,
182
+ userRole: user?.userRole,
183
+ businessOwner: user?.businessOwner,
184
+ });
185
+ setError(BIO_SECTION.ERRORS.UNAUTHORIZED_UPDATE);
186
+ return;
187
+ }
188
+
189
+ if (!formTitle.trim() || !formDescription.trim()) {
190
+ setError(BIO_SECTION.ERRORS.REQUIRED_FIELDS);
191
+ return;
192
+ }
193
+
194
+ setIsSubmitting(true);
195
+ try {
196
+ const formData = new FormData();
197
+ if (formTitle && formTitle !== title) formData.append("title", formTitle);
198
+ if (formDescription && formDescription !== description) formData.append("description", formDescription);
199
+ if (formImage) {
200
+ const compressedImage = await compressImage(formImage);
201
+ if (!["image/jpeg", "image/png", "image/gif"].includes(compressedImage.type)) {
202
+ throw new Error(BIO_SECTION.ERRORS.INVALID_IMAGE_TYPE);
203
+ }
204
+ formData.append("image", compressedImage);
205
+ }
206
+
207
+ if (!formData.has("title") && !formData.has("description") && !formData.has("image")) {
208
+ onSuccess();
209
+ return;
210
+ }
211
+
212
+ const token = await getToken();
213
+ if (!token) throw new Error(BIO_SECTION.ERRORS.NO_AUTH_TOKEN);
214
+
215
+ const response = await fetch("/api/bio", {
216
+ method: "PUT",
217
+ headers: { Authorization: `Bearer ${token}` },
218
+ body: formData,
219
+ });
220
+
221
+ if (!response.ok) {
222
+ const errorData = await response.json();
223
+ throw new Error(errorData.error || BIO_SECTION.ERRORS.UPDATE_FAILED.replace("${response.status}", response.status.toString()));
224
+ }
225
+
226
+ const result = await response.json();
227
+ if (result.data) {
228
+ const bioData: BioContent = Array.isArray(result.data) ? result.data[0] : result.data;
229
+ setImageUrl(bioData.image?.url || null);
230
+ setDescription(bioData.description || null);
231
+ setTitle(bioData.title || null);
232
+ setFormTitle(bioData.title || "");
233
+ setFormDescription(bioData.description || "");
234
+ setError(null);
235
+ onSuccess();
236
+ }
237
+ } catch (err) {
238
+ console.error("Update Error:", err);
239
+ setError(err instanceof Error ? err.message : BIO_SECTION.ERRORS.UPDATE_ERROR);
240
+ } finally {
241
+ setIsSubmitting(false);
242
+ }
243
+ };
244
+
245
+ const sectionVariants = {
246
+ hidden: { opacity: 0, y: 20 },
247
+ visible: { opacity: 1, y: 0, transition: { duration: isMobile ? 0.3 : 0.5, ease: "easeOut" } },
248
+ };
249
+
250
+ const itemVariants = {
251
+ hidden: { opacity: 0, y: 10 },
252
+ visible: { opacity: 1, y: 0, transition: { duration: isMobile ? 0.3 : 0.5, ease: "easeOut" } },
253
+ };
254
+
255
+ const modalVariants = {
256
+ hidden: { opacity: 0, y: "100vh" },
257
+ visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] } },
258
+ exit: { opacity: 0, y: "100vh", transition: { duration: 0.3, ease: "easeIn" } },
259
+ };
260
+
261
+ const paragraphs = description
262
+ ? description.split("\n").filter((paragraph) => paragraph.trim() !== "")
263
+ : [];
264
+
265
+ return (
266
+ <div className="w-full">
267
+ <motion.div
268
+ variants={sectionVariants}
269
+ initial="hidden"
270
+ animate="visible"
271
+ className="mb-24 max-w-[200rem] mx-auto px-8 sm:px-10"
272
+ >
273
+ <div className="flex flex-col md:flow-row gap-10 items-start min-h-[fit-content]">
274
+ <motion.div
275
+ variants={itemVariants}
276
+ 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"
277
+ >
278
+ {isLoading ? (
279
+ <div className="w-reports full h-full flex items-center justify-center">
280
+ <Spinner />
281
+ </div>
282
+ ) : imageUrl ? (
283
+ <div className="relative w-full h-full">
284
+ <Image
285
+ src={imageUrl}
286
+ alt="Portrait of Kathy Caiello"
287
+ fill
288
+ className="object-cover rounded-[1rem] transition-transform duration-700 hover:scale-110"
289
+ sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
290
+ loading="lazy"
291
+ quality={85}
292
+ />
293
+ <div className="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent opacity-0 hover:opacity-100 transition-opacity duration-500"></div>
294
+ {isAdmin && (
295
+ <div className="absolute top-2 right-0 flex">
296
+ <EditIconButton
297
+ onClick={(e) => {
298
+ e.stopPropagation();
299
+ openEditModal();
300
+ }}
301
+ />
302
+ <TrashIconButton
303
+ disabled
304
+ className="disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400"
305
+ title="Cannot delete this image"
306
+ />
307
+ </div>
308
+ )}
309
+ </div>
310
+ ) : (
311
+ <div className="w-full h-full flex items-center justify-center">
312
+ <p className="text-gray-600 text-lg">{BIO_SECTION.UI.NO_IMAGE_AVAILABLE}</p>
313
+ </div>
314
+ )}
315
+ </motion.div>
316
+ <motion.div
317
+ variants={itemVariants}
318
+ className="space-y-6 min-h-[fit-content] h-auto w-full relative"
319
+ >
320
+ {isLoading ? (
321
+ <Spinner />
322
+ ) : (
323
+ <>
324
+ {title && (
325
+ <motion.h2
326
+ variants={itemVariants}
327
+ className="text-2xl sm:text-3xl font-bold text-gray-800"
328
+ >
329
+ {title}
330
+ </motion.h2>
331
+ )}
332
+ {paragraphs.length > 0 ? (
333
+ <>
334
+ {paragraphs
335
+ .slice(0, isExpanded ? paragraphs.length : 3)
336
+ .map((paragraph, index) => (
337
+ <motion.p
338
+ key={index}
339
+ variants={itemVariants}
340
+ className="text-gray-800 text-base sm:text-lg md:text-xl leading-relaxed font-medium"
341
+ >
342
+ {paragraph}
343
+ </motion.p>
344
+ ))}
345
+ {paragraphs.length > 3 && (
346
+ <motion.div variants={itemVariants} className="mt-4">
347
+ <ToggleButton
348
+ variant="toggle-bio"
349
+ onClick={() => setIsExpanded(!isExpanded)}
350
+ >
351
+ {isExpanded ? BIO_SECTION.BUTTONS.READ_LESS_BUTTON : BIO_SECTION.BUTTONS.READ_MORE_BUTTON}
352
+ </ToggleButton>
353
+ </motion.div>
354
+ )}
355
+ </>
356
+ ) : (
357
+ <motion.p
358
+ variants={itemVariants}
359
+ className="text-gray-600 text-base sm:text-lg md:text-xl leading-relaxed font-medium"
360
+ >
361
+ {BIO_SECTION.UI.NO_DESCRIPTION_AVAILABLE}
362
+ </motion.p>
363
+ )}
364
+ {error && (
365
+ <p className="text-red-400 text-base sm:text-lg md:text-xl text-center mt-4">{error}</p>
366
+ )}
367
+ {isAdmin && (
368
+ <p className="text-gray-400 text-sm font-medium">
369
+ {BIO_SECTION.UI.ADMIN_LOGGED_IN_MESSAGE}
370
+ </p>
371
+ )}
372
+ </>
373
+ )}
374
+ </motion.div>
375
+ </div>
376
+ </motion.div>
377
+ {isEditModalOpen && (
378
+ <motion.div
379
+ variants={modalVariants}
380
+ initial="hidden"
381
+ animate="visible"
382
+ exit="exit"
383
+ className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10000] p-4 isolate"
384
+ onClick={handleCancelEdit}
385
+ aria-modal="true"
386
+ role="dialog"
387
+ >
388
+ <div
389
+ 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"
390
+ onClick={(e) => e.stopPropagation()}
391
+ >
392
+ <CloseButton onClick={handleCancelEdit}>
393
+ <X className="h-6 w-6 sm:h-8 sm:w-8" />
394
+ </CloseButton>
395
+ <h3 className="text-xl font-bold text-white mb-4">{BIO_SECTION.UI.MODAL_HEADING}</h3>
396
+ <form
397
+ onSubmit={(e) => handlePatchSubmit(e, formTitle, formDescription, formImage, () => setIsEditModalOpen(false))}
398
+ className="space-y-4 modal-form"
399
+ >
400
+ <div>
401
+ <label htmlFor="bioTitle" className="block text-sm font-medium text-gray-300 mb-1">
402
+ {BIO_SECTION.UI.TITLE_LABEL}
403
+ </label>
404
+ <input
405
+ id="bioTitle"
406
+ type="text"
407
+ value={formTitle}
408
+ onChange={(e) => setFormTitle(e.target.value)}
409
+ 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"
410
+ placeholder={BIO_SECTION.UI.TITLE_PLACEHOLDER}
411
+ aria-label={BIO_SECTION.UI.TITLE_LABEL}
412
+ />
413
+ </div>
414
+ <div>
415
+ <label htmlFor="bioImage" className="block text-sm font-medium text-gray-300 mb-1">
416
+ {BIO_SECTION.UI.IMAGE_LABEL}
417
+ </label>
418
+ <input
419
+ id="bioImage"
420
+ type="file"
421
+ accept="image/jpeg,image/png,image/gif"
422
+ onChange={(e) => setFormImage(e.target.files?.[0] || null)}
423
+ 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"
424
+ aria-label={BIO_SECTION.UI.IMAGE_LABEL}
425
+ />
426
+ {formImage && (
427
+ <div className="mttv-2 mt-2">
428
+ <p className="text-gray-300 text-sm">{BIO_SECTION.UI.SELECTED_IMAGE_TEXT.replace("${formImage.name}", formImage.name)}</p>
429
+ <img
430
+ src={URL.createObjectURL(formImage)}
431
+ alt="Preview"
432
+ className="mt-2 max-w-xs rounded"
433
+ />
434
+ </div>
435
+ )}
436
+ </div>
437
+ <div>
438
+ <label htmlFor="bioDescription" className="block text-sm font-medium text-gray-300 mb-1">
439
+ {BIO_SECTION.UI.DESCRIPTION_LABEL}
440
+ </label>
441
+ <textarea
442
+ id="bioDescription"
443
+ value={formDescription}
444
+ onChange={(e) => setFormDescription(e.target.value)}
445
+ 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]"
446
+ placeholder={BIO_SECTION.UI.DESCRIPTION_PLACEHOLDER}
447
+ aria-label={BIO_SECTION.UI.DESCRIPTION_LABEL}
448
+ />
449
+ </div>
450
+ <div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-3">
451
+ <UpdateButton type="submit" disabled={isSubmitting}>
452
+ {isSubmitting ? (
453
+ <span className="flex items-center">
454
+ <Spinner /> {BIO_SECTION.BUTTONS.SAVING_BUTTON}
455
+ </span>
456
+ ) : (
457
+ BIO_SECTION.BUTTONS.UPDATE_BUTTON
458
+ )}
459
+ </UpdateButton>
460
+ <CancelButton type="button" onClick={handleCancelEdit}>
461
+ {BIO_SECTION.BUTTONS.CANCEL_BUTTON}
462
+ </CancelButton>
463
+ </div>
464
+ {error && <p className="text-red-400 text-sm font-medium">{error}</p>}
465
+ </form>
466
+ </div>
467
+ </motion.div>
468
+ )}
469
+ </div>
470
+ );
471
+ }
@@ -0,0 +1,36 @@
1
+ export const BIO_SECTION = {
2
+ UI: {
3
+ BLOCKQUOTE_TEXT: "“Family Owned & Operated Since 1954.”",
4
+ NO_IMAGE_AVAILABLE: "No image available",
5
+ NO_DESCRIPTION_AVAILABLE: "No bio description available.",
6
+ FALLBACK_TITLE: "About Kathy Caiello",
7
+ FALLBACK_DESCRIPTION: "Learn more about Kathy Caiello, a key member of Milton Supply Co.",
8
+ ADMIN_LOGGED_IN_MESSAGE: "You are logged in as an admin.",
9
+ MODAL_HEADING: "Edit Bio Section",
10
+ TITLE_LABEL: "Bio Title",
11
+ TITLE_PLACEHOLDER: "Enter the bio title",
12
+ IMAGE_LABEL: "Update Image (optional)",
13
+ SELECTED_IMAGE_TEXT: "Selected: ${formImage.name}",
14
+ DESCRIPTION_LABEL: "Bio Description",
15
+ DESCRIPTION_PLACEHOLDER: "Enter the bio description",
16
+ },
17
+ BUTTONS: {
18
+ EDIT_BUTTON: "Edit",
19
+ READ_MORE_BUTTON: "Read More",
20
+ READ_LESS_BUTTON: "Read Less",
21
+ UPDATE_BUTTON: "Update",
22
+ SAVING_BUTTON: "Saving...",
23
+ CANCEL_BUTTON: "Cancel",
24
+ },
25
+ ERRORS: {
26
+ NO_DATA_FOUND: "Bio data not found",
27
+ FETCH_FAILED: "Failed to fetch bio data: ${response.status}",
28
+ FETCH_ERROR: "An error occurred while fetching bio data",
29
+ UNAUTHORIZED_UPDATE: "Unauthorized: Only admin can update bio data",
30
+ INVALID_IMAGE_TYPE: "Only JPEG, PNG, or GIF images are allowed",
31
+ NO_AUTH_TOKEN: "No authentication token available",
32
+ UPDATE_FAILED: "Failed to update bio data: ${response.status}",
33
+ UPDATE_ERROR: "Failed to update bio data",
34
+ REQUIRED_FIELDS: "Title and description are required.",
35
+ },
36
+ };
@@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com
7
7
  import { Trash2, Edit, Heart, Send } from "lucide-react";
8
8
  import { Separator } from "@/components/other/separator";
9
9
  import { useAuth, useUser } from "@clerk/nextjs";
10
- import { useStrapiAuth } from "@/lib/auth-context";
10
+ import { useStrapiAuth } from "@/lib/auth/auth-context";
11
11
  import { BLOG_DASHBOARD } from "./constants/blogDashboard";
12
12
 
13
13
  interface BlogPost {
@@ -7,7 +7,7 @@ import { Input } from '@/components/other/input';
7
7
  import { Label } from '@/components/other/label';
8
8
  import { X, ChevronLeft, ChevronRight } from 'lucide-react';
9
9
  import { useAuth, useUser } from '@clerk/nextjs';
10
- import { useStrapiAuth } from '@/lib/auth-context';
10
+ import { useStrapiAuth } from '@/lib/auth/auth-context';
11
11
  import { useChat } from 'ai/react';
12
12
 
13
13
  interface BlogPost {
@@ -418,6 +418,7 @@ const BlogFormPopUp: React.FC<BlogFormPopUpProps> = ({
418
418
  </div>
419
419
  <div>
420
420
  <ToggleButton
421
+ type="button"
421
422
  variant="toggle-keywords"
422
423
  onClick={() => setShowKeywords(!showKeywords)}
423
424
  aria-expanded={showKeywords}