@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
package/app/page.tsx
CHANGED
|
@@ -1,289 +1,33 @@
|
|
|
1
|
+
// src/app/page.tsx
|
|
1
2
|
"use client";
|
|
2
3
|
|
|
3
|
-
import { useState, useEffect
|
|
4
|
-
import {
|
|
4
|
+
import { useState, useEffect } from "react";
|
|
5
|
+
import { HomeHero } from "@/components/addOns/non-functional/heros/HomeHero";
|
|
5
6
|
import { AboutSection } from "@/components/addOns/functional/aboutSections/AboutSection";
|
|
6
|
-
import { ScheduleCarousel } from "@/components/addOns/functional/ScheduleCarousel";
|
|
7
|
-
import { FileUploader } from "@/components/addOns/functional/FileUploader";
|
|
8
|
-
import { FeaturesSection } from "@/components/addOns/non-functional/FeaturesSection";
|
|
7
|
+
import { ScheduleCarousel } from "@/components/addOns/functional/carousels/ScheduleCarousel";
|
|
8
|
+
import { FileUploader } from "@/components/addOns/functional/fileUploaders/FileUploader";
|
|
9
9
|
import BlogList from "@/components/addOns/functional/blogSections/BlogList";
|
|
10
|
+
import { FeaturesSection } from "@/components/addOns/non-functional/featureSections/FeaturesSection";
|
|
10
11
|
import Spinner from "@/components/addOns/non-functional/spinner";
|
|
11
|
-
import {
|
|
12
|
-
import { useStrapiAuth } from "@/lib/auth-context";
|
|
13
|
-
import { HOME_PAGE } from "
|
|
14
|
-
|
|
15
|
-
interface ScheduleClass {
|
|
16
|
-
name: string;
|
|
17
|
-
startTime: string;
|
|
18
|
-
endTime: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface WeeklySchedule {
|
|
22
|
-
[key: string]: ScheduleClass[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface UploadedFile {
|
|
26
|
-
id: number;
|
|
27
|
-
documentId: string;
|
|
28
|
-
name: string;
|
|
29
|
-
url: string;
|
|
30
|
-
createdAt: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface StrapiUser {
|
|
34
|
-
id: number;
|
|
35
|
-
username: string;
|
|
36
|
-
email: string;
|
|
37
|
-
businessAdminId?: string;
|
|
38
|
-
documentId?: string;
|
|
39
|
-
}
|
|
12
|
+
import { useUser } from "@clerk/nextjs";
|
|
13
|
+
import { useStrapiAuth } from "@/lib/auth/auth-context";
|
|
14
|
+
import { HOME_PAGE } from "@/lib/constants/page";
|
|
15
|
+
import ThreeSetGallery from "@/components/addOns/functional/galleries/ThreeSetGallery";
|
|
40
16
|
|
|
41
17
|
export default function Home() {
|
|
42
|
-
const [pdfUrl, setPdfUrl] = useState<string>("");
|
|
43
|
-
const [weeklySchedule, setWeeklySchedule] = useState<WeeklySchedule>({
|
|
44
|
-
Monday: [], Tuesday: [], Wednesday: [], Thursday: [], Friday: [], Saturday: [], Sunday: [],
|
|
45
|
-
});
|
|
46
18
|
const [isLoading, setIsLoading] = useState(false);
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const hasFetched = useRef(false);
|
|
50
|
-
|
|
51
|
-
const { getToken } = useAuth();
|
|
52
|
-
const { user: clerkUser, isSignedIn } = useUser();
|
|
53
|
-
const { user, authLoading } = useStrapiAuth();
|
|
54
|
-
|
|
55
|
-
const numberToDayKey: { [key: number]: string } = {
|
|
56
|
-
1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday", 7: "Sunday",
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const backgroundImages = [
|
|
60
|
-
HOME_PAGE.UI.BACKGROUND_IMAGE_1,
|
|
61
|
-
HOME_PAGE.UI.BACKGROUND_IMAGE_2,
|
|
62
|
-
HOME_PAGE.UI.BACKGROUND_IMAGE_3,
|
|
63
|
-
HOME_PAGE.UI.BACKGROUND_IMAGE_4,
|
|
64
|
-
HOME_PAGE.UI.BACKGROUND_IMAGE_5,
|
|
65
|
-
HOME_PAGE.UI.BACKGROUND_IMAGE_6,
|
|
66
|
-
HOME_PAGE.UI.BACKGROUND_IMAGE_7,
|
|
67
|
-
];
|
|
19
|
+
const { user } = useStrapiAuth();
|
|
20
|
+
const { isSignedIn } = useUser();
|
|
68
21
|
|
|
69
22
|
useEffect(() => {
|
|
70
23
|
window.scrollTo(0, 0);
|
|
71
24
|
}, []);
|
|
72
25
|
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (hasFetched.current) return;
|
|
75
|
-
hasFetched.current = true;
|
|
76
|
-
|
|
77
|
-
const fetchInitialData = async () => {
|
|
78
|
-
setIsLoading(true);
|
|
79
|
-
try {
|
|
80
|
-
let updatedSchedule: WeeklySchedule = {
|
|
81
|
-
Monday: [], Tuesday: [], Wednesday: [], Thursday: [], Friday: [], Saturday: [], Sunday: [],
|
|
82
|
-
};
|
|
83
|
-
const scheduleResponse = await fetch(`/api/schedule?t=${Date.now()}`, {
|
|
84
|
-
headers: { "Content-Type": "application/json" },
|
|
85
|
-
cache: "no-store",
|
|
86
|
-
});
|
|
87
|
-
if (scheduleResponse.ok) {
|
|
88
|
-
const scheduleResult = await scheduleResponse.json();
|
|
89
|
-
const fetchedClasses = (scheduleResult.data || []).map((event: any) => ({
|
|
90
|
-
name: event.title || "Untitled",
|
|
91
|
-
startTime: event.startTime || "00:00",
|
|
92
|
-
endTime: event.endTime || "00:00",
|
|
93
|
-
daysOfWeek: event.daysOfWeek || [],
|
|
94
|
-
}));
|
|
95
|
-
fetchedClasses.forEach((cls: any) => {
|
|
96
|
-
cls.daysOfWeek.forEach((dayNum: number) => {
|
|
97
|
-
const dayKey = numberToDayKey[dayNum];
|
|
98
|
-
if (dayKey && updatedSchedule[dayKey]) {
|
|
99
|
-
updatedSchedule[dayKey].push({
|
|
100
|
-
name: cls.name,
|
|
101
|
-
startTime: cls.startTime,
|
|
102
|
-
endTime: cls.endTime,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
} else {
|
|
108
|
-
setError(HOME_PAGE.ERRORS.FETCH_SCHEDULE_FAILED);
|
|
109
|
-
}
|
|
110
|
-
setWeeklySchedule(updatedSchedule);
|
|
111
|
-
|
|
112
|
-
const filesResponse = await fetch(`/api/files?t=${Date.now()}`, {
|
|
113
|
-
headers: { "Content-Type": "application/json" },
|
|
114
|
-
cache: "no-store",
|
|
115
|
-
});
|
|
116
|
-
if (filesResponse.ok) {
|
|
117
|
-
const filesResult = await filesResponse.json();
|
|
118
|
-
if (filesResult.meta?.pagination?.total > 100) {
|
|
119
|
-
setError(HOME_PAGE.ERRORS.PAGINATION_WARNING);
|
|
120
|
-
}
|
|
121
|
-
setUploadedFiles(
|
|
122
|
-
filesResult.data.map((file: any) => ({
|
|
123
|
-
id: file.id,
|
|
124
|
-
documentId: file.documentId,
|
|
125
|
-
name: file.name || "Untitled",
|
|
126
|
-
url: file.url,
|
|
127
|
-
createdAt: file.createdAt,
|
|
128
|
-
})) || []
|
|
129
|
-
);
|
|
130
|
-
} else {
|
|
131
|
-
setError(HOME_PAGE.ERRORS.FETCH_FILES_FAILED);
|
|
132
|
-
}
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error("Fetch Error:", err);
|
|
135
|
-
setError(err instanceof Error ? err.message : HOME_PAGE.ERRORS.FETCH_ERROR);
|
|
136
|
-
} finally {
|
|
137
|
-
setIsLoading(false);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
fetchInitialData();
|
|
141
|
-
}, []);
|
|
142
|
-
|
|
143
|
-
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
144
|
-
if (!isSignedIn || !user?.businessAdminId) {
|
|
145
|
-
setError(HOME_PAGE.ERRORS.UNAUTHORIZED_UPLOAD);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const file = e.target.files?.[0];
|
|
149
|
-
if (!file) {
|
|
150
|
-
setError(HOME_PAGE.ERRORS.NO_FILE_SELECTED);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (file.type !== "application/pdf") {
|
|
154
|
-
setError(HOME_PAGE.ERRORS.INVALID_FILE_TYPE);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
setError(null);
|
|
158
|
-
try {
|
|
159
|
-
const token = await getToken();
|
|
160
|
-
if (!token) {
|
|
161
|
-
throw new Error(HOME_PAGE.ERRORS.NO_AUTH_TOKEN);
|
|
162
|
-
}
|
|
163
|
-
const formData = new FormData();
|
|
164
|
-
formData.append("files", file);
|
|
165
|
-
|
|
166
|
-
const response = await fetch("/api/files", {
|
|
167
|
-
method: "POST",
|
|
168
|
-
headers: {
|
|
169
|
-
Authorization: `Bearer ${token}`,
|
|
170
|
-
},
|
|
171
|
-
body: formData,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (!response.ok) {
|
|
175
|
-
const errorData = await response.json();
|
|
176
|
-
throw new Error(errorData.error || HOME_PAGE.ERRORS.UPLOAD_FILE_FAILED.replace("${response.status}", response.status.toString()));
|
|
177
|
-
}
|
|
178
|
-
const filesResult = await response.json();
|
|
179
|
-
setUploadedFiles(
|
|
180
|
-
filesResult.data.map((file: any) => ({
|
|
181
|
-
id: file.id,
|
|
182
|
-
documentId: file.documentId,
|
|
183
|
-
name: file.name || "Unnamed File",
|
|
184
|
-
url: file.url,
|
|
185
|
-
createdAt: file.createdAt,
|
|
186
|
-
})) || []
|
|
187
|
-
);
|
|
188
|
-
setPdfUrl(filesResult.newFileUrl || "");
|
|
189
|
-
} catch (err) {
|
|
190
|
-
console.error("Upload Error:", err);
|
|
191
|
-
setError(err instanceof Error ? err.message : HOME_PAGE.ERRORS.UPLOAD_FILE_ERROR);
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const handleDeleteFile = async (documentId: string) => {
|
|
196
|
-
if (!isSignedIn || !user?.businessAdminId) {
|
|
197
|
-
setError(HOME_PAGE.ERRORS.UNAUTHORIZED_DELETE);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
setError(null);
|
|
201
|
-
try {
|
|
202
|
-
const token = await getToken();
|
|
203
|
-
if (!token) {
|
|
204
|
-
throw new Error(HOME_PAGE.ERRORS.NO_AUTH_TOKEN);
|
|
205
|
-
}
|
|
206
|
-
const response = await fetch("/api/files", {
|
|
207
|
-
method: "DELETE",
|
|
208
|
-
headers: {
|
|
209
|
-
"Content-Type": "application/json",
|
|
210
|
-
Authorization: `Bearer ${token}`,
|
|
211
|
-
},
|
|
212
|
-
body: JSON.stringify({ documentId }),
|
|
213
|
-
});
|
|
214
|
-
if (!response.ok) {
|
|
215
|
-
const errorData = await response.json();
|
|
216
|
-
throw new Error(errorData.error || HOME_PAGE.ERRORS.DELETE_FILE_FAILED.replace("${response.status}", response.status.toString()));
|
|
217
|
-
}
|
|
218
|
-
const filesResult = await response.json();
|
|
219
|
-
setUploadedFiles(
|
|
220
|
-
filesResult.data.map((file: any) => ({
|
|
221
|
-
id: file.id,
|
|
222
|
-
documentId: file.documentId,
|
|
223
|
-
name: file.name || "Unnamed File",
|
|
224
|
-
url: file.url,
|
|
225
|
-
createdAt: file.createdAt,
|
|
226
|
-
})) || []
|
|
227
|
-
);
|
|
228
|
-
if (pdfUrl === uploadedFiles.find((f) => f.documentId === documentId)?.url) {
|
|
229
|
-
setPdfUrl("");
|
|
230
|
-
}
|
|
231
|
-
} catch (err) {
|
|
232
|
-
console.error("Delete Error:", err);
|
|
233
|
-
setError(err instanceof Error ? err.message : HOME_PAGE.ERRORS.DELETE_FILE_ERROR);
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const isAdmin = isSignedIn && !!user?.businessAdminId || false;
|
|
238
|
-
|
|
239
|
-
if (authLoading || isLoading) {
|
|
240
|
-
return <Spinner />;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
26
|
return (
|
|
244
|
-
<div className="w-full min-h-screen bg-
|
|
245
|
-
<
|
|
246
|
-
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap");
|
|
247
|
-
|
|
248
|
-
:root {
|
|
249
|
-
--jubilee: #F47C7C;
|
|
250
|
-
--natural-white: #FFFFFF;
|
|
251
|
-
--caviar: #2D2D2D;
|
|
252
|
-
--storm-cloud: #4A636E;
|
|
253
|
-
--exuberant-blue: #FF69B4;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
* {
|
|
257
|
-
font-family: "Inter", sans-serif;
|
|
258
|
-
font-weight: 400;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
.gradient-text {
|
|
262
|
-
background: linear-gradient(90deg, var(--exuberant-blue), var(--jubilee));
|
|
263
|
-
-webkit-background-clip: text;
|
|
264
|
-
background-clip: text;
|
|
265
|
-
color: transparent;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
body.overflow-hidden {
|
|
269
|
-
overflow: hidden !important;
|
|
270
|
-
-webkit-overflow-scrolling: none;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
section {
|
|
274
|
-
position: relative;
|
|
275
|
-
z-index: 1;
|
|
276
|
-
}
|
|
277
|
-
`}</style>
|
|
278
|
-
|
|
279
|
-
<div className="hidden">
|
|
280
|
-
{backgroundImages.map((src, index) => (
|
|
281
|
-
<img key={index} src={src} alt="" />
|
|
282
|
-
))}
|
|
283
|
-
</div>
|
|
284
|
-
|
|
27
|
+
<div className="w-full min-h-screen bg-white/70 overflow-hidden font-inter">
|
|
28
|
+
{isLoading && <Spinner />}
|
|
285
29
|
<div className="relative w-full min-h-screen flex flex-col">
|
|
286
|
-
<
|
|
30
|
+
<HomeHero
|
|
287
31
|
title={HOME_PAGE.UI.HERO_TITLE}
|
|
288
32
|
subtitle={HOME_PAGE.UI.HERO_SUBTITLE}
|
|
289
33
|
ctaText={HOME_PAGE.BUTTONS.HERO_CTA_TEXT}
|
|
@@ -292,47 +36,12 @@ export default function Home() {
|
|
|
292
36
|
posterSrc=""
|
|
293
37
|
videoAriaLabel={HOME_PAGE.UI.HERO_VIDEO_ARIA_LABEL}
|
|
294
38
|
/>
|
|
295
|
-
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
authLoading={authLoading}
|
|
299
|
-
/>
|
|
300
|
-
|
|
301
|
-
<ScheduleCarousel
|
|
302
|
-
weeklySchedule={weeklySchedule}
|
|
303
|
-
backgroundImages={backgroundImages}
|
|
304
|
-
error={error}
|
|
305
|
-
/>
|
|
306
|
-
|
|
39
|
+
<AboutSection user={user} isSignedIn={isSignedIn} />
|
|
40
|
+
<ThreeSetGallery/>
|
|
41
|
+
<ScheduleCarousel />
|
|
307
42
|
<BlogList />
|
|
308
|
-
|
|
309
|
-
<
|
|
310
|
-
<FileUploader
|
|
311
|
-
uploadedFiles={uploadedFiles}
|
|
312
|
-
setUploadedFiles={setUploadedFiles}
|
|
313
|
-
setPdfUrl={setPdfUrl}
|
|
314
|
-
pdfUrl={pdfUrl}
|
|
315
|
-
isAdmin={isAdmin}
|
|
316
|
-
error={error}
|
|
317
|
-
setError={setError}
|
|
318
|
-
handleFileUpload={handleFileUpload}
|
|
319
|
-
handleDeleteFile={handleDeleteFile}
|
|
320
|
-
/>
|
|
321
|
-
</section>
|
|
322
|
-
|
|
323
|
-
<FeaturesSection
|
|
324
|
-
features={[
|
|
325
|
-
{ text: HOME_PAGE.UI.FEATURE_1 },
|
|
326
|
-
{ text: HOME_PAGE.UI.FEATURE_2 },
|
|
327
|
-
{ text: HOME_PAGE.UI.FEATURE_3 },
|
|
328
|
-
{ text: HOME_PAGE.UI.FEATURE_4 },
|
|
329
|
-
{ text: HOME_PAGE.UI.FEATURE_5 },
|
|
330
|
-
]}
|
|
331
|
-
title={HOME_PAGE.UI.FEATURES_TITLE}
|
|
332
|
-
description={HOME_PAGE.UI.FEATURES_DESCRIPTION}
|
|
333
|
-
ctaText={HOME_PAGE.BUTTONS.FEATURES_CTA_TEXT}
|
|
334
|
-
ctaLink="/schedule"
|
|
335
|
-
/>
|
|
43
|
+
<FileUploader user={user} isSignedIn={isSignedIn} />
|
|
44
|
+
<FeaturesSection />
|
|
336
45
|
</div>
|
|
337
46
|
</div>
|
|
338
47
|
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// app/products/constants/productOne.ts
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
export const PRODUCT_CATEGORIES: string[] = [
|
|
5
|
+
"indoor",
|
|
6
|
+
"outdoor",
|
|
7
|
+
"commercial",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const PRODUCT_PAGE = {
|
|
11
|
+
ERRORS: {
|
|
12
|
+
NO_AUTH_TOKEN: "No authentication token found.",
|
|
13
|
+
FETCH_IMAGES_FAILED: "Failed to fetch images: ${errorText}",
|
|
14
|
+
FETCH_ERROR: "An error occurred while fetching images.",
|
|
15
|
+
NO_FILE_SELECTED: "No file selected for upload.",
|
|
16
|
+
INVALID_FILE_TYPE: "Invalid file type. Please upload a JPEG, PNG, or GIF image.",
|
|
17
|
+
UNAUTHORIZED_UPLOAD: "You are not authorized to upload images.",
|
|
18
|
+
AUTHENTICATION_ERROR: "Authentication error. Please log in again.",
|
|
19
|
+
UPLOAD_IMAGE_FAILED: "Failed to upload image: ${response.status}",
|
|
20
|
+
UPLOAD_IMAGE_ERROR: "An error occurred while uploading the image.",
|
|
21
|
+
UNAUTHORIZED_DELETE: "You are not authorized to delete images.",
|
|
22
|
+
DELETE_IMAGE_FAILED: "Failed to delete image: ${response.status}",
|
|
23
|
+
DELETE_IMAGE_ERROR: "An error occurred while deleting the image.",
|
|
24
|
+
PAGINATION_WARNING: "Too many images to display. Only the first 100 images are shown.",
|
|
25
|
+
},
|
|
26
|
+
DEFAULT_CATEGORY: "bagged-cement-products",
|
|
27
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { motion } from "framer-motion";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import ProductHero from "@/components/addOns/non-functional/heros/ProductHero";
|
|
8
|
+
import Spinner from "@/components/addOns/non-functional/spinner";
|
|
9
|
+
|
|
10
|
+
interface ProductCategory {
|
|
11
|
+
title: string;
|
|
12
|
+
description: string;
|
|
13
|
+
image: string;
|
|
14
|
+
link: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Products() {
|
|
18
|
+
const sectionBackgroundRef = useRef<HTMLDivElement | null>(null);
|
|
19
|
+
const [activeTab, setActiveTab] = useState<string>("All");
|
|
20
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
21
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
window.scrollTo(0, 0);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const debounce = (fn: () => void, delay: number) => {
|
|
29
|
+
let timeout: NodeJS.Timeout;
|
|
30
|
+
return () => {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
timeout = setTimeout(fn, delay);
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const checkMobile = debounce(() => {
|
|
37
|
+
setIsMobile(window.innerWidth < 768);
|
|
38
|
+
}, 100);
|
|
39
|
+
|
|
40
|
+
checkMobile();
|
|
41
|
+
window.addEventListener("resize", checkMobile);
|
|
42
|
+
return () => window.removeEventListener("resize", checkMobile);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const productCategories: ProductCategory[] = [
|
|
46
|
+
{
|
|
47
|
+
title: "Test Product",
|
|
48
|
+
description:
|
|
49
|
+
"New for 2025 are several styles of natural stone boulders. Check out the awesome patterns!",
|
|
50
|
+
image: "images/products/boulders.jpg",
|
|
51
|
+
link: "/products/productOne",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const tabs = ["All", ...productCategories.map((category) => category.title)];
|
|
56
|
+
|
|
57
|
+
const filteredProducts =
|
|
58
|
+
activeTab === "All"
|
|
59
|
+
? productCategories
|
|
60
|
+
: productCategories.filter((category) => category.title === activeTab);
|
|
61
|
+
|
|
62
|
+
const sectionVariants = {
|
|
63
|
+
hidden: { opacity: 0, y: 20 },
|
|
64
|
+
visible: {
|
|
65
|
+
opacity: 1,
|
|
66
|
+
y: 0,
|
|
67
|
+
transition: { duration: isMobile ? 0.3 : 0.5, ease: "easeOut" },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const cardVariants = {
|
|
72
|
+
hidden: { opacity: 0, y: 20 },
|
|
73
|
+
visible: {
|
|
74
|
+
opacity: 1,
|
|
75
|
+
y: 0,
|
|
76
|
+
transition: { duration: isMobile ? 0.3 : 0.5, ease: "easeOut" },
|
|
77
|
+
},
|
|
78
|
+
hover: {
|
|
79
|
+
scale: isMobile ? 1 : 1.05,
|
|
80
|
+
transition: { duration: 0.2, ease: "easeOut" },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const itemVariants = {
|
|
85
|
+
hidden: { opacity: 0, y: 10 },
|
|
86
|
+
visible: {
|
|
87
|
+
opacity: 1,
|
|
88
|
+
y: 0,
|
|
89
|
+
transition: { duration: isMobile ? 0.3 : 0.5, ease: "easeOut" },
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (isLoading) {
|
|
94
|
+
return <Spinner />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="min-h-screen w-full relative flex flex-col">
|
|
99
|
+
<style jsx>{`
|
|
100
|
+
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&display=swap");
|
|
101
|
+
|
|
102
|
+
:root {
|
|
103
|
+
--jubilee: #F47C7C;
|
|
104
|
+
--natural-white: #F7F7F7;
|
|
105
|
+
--caviar: #2D2D2D;
|
|
106
|
+
--storm-cloud: #4A636E;
|
|
107
|
+
--exuberant-blue: #FF69B4;
|
|
108
|
+
--modern-purple: #D946EF;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
* {
|
|
112
|
+
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.gradient-text {
|
|
116
|
+
background: linear-gradient(135deg, var(--exuberant-blue), var(--jubilee), var(--modern-purple));
|
|
117
|
+
-webkit-background-clip: text;
|
|
118
|
+
background-clip: text;
|
|
119
|
+
color: transparent;
|
|
120
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.glassmorphism {
|
|
124
|
+
background: rgba(255, 255, 255, 0.1);
|
|
125
|
+
backdrop-filter: blur(12px);
|
|
126
|
+
-webkit-backdrop-filter: blur(12px);
|
|
127
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
128
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
|
129
|
+
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@supports not (backdrop-filter: blur(12px)) {
|
|
133
|
+
.glassmorphism {
|
|
134
|
+
background: rgba(255, 255, 255, 0.3);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.glassmorphism:hover {
|
|
139
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@media (max-width: 768px) {
|
|
143
|
+
.glassmorphism {
|
|
144
|
+
padding: 1rem;
|
|
145
|
+
}
|
|
146
|
+
.gradient-text {
|
|
147
|
+
font-size: 2.5rem;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.line-clamp-8 {
|
|
152
|
+
display: -webkit-box;
|
|
153
|
+
-webkit-line-clamp: 8;
|
|
154
|
+
-webkit-box-orient: vertical;
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
}
|
|
157
|
+
`}</style>
|
|
158
|
+
<div className="w-full pt-20 lg:pt-40 flex-grow relative z-10 container-padding bg-white">
|
|
159
|
+
<ProductHero />
|
|
160
|
+
<motion.section
|
|
161
|
+
className="relative z-10 py-16 sm:py-20 lg:py-24 px-4 sm:px-6 lg:px-8 bg-white"
|
|
162
|
+
variants={sectionVariants}
|
|
163
|
+
initial="hidden"
|
|
164
|
+
animate="visible"
|
|
165
|
+
>
|
|
166
|
+
<div className="relative max-w-7xl mx-auto">
|
|
167
|
+
{/* Tabs Filter */}
|
|
168
|
+
<motion.div
|
|
169
|
+
className="mb-12 hidden sm:flex flex-wrap justify-center gap-4"
|
|
170
|
+
variants={itemVariants}
|
|
171
|
+
>
|
|
172
|
+
{tabs.map((tab) => (
|
|
173
|
+
<motion.button
|
|
174
|
+
key={tab}
|
|
175
|
+
onClick={() => setActiveTab(tab)}
|
|
176
|
+
className={`px-6 py-3 rounded-lg text-base sm:text-lg font-medium transition-all duration-300 ${
|
|
177
|
+
activeTab === tab
|
|
178
|
+
? "bg-amber-600 text-white shadow-md"
|
|
179
|
+
: "bg-gray-200 text-gray-800 hover:bg-gray-300"
|
|
180
|
+
}`}
|
|
181
|
+
variants={itemVariants}
|
|
182
|
+
>
|
|
183
|
+
{tab}
|
|
184
|
+
</motion.button>
|
|
185
|
+
))}
|
|
186
|
+
</motion.div>
|
|
187
|
+
{/* Dropdown (Mobile) */}
|
|
188
|
+
<motion.div
|
|
189
|
+
className="mb-12 sm:hidden px-2"
|
|
190
|
+
variants={itemVariants}
|
|
191
|
+
>
|
|
192
|
+
<div className="relative">
|
|
193
|
+
<select
|
|
194
|
+
value={activeTab}
|
|
195
|
+
onChange={(e) => setActiveTab(e.target.value)}
|
|
196
|
+
className="w-full appearance-none px-5 py-3 bg-gray-200 text-gray-800 font-medium rounded-lg shadow-md focus:outline-none focus:ring-2 focus:ring-green-600 transition-all duration-200"
|
|
197
|
+
>
|
|
198
|
+
{tabs.map((tab) => (
|
|
199
|
+
<option key={tab} value={tab}>
|
|
200
|
+
{tab}
|
|
201
|
+
</option>
|
|
202
|
+
))}
|
|
203
|
+
</select>
|
|
204
|
+
<div className="absolute inset-y-0 right-4 flex items-center pointer-events-none">
|
|
205
|
+
<svg
|
|
206
|
+
className="w-4 h-4 text-gray-500"
|
|
207
|
+
fill="none"
|
|
208
|
+
stroke="currentColor"
|
|
209
|
+
strokeWidth="2"
|
|
210
|
+
viewBox="0 0 24 24"
|
|
211
|
+
>
|
|
212
|
+
<path
|
|
213
|
+
d="M19 9l-7 7-7-7"
|
|
214
|
+
strokeLinecap="round"
|
|
215
|
+
strokeLinejoin="round"
|
|
216
|
+
/>
|
|
217
|
+
</svg>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</motion.div>
|
|
221
|
+
{/* Filtered Products */}
|
|
222
|
+
<motion.div
|
|
223
|
+
className="space-y-12"
|
|
224
|
+
variants={sectionVariants}
|
|
225
|
+
>
|
|
226
|
+
{filteredProducts.length > 0 ? (
|
|
227
|
+
filteredProducts.map((category, index) => (
|
|
228
|
+
<motion.div
|
|
229
|
+
key={index}
|
|
230
|
+
className="group bg-white rounded-xl shadow-md hover:shadow-lg hover:bg-gray-50 transition-all duration-300 overflow-hidden"
|
|
231
|
+
variants={cardVariants}
|
|
232
|
+
initial="hidden"
|
|
233
|
+
animate="visible"
|
|
234
|
+
whileHover="hover"
|
|
235
|
+
>
|
|
236
|
+
<div className="flex flex-col md:flex-row">
|
|
237
|
+
<div className="relative w-full md:w-1/2 h-80 sm:h-96 md:h-[600px] overflow-hidden">
|
|
238
|
+
<Image
|
|
239
|
+
src={category.image}
|
|
240
|
+
alt={category.title}
|
|
241
|
+
fill
|
|
242
|
+
sizes="100vw"
|
|
243
|
+
className="object-cover transition-transform duration-500 group-hover:scale-110"
|
|
244
|
+
/>
|
|
245
|
+
<div className="absolute inset-0 bg-gradient-to-t from-gray-800/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="p-6 sm:p-8 lg:p-12 flex flex-col w-full md:w-1/2">
|
|
248
|
+
<h3 className="text-xl sm:text-2xl lg:text-3xl font-semibold mb-4 text-gray-900">
|
|
249
|
+
<Link
|
|
250
|
+
href={category.link}
|
|
251
|
+
className="hover:text-green-600 transition-colors duration-300"
|
|
252
|
+
>
|
|
253
|
+
{category.title}
|
|
254
|
+
</Link>
|
|
255
|
+
</h3>
|
|
256
|
+
<p className="text-base sm:text-lg lg:text-xl text-gray-700 mb-6 line-clamp-8">
|
|
257
|
+
{category.description}
|
|
258
|
+
</p>
|
|
259
|
+
<Link
|
|
260
|
+
href={category.link}
|
|
261
|
+
className="inline-flex items-center text-gray-900 font-semibold text-base sm:text-lg lg:text-lg hover:text-green-600 transition-colors duration-300"
|
|
262
|
+
>
|
|
263
|
+
Explore {category.title}
|
|
264
|
+
<svg
|
|
265
|
+
className="ml-3 h-5 w-5 text-gray-900 group-hover:text-green-600 transition-transform duration-300 group-hover:translate-x-2"
|
|
266
|
+
fill="none"
|
|
267
|
+
stroke="currentColor"
|
|
268
|
+
strokeWidth="2"
|
|
269
|
+
viewBox="0 0 24 24"
|
|
270
|
+
>
|
|
271
|
+
<path
|
|
272
|
+
d="M9 5l7 7-7 7"
|
|
273
|
+
strokeLinecap="round"
|
|
274
|
+
strokeLinejoin="round"
|
|
275
|
+
/>
|
|
276
|
+
</svg>
|
|
277
|
+
</Link>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</motion.div>
|
|
281
|
+
))
|
|
282
|
+
) : (
|
|
283
|
+
<motion.p
|
|
284
|
+
className="text-center text-lg text-gray-700"
|
|
285
|
+
variants={itemVariants}
|
|
286
|
+
>
|
|
287
|
+
No products found for this category.
|
|
288
|
+
</motion.p>
|
|
289
|
+
)}
|
|
290
|
+
</motion.div>
|
|
291
|
+
</div>
|
|
292
|
+
</motion.section>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|