@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
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { Button, UploadButton, ToggleButton, CloseButton, DeleteButton, CancelButton, TrashIconButton } from '@/components/other/button'; // Updated import
|
|
5
|
-
import { Card } from '@/components/other/card';
|
|
6
|
-
import { Upload, FileText, Trash2, X } from 'lucide-react';
|
|
7
|
-
import { motion } from 'framer-motion';
|
|
8
|
-
import { useUser } from '@clerk/nextjs';
|
|
9
|
-
|
|
10
|
-
interface UploadedFile {
|
|
11
|
-
id: number;
|
|
12
|
-
documentId: string;
|
|
13
|
-
name: string;
|
|
14
|
-
url: string;
|
|
15
|
-
createdAt: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface FileUploaderProps {
|
|
19
|
-
uploadedFiles: UploadedFile[];
|
|
20
|
-
setUploadedFiles: (files: UploadedFile[]) => void;
|
|
21
|
-
setPdfUrl: (url: string) => void;
|
|
22
|
-
pdfUrl: string;
|
|
23
|
-
isAdmin: boolean;
|
|
24
|
-
error?: string | null;
|
|
25
|
-
setError: (error: string | null) => void;
|
|
26
|
-
handleFileUpload: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
|
|
27
|
-
handleDeleteFile: (documentId: string) => Promise<void>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function FileUploader({
|
|
31
|
-
uploadedFiles,
|
|
32
|
-
setUploadedFiles,
|
|
33
|
-
setPdfUrl,
|
|
34
|
-
pdfUrl,
|
|
35
|
-
isAdmin,
|
|
36
|
-
error,
|
|
37
|
-
setError,
|
|
38
|
-
handleFileUpload,
|
|
39
|
-
handleDeleteFile,
|
|
40
|
-
}: FileUploaderProps) {
|
|
41
|
-
const { isSignedIn } = useUser();
|
|
42
|
-
const [uploading, setUploading] = useState(false);
|
|
43
|
-
const [showAllFiles, setShowAllFiles] = useState(false);
|
|
44
|
-
const [isConfirmDeleteOpen, setIsConfirmDeleteOpen] = useState(false);
|
|
45
|
-
const [fileToDelete, setFileToDelete] = useState<string | null>(null);
|
|
46
|
-
|
|
47
|
-
const baseUrl = process.env.BASE_URL || '';
|
|
48
|
-
|
|
49
|
-
useEffect(() => { }, [isSignedIn, isAdmin]);
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
const event = new CustomEvent('modalStateChange', {
|
|
53
|
-
detail: { isOpen: isConfirmDeleteOpen },
|
|
54
|
-
});
|
|
55
|
-
window.dispatchEvent(event);
|
|
56
|
-
}, [isConfirmDeleteOpen]);
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (isConfirmDeleteOpen) {
|
|
60
|
-
document.body.classList.add('overflow-hidden');
|
|
61
|
-
} else {
|
|
62
|
-
document.body.classList.remove('overflow-hidden');
|
|
63
|
-
}
|
|
64
|
-
return () => {
|
|
65
|
-
document.body.classList.remove('overflow-hidden');
|
|
66
|
-
};
|
|
67
|
-
}, [isConfirmDeleteOpen]);
|
|
68
|
-
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
const handleEsc = (e: KeyboardEvent) => {
|
|
71
|
-
if (e.key === 'Escape' && isConfirmDeleteOpen) {
|
|
72
|
-
handleCancelDelete();
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
window.addEventListener('keydown', handleEsc);
|
|
76
|
-
return () => window.removeEventListener('keydown', handleEsc);
|
|
77
|
-
}, [isConfirmDeleteOpen]);
|
|
78
|
-
|
|
79
|
-
const openConfirmDelete = (documentId: string) => {
|
|
80
|
-
setFileToDelete(documentId);
|
|
81
|
-
setIsConfirmDeleteOpen(true);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const handleConfirmDelete = async () => {
|
|
85
|
-
if (fileToDelete !== null) {
|
|
86
|
-
setUploading(true);
|
|
87
|
-
setError(null);
|
|
88
|
-
try {
|
|
89
|
-
await handleDeleteFile(fileToDelete);
|
|
90
|
-
} finally {
|
|
91
|
-
setUploading(false);
|
|
92
|
-
setIsConfirmDeleteOpen(false);
|
|
93
|
-
setFileToDelete(null);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const handleCancelDelete = () => {
|
|
99
|
-
setIsConfirmDeleteOpen(false);
|
|
100
|
-
setFileToDelete(null);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const truncateText = (text: string, maxLength: number = 30): string => {
|
|
104
|
-
if (text.length <= maxLength) return text;
|
|
105
|
-
return text.slice(0, maxLength - 3) + '...';
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const modalVariants = {
|
|
109
|
-
hidden: { opacity: 0, y: '100vh' },
|
|
110
|
-
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] } },
|
|
111
|
-
exit: { opacity: 0, y: '100vh', transition: { duration: 0.3, ease: 'easeIn' } },
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
115
|
-
setUploading(true);
|
|
116
|
-
setError(null);
|
|
117
|
-
try {
|
|
118
|
-
await handleFileUpload(e);
|
|
119
|
-
} finally {
|
|
120
|
-
setUploading(false);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<div className="w-full bg-[#1E2A44] px-4 py-12 sm:px-8 sm:py-16 border-none outline-none z-0">
|
|
126
|
-
<style jsx>{`
|
|
127
|
-
.modal-glassmorphism {
|
|
128
|
-
background: rgba(255, 255, 255, 0.08);
|
|
129
|
-
backdrop-filter: blur(10px);
|
|
130
|
-
-webkit-backdrop-filter: blur(10px);
|
|
131
|
-
border: 2px solid transparent;
|
|
132
|
-
border-image: linear-gradient(
|
|
133
|
-
45deg,
|
|
134
|
-
var(--exuberant-blue),
|
|
135
|
-
var(--jubilee),
|
|
136
|
-
var(--modern-purple)
|
|
137
|
-
) 1;
|
|
138
|
-
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.1);
|
|
139
|
-
border-radius: 2rem;
|
|
140
|
-
max-height: 90vh;
|
|
141
|
-
overflow: hidden;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
@supports not (backdrop-filter: blur(10px)) {
|
|
145
|
-
.modal-glassmorphism {
|
|
146
|
-
background: rgba(255, 255, 255, 0.2);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
@media (max-width: 640px) {
|
|
151
|
-
.modal-glassmorphism {
|
|
152
|
-
padding: 1rem;
|
|
153
|
-
max-height: 85vh;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.file-item {
|
|
158
|
-
z-index: 30;
|
|
159
|
-
pointer-events: auto;
|
|
160
|
-
cursor: pointer;
|
|
161
|
-
}
|
|
162
|
-
`}</style>
|
|
163
|
-
<Card className="w-full max-w-7xl mx-auto p-4 sm:p-8 bg-[#1E2A44] text-white flex flex-col items-center space-y-6 rounded-none border-none outline-none relative z-10">
|
|
164
|
-
<h3 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight text-white text-center">
|
|
165
|
-
Fitness Forum
|
|
166
|
-
</h3>
|
|
167
|
-
<div className="space-y-4 w-full">
|
|
168
|
-
{error && (
|
|
169
|
-
<p className="text-red-400 text-base sm:text-lg md:text-xl text-center">{error}</p>
|
|
170
|
-
)}
|
|
171
|
-
{isAdmin ? (
|
|
172
|
-
<div className="flex flex-col sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4">
|
|
173
|
-
<input
|
|
174
|
-
type="file"
|
|
175
|
-
accept="application/pdf"
|
|
176
|
-
onChange={onFileChange}
|
|
177
|
-
disabled={uploading}
|
|
178
|
-
className="hidden"
|
|
179
|
-
id="file-upload"
|
|
180
|
-
/>
|
|
181
|
-
<label htmlFor="file-upload" className="cursor-pointer">
|
|
182
|
-
<UploadButton disabled={uploading} asChild>
|
|
183
|
-
<span className="flex items-center justify-center">
|
|
184
|
-
<Upload className="mr-2 h-4 w-4 sm:h-5 sm:w-5" />
|
|
185
|
-
Upload PDF
|
|
186
|
-
</span>
|
|
187
|
-
</UploadButton>
|
|
188
|
-
</label>
|
|
189
|
-
{uploading && (
|
|
190
|
-
<span className="text-white font-bold text-base sm:text-lg">Uploading...</span>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
) : (
|
|
194
|
-
<p className="text-gray-400 text-sm font-medium text-center">
|
|
195
|
-
{isSignedIn ? '' : ''}
|
|
196
|
-
</p>
|
|
197
|
-
)}
|
|
198
|
-
{uploadedFiles.length > 0 ? (
|
|
199
|
-
<>
|
|
200
|
-
{uploadedFiles.slice(0, showAllFiles ? undefined : 8).map((file) => {
|
|
201
|
-
const token = localStorage.getItem('jwt') || '';
|
|
202
|
-
const fullUrl = file.url.startsWith('http')
|
|
203
|
-
? `${file.url}${token ? (file.url.includes('?') ? '&' : '?') + `token=${token}` : ''}`
|
|
204
|
-
: `${baseUrl}${file.url}${token ? `?token=${token}` : ''}`;
|
|
205
|
-
return (
|
|
206
|
-
<div
|
|
207
|
-
key={file.documentId}
|
|
208
|
-
className="file-item flex flex-col sm:flex-row items-center justify-between p-4 sm:p-6 rounded-lg bg-[#2A3A66]/50 hover:bg-[#2A3A66]/70 hover:shadow-lg hover:border-[#FF69B4]/50 transition-all cursor-pointer"
|
|
209
|
-
onClick={() => {
|
|
210
|
-
setPdfUrl(fullUrl);
|
|
211
|
-
window.open(fullUrl, '_blank', 'noopener,noreferrer');
|
|
212
|
-
}}
|
|
213
|
-
role="button"
|
|
214
|
-
tabIndex={0}
|
|
215
|
-
onKeyDown={(e) => {
|
|
216
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
217
|
-
setPdfUrl(fullUrl);
|
|
218
|
-
window.open(fullUrl, '_blank', 'noopener,noreferrer');
|
|
219
|
-
}
|
|
220
|
-
}}
|
|
221
|
-
aria-label={`Open PDF: ${truncateText(file.name)}`}
|
|
222
|
-
>
|
|
223
|
-
<div className="text-white font-bold text-base sm:text-lg md:text-xl flex items-center w-full sm:w-auto min-w-0 sm:pb-0 pb-6">
|
|
224
|
-
<FileText className="mr-2 h-4 w-4 sm:h-5 sm:w-5 text-[#FF69B4] flex-shrink-0" />
|
|
225
|
-
<span className="truncate">{truncateText(file.name, 30)}</span>
|
|
226
|
-
</div>
|
|
227
|
-
{isAdmin && (
|
|
228
|
-
<TrashIconButton
|
|
229
|
-
onClick={(e) => {
|
|
230
|
-
e.stopPropagation();
|
|
231
|
-
openConfirmDelete(file.documentId);
|
|
232
|
-
}}
|
|
233
|
-
aria-label={`Delete file ${truncateText(file.name)}`}
|
|
234
|
-
variant="trash-icon"
|
|
235
|
-
className="relative"
|
|
236
|
-
>
|
|
237
|
-
<Trash2 />
|
|
238
|
-
</TrashIconButton>
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
);
|
|
242
|
-
})}
|
|
243
|
-
{uploadedFiles.length > 8 && (
|
|
244
|
-
<div className="text-center mt-4">
|
|
245
|
-
<ToggleButton onClick={() => setShowAllFiles(!showAllFiles)}>
|
|
246
|
-
{showAllFiles ? 'Show Less' : 'Show More'}
|
|
247
|
-
</ToggleButton>
|
|
248
|
-
</div>
|
|
249
|
-
)}
|
|
250
|
-
</>
|
|
251
|
-
) : (
|
|
252
|
-
<p className="text-gray-300 font-bold text-base sm:text-lg md:text-xl text-center">
|
|
253
|
-
No files uploaded yet.
|
|
254
|
-
</p>
|
|
255
|
-
)}
|
|
256
|
-
</div>
|
|
257
|
-
</Card>
|
|
258
|
-
|
|
259
|
-
{isConfirmDeleteOpen && isAdmin && (
|
|
260
|
-
<motion.div
|
|
261
|
-
variants={modalVariants}
|
|
262
|
-
initial="hidden"
|
|
263
|
-
animate="visible"
|
|
264
|
-
exit="exit"
|
|
265
|
-
className="fixed inset-0 bg-black/90 flex items-center justify-center z-[10000] p-4 isolate"
|
|
266
|
-
onClick={handleCancelDelete}
|
|
267
|
-
role="dialog"
|
|
268
|
-
aria-modal="true"
|
|
269
|
-
aria-labelledby="modal-title"
|
|
270
|
-
>
|
|
271
|
-
<div
|
|
272
|
-
className="relative max-w-5xl w-full mx-4 p-6 bg-gray-800/50 border border-gray-700/50 rounded-lg shadow-lg"
|
|
273
|
-
onClick={(e) => e.stopPropagation()}
|
|
274
|
-
>
|
|
275
|
-
<CloseButton onClick={handleCancelDelete}>
|
|
276
|
-
<X className="h-6 w-6 sm:h-8 sm:w-8" />
|
|
277
|
-
</CloseButton>
|
|
278
|
-
<h3 id="modal-title" className="text-xl font-bold text-white mb-4">
|
|
279
|
-
Confirm Deletion
|
|
280
|
-
</h3>
|
|
281
|
-
<p className="text-gray-300 text-sm mb-4">
|
|
282
|
-
Are you sure you want to delete this file? This action cannot be undone.
|
|
283
|
-
</p>
|
|
284
|
-
<div className="flex space-x-3">
|
|
285
|
-
<DeleteButton onClick={handleConfirmDelete} disabled={uploading}>
|
|
286
|
-
{uploading ? 'Deleting...' : 'Delete'}
|
|
287
|
-
</DeleteButton>
|
|
288
|
-
<CancelButton onClick={handleCancelDelete}>Cancel</CancelButton>
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
</motion.div>
|
|
292
|
-
)}
|
|
293
|
-
</div>
|
|
294
|
-
);
|
|
295
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// src/components/FeaturesSection.tsx
|
|
2
|
-
"use client";
|
|
3
|
-
|
|
4
|
-
import { ActionButton } from "@/components/other/button";
|
|
5
|
-
import { CheckCircle, ArrowRight } from "lucide-react";
|
|
6
|
-
import Link from "next/link";
|
|
7
|
-
|
|
8
|
-
interface Feature {
|
|
9
|
-
text: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface FeaturesSectionProps {
|
|
13
|
-
features: Feature[];
|
|
14
|
-
title: string;
|
|
15
|
-
description: string;
|
|
16
|
-
ctaText: string;
|
|
17
|
-
ctaLink: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function FeaturesSection({
|
|
21
|
-
features,
|
|
22
|
-
title,
|
|
23
|
-
description,
|
|
24
|
-
ctaText,
|
|
25
|
-
ctaLink,
|
|
26
|
-
}: FeaturesSectionProps) {
|
|
27
|
-
return (
|
|
28
|
-
<section className="w-full py-16 sm:py-24 px-4 sm:px-8 bg-blue-200 z-0">
|
|
29
|
-
<div className="w-full pt-16 sm:pt-24 pb-16 sm:pb-24">
|
|
30
|
-
<div className="max-w-7xl mx-auto px-4 sm:px-6">
|
|
31
|
-
<div className="flex flex-col md:flex-row gap-6 justify-center items-center">
|
|
32
|
-
<div className="w-full md:w-1/2 space-y-6 flex justify-center">
|
|
33
|
-
<ul className="space-y-4 text-2xl font-semibold">
|
|
34
|
-
{features.map((feature, index) => (
|
|
35
|
-
<li key={index} className="flex items-center justify-start">
|
|
36
|
-
<CheckCircle className="mr-3 h-6 w-6 text-[#FF69B4] flex-shrink-0" />
|
|
37
|
-
<span>{feature.text}</span>
|
|
38
|
-
</li>
|
|
39
|
-
))}
|
|
40
|
-
</ul>
|
|
41
|
-
</div>
|
|
42
|
-
<div className="w-full md:w-1/2 space-y-6 text-center pt-12">
|
|
43
|
-
<h4 className="text-4xl sm:text-5xl font-bold text-[#333] tracking-tight">
|
|
44
|
-
{title}
|
|
45
|
-
</h4>
|
|
46
|
-
<p className="text-xl sm:text-2xl font-semibold text-[#333]">
|
|
47
|
-
{description}
|
|
48
|
-
</p>
|
|
49
|
-
<ActionButton
|
|
50
|
-
asChild
|
|
51
|
-
>
|
|
52
|
-
<Link href={ctaLink} className="flex items-center justify-center">
|
|
53
|
-
{ctaText}
|
|
54
|
-
<ArrowRight className="ml-2 h-6 w-6" />
|
|
55
|
-
</Link>
|
|
56
|
-
</ActionButton>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
</section>
|
|
62
|
-
);
|
|
63
|
-
}
|
package/components/types.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// src/pages/types.ts
|
|
2
|
-
export interface ScheduleClass {
|
|
3
|
-
name: string;
|
|
4
|
-
startTime: string;
|
|
5
|
-
endTime: string;
|
|
6
|
-
id: number;
|
|
7
|
-
documentId: string;
|
|
8
|
-
daysOfWeek: number[];
|
|
9
|
-
classDescription?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface WeeklySchedule {
|
|
13
|
-
[key: string]: ScheduleClass[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface StrapiUser {
|
|
17
|
-
id: number;
|
|
18
|
-
username: string;
|
|
19
|
-
email: string;
|
|
20
|
-
businessAdminId?: string;
|
|
21
|
-
userRole?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type Category =
|
|
25
|
-
| "none"
|
|
26
|
-
| "indoor"
|
|
27
|
-
| "outdoor"
|
|
28
|
-
| "commercial";
|
|
29
|
-
|
|
30
|
-
export interface UploadedImage {
|
|
31
|
-
id: number;
|
|
32
|
-
documentId: string;
|
|
33
|
-
title?: string;
|
|
34
|
-
description?: string;
|
|
35
|
-
url: string;
|
|
36
|
-
createdAt: string;
|
|
37
|
-
category?: Category;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface FormState {
|
|
41
|
-
file: File | null;
|
|
42
|
-
title: string;
|
|
43
|
-
description: string;
|
|
44
|
-
category: Category;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface EditFormState extends FormState {
|
|
48
|
-
id: number;
|
|
49
|
-
documentId: string;
|
|
50
|
-
}
|
package/lib/auth-context.tsx
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { createContext, useContext, useState, useEffect } from "react";
|
|
4
|
-
import { useAuth as useClerkAuth, useUser } from "@clerk/nextjs";
|
|
5
|
-
|
|
6
|
-
interface StrapiUser {
|
|
7
|
-
id: number;
|
|
8
|
-
username: string;
|
|
9
|
-
email: string;
|
|
10
|
-
businessAdminId?: string;
|
|
11
|
-
isAdmin?: boolean;
|
|
12
|
-
firstName?: string;
|
|
13
|
-
lastName?: string;
|
|
14
|
-
businessId?: string[];
|
|
15
|
-
dateJoined?: string;
|
|
16
|
-
businessOwner?: boolean;
|
|
17
|
-
userStatus?: string;
|
|
18
|
-
timezone?: string;
|
|
19
|
-
language?: string;
|
|
20
|
-
isVerified?: boolean;
|
|
21
|
-
authProvider?: string;
|
|
22
|
-
authId?: string;
|
|
23
|
-
businessTitle?: string;
|
|
24
|
-
userTitle?: string;
|
|
25
|
-
number?: string;
|
|
26
|
-
address?: {
|
|
27
|
-
zip: string;
|
|
28
|
-
city: string;
|
|
29
|
-
state: string;
|
|
30
|
-
street: string;
|
|
31
|
-
country: string;
|
|
32
|
-
};
|
|
33
|
-
websiteUrl?: string;
|
|
34
|
-
userRole?: string;
|
|
35
|
-
primaryBusinessColor?: string;
|
|
36
|
-
secondaryBusinessColor?: string;
|
|
37
|
-
logoImage?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface AuthContextType {
|
|
41
|
-
user: StrapiUser | null;
|
|
42
|
-
authLoading: boolean;
|
|
43
|
-
error: string | null;
|
|
44
|
-
checkSession: () => Promise<void>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
48
|
-
|
|
49
|
-
const userCache = new Map<string, StrapiUser>();
|
|
50
|
-
|
|
51
|
-
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
52
|
-
const [user, setUser] = useState<StrapiUser | null>(null);
|
|
53
|
-
const [authLoading, setAuthLoading] = useState(true);
|
|
54
|
-
const [error, setError] = useState<string | null>(null); // Added error state
|
|
55
|
-
const { isSignedIn, userId } = useClerkAuth();
|
|
56
|
-
const { user: clerkUser } = useUser();
|
|
57
|
-
|
|
58
|
-
const syncUserToStrapi = async (clerkUserData: any) => {
|
|
59
|
-
if (!clerkUserData || !userId) {
|
|
60
|
-
console.error("Missing clerkUserData or userId:", { clerkUserData, userId });
|
|
61
|
-
setError("Missing user data for sync");
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (userCache.has(userId)) {
|
|
66
|
-
setUser(userCache.get(userId)!);
|
|
67
|
-
setError(null);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const response = await fetch("/api/sync-user", {
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: {
|
|
75
|
-
"Content-Type": "application/json",
|
|
76
|
-
},
|
|
77
|
-
body: JSON.stringify(clerkUserData),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (!response.ok) {
|
|
81
|
-
const errorText = await response.text();
|
|
82
|
-
console.error(`Failed to sync user: ${response.status}`, errorText);
|
|
83
|
-
setError(`Failed to sync user: ${response.status}`);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const strapiUser = await response.json();
|
|
88
|
-
userCache.set(userId, strapiUser);
|
|
89
|
-
setUser(strapiUser);
|
|
90
|
-
setError(null);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
console.error("Error syncing user to Strapi:", error);
|
|
93
|
-
setError(error instanceof Error ? error.message : "Failed to sync user");
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const checkSession = async () => {
|
|
98
|
-
setAuthLoading(true);
|
|
99
|
-
if (!isSignedIn || !userId || !clerkUser) {
|
|
100
|
-
setUser(null);
|
|
101
|
-
setError("User not signed in");
|
|
102
|
-
setAuthLoading(false);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
await syncUserToStrapi(clerkUser);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
console.error("checkSession error:", err);
|
|
109
|
-
setError(err instanceof Error ? err.message : "Session check failed");
|
|
110
|
-
setUser(null);
|
|
111
|
-
} finally {
|
|
112
|
-
setAuthLoading(false);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
checkSession();
|
|
118
|
-
}, [isSignedIn, userId, clerkUser]);
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<AuthContext.Provider value={{ user, authLoading, error, checkSession }}>
|
|
122
|
-
{children}
|
|
123
|
-
</AuthContext.Provider>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function useStrapiAuth() {
|
|
128
|
-
const context = useContext(AuthContext);
|
|
129
|
-
if (!context) throw new Error("useStrapiAuth must be used within an AuthProvider");
|
|
130
|
-
return context;
|
|
131
|
-
}
|
package/lib/verify-user.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { getAuth } from "@clerk/nextjs/server";
|
|
2
|
-
import { NextRequest } from "next/server";
|
|
3
|
-
|
|
4
|
-
interface StrapiUser {
|
|
5
|
-
id: number;
|
|
6
|
-
username: string;
|
|
7
|
-
email: string;
|
|
8
|
-
businessAdminId?: string;
|
|
9
|
-
isAdmin?: boolean;
|
|
10
|
-
firstName?: string;
|
|
11
|
-
lastName?: string;
|
|
12
|
-
businessId?: string[];
|
|
13
|
-
dateJoined?: string;
|
|
14
|
-
businessOwner?: boolean;
|
|
15
|
-
userStatus?: string;
|
|
16
|
-
timezone?: string;
|
|
17
|
-
language?: string;
|
|
18
|
-
isVerified?: boolean;
|
|
19
|
-
authProvider?: string;
|
|
20
|
-
authId?: string;
|
|
21
|
-
businessTitle?: string;
|
|
22
|
-
userTitle?: string;
|
|
23
|
-
number?: string;
|
|
24
|
-
address?: {
|
|
25
|
-
zip: string;
|
|
26
|
-
city: string;
|
|
27
|
-
state: string;
|
|
28
|
-
street: string;
|
|
29
|
-
country: string;
|
|
30
|
-
};
|
|
31
|
-
websiteUrl?: string;
|
|
32
|
-
userRole?: string;
|
|
33
|
-
primaryBusinessColor?: string;
|
|
34
|
-
secondaryBusinessColor?: string;
|
|
35
|
-
logoImage?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function verifyUser(request: NextRequest): Promise<StrapiUser | null> {
|
|
39
|
-
const { userId } = getAuth(request);
|
|
40
|
-
if (!userId) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const maxRetries = 3;
|
|
45
|
-
let attempt = 0;
|
|
46
|
-
|
|
47
|
-
while (attempt < maxRetries) {
|
|
48
|
-
try {
|
|
49
|
-
const response = await fetch(
|
|
50
|
-
`${process.env.STRAPI_USER_LIST_API_URL}?filters[authId][$eq]=${userId}&populate=*`,
|
|
51
|
-
{
|
|
52
|
-
headers: {
|
|
53
|
-
Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`,
|
|
54
|
-
"Content-Type": "application/json",
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
if (response.status === 429) {
|
|
61
|
-
attempt++;
|
|
62
|
-
const delay = Math.pow(2, attempt) * 1000;
|
|
63
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
console.error("User verification failed:", response.status);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const users = await response.json();
|
|
71
|
-
if (!users.data || users.data.length === 0) {
|
|
72
|
-
console.error("No user found in user-list for authId:", userId);
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const userData = users.data[0].attributes;
|
|
77
|
-
return {
|
|
78
|
-
id: users.data[0].id,
|
|
79
|
-
authId: userData.authId,
|
|
80
|
-
authProvider: userData.authProvider,
|
|
81
|
-
email: userData.email,
|
|
82
|
-
username: userData.username,
|
|
83
|
-
businessAdminId: userData.businessAdminId,
|
|
84
|
-
isAdmin: userData.userRole === "admin" && userData.businessAdminId === process.env.ADMIN_BUSINESS_ID,
|
|
85
|
-
firstName: userData.firstName,
|
|
86
|
-
lastName: userData.lastName,
|
|
87
|
-
dateJoined: userData.dateJoined,
|
|
88
|
-
businessOwner: userData.businessOwner,
|
|
89
|
-
userStatus: userData.userStatus,
|
|
90
|
-
timezone: userData.timezone,
|
|
91
|
-
language: userData.language,
|
|
92
|
-
isVerified: userData.isVerified,
|
|
93
|
-
businessTitle: userData.businessTitle,
|
|
94
|
-
userTitle: userData.userTitle,
|
|
95
|
-
number: userData.number,
|
|
96
|
-
address: userData.address,
|
|
97
|
-
websiteUrl: userData.websiteUrl,
|
|
98
|
-
userRole: userData.userRole,
|
|
99
|
-
primaryBusinessColor: userData.primaryBusinessColor,
|
|
100
|
-
secondaryBusinessColor: userData.secondaryBusinessColor,
|
|
101
|
-
logoImage: userData.logoImage?.data?.attributes?.url,
|
|
102
|
-
businessId: userData.businessId,
|
|
103
|
-
};
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error(`Error verifying user (attempt ${attempt + 1}/${maxRetries}):`, error);
|
|
106
|
-
attempt++;
|
|
107
|
-
if (attempt < maxRetries) {
|
|
108
|
-
const delay = Math.pow(2, attempt) * 1000;
|
|
109
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
110
|
-
} else {
|
|
111
|
-
console.error("Max retries reached for Strapi API");
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
File without changes
|