@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
|
@@ -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}
|