@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/README.md
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
- Initialize the project with the prebuilt template: `npx devvista-kit`
|
|
7
7
|
- Start the development server: `npm run dev`
|
|
8
8
|
|
|
9
|
+
Now after install, the steps to set up a repo:
|
|
10
|
+
$ rm -rf .git
|
|
11
|
+
$ git init
|
|
12
|
+
$ cat .gitignore
|
|
13
|
+
node_modules/
|
|
14
|
+
$ git add .
|
|
15
|
+
git remote add origin https://github.com/DevVistaTech/reponame.git
|
|
16
|
+
git push -f -u origin main
|
|
17
|
+
|
|
9
18
|
### Updating the npm Package with the Latest Version
|
|
10
19
|
|
|
11
20
|
- Increment the package version: `npm version patch # e.g., to 0.1.8`
|
|
@@ -13,3 +22,34 @@
|
|
|
13
22
|
- Commit the changes: `git commit -m "Increment version to 0.1.8"`
|
|
14
23
|
- Push to the remote repository: `git push origin main`
|
|
15
24
|
- Update projects with the new version: `npm install --save @devvistatech/devvista-kit@latest && npx devvista-kit`
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Instructions to Set Up Environment VariablesGOOGLE_ANALYTICS_CLIENT_EMAIL & GOOGLE_ANALYTICS_PRIVATE_KEY Go to console.cloud.google.com.
|
|
28
|
+
Navigate to IAM & Admin > Service Accounts.
|
|
29
|
+
Click + Create Service Account, name it (e.g., "devvistatest"), and create.
|
|
30
|
+
Click the service account, go to Keys > Add Key > Create New Key, select JSON, and download.
|
|
31
|
+
Open the JSON file:client_email is the email (e.g., devvistatest@devvista-test.iam.gserviceaccount.com).
|
|
32
|
+
private_key is the key (e.g., -----BEGIN PRIVATE KEY-----\n[long string]\n-----END PRIVATE KEY-----), copy it exactly with all \n newlines.
|
|
33
|
+
|
|
34
|
+
GOOGLE_ANALYTICS_PROPERTY_ID
|
|
35
|
+
Go to analytics.google.com.
|
|
36
|
+
Select "Gabriel Labbate" account, then "DevVista" property.
|
|
37
|
+
Go to Admin > Property Settings > Property Details; note the Property ID (e.g., 479012946).
|
|
38
|
+
|
|
39
|
+
NEXT_PUBLIC_GA_MEASUREMENT_ID
|
|
40
|
+
In analytics.google.com, select "Gabriel Labbate" account and "DevVista" property.
|
|
41
|
+
Go to Admin > Data Streams, select your stream, and note the Measurement ID (e.g., G-VX3LV8FCS8).
|
|
42
|
+
|
|
43
|
+
Add Service Account to PermissionsIn analytics.google.com, select "Gabriel Labbate" account and "DevVista" property.
|
|
44
|
+
Go to Admin > Account Access Management.
|
|
45
|
+
Click +, enter devvistatest@devvista-test.iam.gserviceaccount.com, assign Viewer role, and click Add. Wait 5-10 minutes for propagation.
|
|
46
|
+
|
|
47
|
+
Save VariablesAdd to your .env file:
|
|
48
|
+
|
|
49
|
+
GOOGLE_ANALYTICS_CLIENT_EMAIL=devvistatest@devvista-test.iam.gserviceaccount.com
|
|
50
|
+
GOOGLE_ANALYTICS_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkikVXFFwjmA==\n-----END PRIVATE KEY----- //This is an example
|
|
51
|
+
GOOGLE_ANALYTICS_PROPERTY_ID=47934342 //This is an example
|
|
52
|
+
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-VX3SDFDF //This is an example
|
|
53
|
+
|
|
54
|
+
Troubleshooting StepsRestart Application: After saving the .env file, stop your Next.js app (e.g., Ctrl+C) and restart it (e.g., npm run dev). Clear the .next folder and rebuild if needed.
|
|
55
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { ThemeProvider } from "@/components/theme-provider";
|
|
5
|
+
import { AuthProvider } from "@/lib/auth/auth-context";
|
|
6
|
+
import Navbar from "@/components/navBars/navbar";
|
|
7
|
+
import Banner from "@/components/addOns/functional/banner/Banner";
|
|
8
|
+
import { Toaster } from "@/components/other/toaster";
|
|
9
|
+
import Footer from "@/components/footers/footer";
|
|
10
|
+
import IconBubble from "@/components/addOns/non-functional/IconBubble";
|
|
11
|
+
import { useAuth } from "@clerk/nextjs";
|
|
12
|
+
import Spinner from "@/components/addOns/non-functional/spinner";
|
|
13
|
+
|
|
14
|
+
export default function ClientLayout({ children }: { children: React.ReactNode }) {
|
|
15
|
+
const [hasError, setHasError] = useState(false);
|
|
16
|
+
const { isLoaded } = useAuth();
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
setHasError(false);
|
|
20
|
+
const handleError = (event: ErrorEvent) => {
|
|
21
|
+
console.error("Global error:", {
|
|
22
|
+
message: event.message,
|
|
23
|
+
filename: event.filename,
|
|
24
|
+
lineno: event.lineno,
|
|
25
|
+
colno: event.colno,
|
|
26
|
+
error: event.error,
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
window.addEventListener("error", handleError);
|
|
30
|
+
return () => window.removeEventListener("error", handleError);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
if (!isLoaded) {
|
|
34
|
+
console.log("ClientLayout: Waiting for auth to load");
|
|
35
|
+
return <Spinner />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (hasError) {
|
|
39
|
+
console.log("ClientLayout: Rendering error state");
|
|
40
|
+
return (
|
|
41
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-background text-foreground">
|
|
42
|
+
<h1 className="text-2xl font-bold">Something went wrong</h1>
|
|
43
|
+
<p className="mt-2 text-gray-600">Please refresh the page or try again later.</p>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log("ClientLayout: Rendering main layout");
|
|
49
|
+
return (
|
|
50
|
+
<ThemeProvider
|
|
51
|
+
attribute="class"
|
|
52
|
+
defaultTheme="dark"
|
|
53
|
+
enableSystem={false}
|
|
54
|
+
disableTransitionOnChange
|
|
55
|
+
>
|
|
56
|
+
<AuthProvider>
|
|
57
|
+
<Navbar />
|
|
58
|
+
<Banner />
|
|
59
|
+
<main className="min-h-screen w-full">{children}</main>
|
|
60
|
+
<Toaster />
|
|
61
|
+
<Footer />
|
|
62
|
+
<IconBubble />
|
|
63
|
+
</AuthProvider>
|
|
64
|
+
</ThemeProvider>
|
|
65
|
+
);
|
|
66
|
+
}
|
package/app/about/page.tsx
CHANGED
|
@@ -1,48 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { motion } from "framer-motion";
|
|
4
|
-
import {
|
|
5
|
-
import { BioSection } from "@/components/addOns/functional/BioEditor";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { BioSection } from "@/components/addOns/functional/bioSections/BioEditor";
|
|
6
6
|
import Spinner from "@/components/addOns/non-functional/spinner";
|
|
7
|
-
import { useStrapiAuth } from "@/lib/auth-context";
|
|
8
|
-
import {
|
|
7
|
+
import { useStrapiAuth } from "@/lib/auth/auth-context";
|
|
8
|
+
import { useUser } from "@clerk/nextjs";
|
|
9
9
|
import Testimonials from "@/components/addOns/non-functional/Testimonials";
|
|
10
10
|
import { ABOUT_PAGE } from "../../lib/constants/about";
|
|
11
11
|
|
|
12
|
-
interface BioContent {
|
|
13
|
-
id: number;
|
|
14
|
-
documentId: string;
|
|
15
|
-
title: string;
|
|
16
|
-
description: string;
|
|
17
|
-
bio: boolean;
|
|
18
|
-
about: boolean;
|
|
19
|
-
image?: { url: string; id: number };
|
|
20
|
-
createdAt: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
12
|
export default function AboutPage() {
|
|
24
|
-
const { user, authLoading
|
|
25
|
-
const {
|
|
26
|
-
const { user: clerkUser, isSignedIn } = useUser();
|
|
13
|
+
const { user, authLoading } = useStrapiAuth();
|
|
14
|
+
const { isSignedIn } = useUser();
|
|
27
15
|
const [isMobile, setIsMobile] = useState(false);
|
|
28
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
29
|
-
const [error, setError] = useState<string | null>(null);
|
|
30
|
-
const [bioImageUrl, setBioImageUrl] = useState<string | null>(null);
|
|
31
|
-
const [bioText, setBioText] = useState<string | null>(null);
|
|
32
|
-
const [bioTitle, setBioTitle] = useState<string | null>(null);
|
|
33
|
-
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
34
16
|
|
|
35
17
|
useEffect(() => {
|
|
36
18
|
window.scrollTo(0, 0);
|
|
37
19
|
}, []);
|
|
38
20
|
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
const event = new CustomEvent("modalStateChange", {
|
|
41
|
-
detail: { isOpen: isEditModalOpen },
|
|
42
|
-
});
|
|
43
|
-
window.dispatchEvent(event);
|
|
44
|
-
}, [isEditModalOpen]);
|
|
45
|
-
|
|
46
21
|
useEffect(() => {
|
|
47
22
|
const debounce = (fn: () => void, delay: number) => {
|
|
48
23
|
let timeout: NodeJS.Timeout;
|
|
@@ -51,241 +26,29 @@ export default function AboutPage() {
|
|
|
51
26
|
timeout = setTimeout(fn, delay);
|
|
52
27
|
};
|
|
53
28
|
};
|
|
54
|
-
|
|
55
29
|
const checkMobile = debounce(() => {
|
|
56
30
|
setIsMobile(window.innerWidth < 768);
|
|
57
31
|
}, 100);
|
|
58
|
-
|
|
59
32
|
checkMobile();
|
|
60
33
|
window.addEventListener("resize", checkMobile);
|
|
61
34
|
return () => window.removeEventListener("resize", checkMobile);
|
|
62
35
|
}, []);
|
|
63
36
|
|
|
64
|
-
|
|
65
|
-
const handleLogin = () => checkSession();
|
|
66
|
-
const handleLogout = () => checkSession();
|
|
67
|
-
window.addEventListener("user-login", handleLogin);
|
|
68
|
-
window.removeEventListener("user-logout", handleLogout);
|
|
69
|
-
return () => {
|
|
70
|
-
window.removeEventListener("user-login", handleLogin);
|
|
71
|
-
window.removeEventListener("user-logout", handleLogout);
|
|
72
|
-
};
|
|
73
|
-
}, [checkSession]);
|
|
74
|
-
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
const fetchBioData = async () => {
|
|
77
|
-
setIsLoading(true);
|
|
78
|
-
try {
|
|
79
|
-
const response = await fetch("/api/bio", {
|
|
80
|
-
headers: { "Content-Type": "application/json" },
|
|
81
|
-
});
|
|
82
|
-
if (!response.ok) {
|
|
83
|
-
const errorData = await response.json();
|
|
84
|
-
throw new Error(errorData.error || ABOUT_PAGE.ERRORS.FETCH_BIO_FAILED);
|
|
85
|
-
}
|
|
86
|
-
const result = await response.json();
|
|
87
|
-
if (result.data) {
|
|
88
|
-
const bioData: BioContent = result.data;
|
|
89
|
-
setBioImageUrl(bioData.image?.url || null);
|
|
90
|
-
setBioText(bioData.description || null);
|
|
91
|
-
setBioTitle(bioData.title || null);
|
|
92
|
-
} else {
|
|
93
|
-
throw new Error(ABOUT_PAGE.ERRORS.BIO_NOT_FOUND);
|
|
94
|
-
}
|
|
95
|
-
} catch (err) {
|
|
96
|
-
console.error("Fetch Error:", err);
|
|
97
|
-
setError(err instanceof Error ? err.message : ABOUT_PAGE.ERRORS.FETCH_BIO_ERROR);
|
|
98
|
-
} finally {
|
|
99
|
-
setIsLoading(false);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
fetchBioData();
|
|
103
|
-
}, []);
|
|
104
|
-
|
|
105
|
-
const handlePatchSubmit = async (
|
|
106
|
-
e: React.FormEvent,
|
|
107
|
-
formTitle: string,
|
|
108
|
-
formDescription: string,
|
|
109
|
-
formImage: File | null,
|
|
110
|
-
onSuccess: () => void
|
|
111
|
-
) => {
|
|
112
|
-
e.preventDefault();
|
|
113
|
-
if (!user?.businessAdminId) {
|
|
114
|
-
console.error("No user or businessAdminId:", { user, businessAdminId: user?.businessAdminId });
|
|
115
|
-
setError(ABOUT_PAGE.ERRORS.UNAUTHORIZED_UPDATE);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const formData = new FormData();
|
|
121
|
-
if (formTitle && formTitle !== bioTitle) {
|
|
122
|
-
formData.append("title", formTitle);
|
|
123
|
-
}
|
|
124
|
-
if (formDescription && formDescription !== bioText) {
|
|
125
|
-
formData.append("description", formDescription);
|
|
126
|
-
}
|
|
127
|
-
if (formImage) {
|
|
128
|
-
if (!["image/jpeg", "image/png", "image/gif"].includes(formImage.type)) {
|
|
129
|
-
throw new Error(ABOUT_PAGE.ERRORS.INVALID_IMAGE_TYPE);
|
|
130
|
-
}
|
|
131
|
-
formData.append("image", formImage);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!formData.has("title") && !formData.has("description") && !formData.has("image")) {
|
|
135
|
-
onSuccess();
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const token = await getToken();
|
|
140
|
-
if (!token) {
|
|
141
|
-
throw new Error(ABOUT_PAGE.ERRORS.NO_AUTH_TOKEN);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const response = await fetch("/api/bio", {
|
|
145
|
-
method: "PUT",
|
|
146
|
-
headers: {
|
|
147
|
-
Authorization: `Bearer ${token}`,
|
|
148
|
-
},
|
|
149
|
-
body: formData,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const result = await response.json();
|
|
153
|
-
if (!response.ok) {
|
|
154
|
-
throw new Error(result.error || ABOUT_PAGE.ERRORS.UPDATE_BIO_FAILED);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (result.data) {
|
|
158
|
-
const bioData: BioContent = result.data;
|
|
159
|
-
setBioImageUrl(bioData.image?.url || null);
|
|
160
|
-
setBioText(bioData.description || null);
|
|
161
|
-
setBioTitle(bioData.title || null);
|
|
162
|
-
setError(null);
|
|
163
|
-
onSuccess();
|
|
164
|
-
}
|
|
165
|
-
} catch (err) {
|
|
166
|
-
console.error("Update Error:", err);
|
|
167
|
-
setError(err instanceof Error ? err.message : ABOUT_PAGE.ERRORS.UPDATE_BIO_ERROR);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const sectionVariants = {
|
|
172
|
-
hidden: { opacity: 0, y: 20 },
|
|
173
|
-
visible: {
|
|
174
|
-
opacity: 1,
|
|
175
|
-
y: 0,
|
|
176
|
-
transition: { duration: isMobile ? 0.3 : 0.5, ease: "easeOut" },
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const cardVariants = {
|
|
181
|
-
hidden: { opacity: 0, y: 20 },
|
|
182
|
-
visible: {
|
|
183
|
-
opacity: 1,
|
|
184
|
-
y: 0,
|
|
185
|
-
transition: { duration: isMobile ? 0.3 : 0.5, ease: "easeOut" },
|
|
186
|
-
},
|
|
187
|
-
hover: {
|
|
188
|
-
scale: isMobile ? 1 : 1.05,
|
|
189
|
-
transition: { duration: 0.2, ease: "easeOut" },
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const itemVariants = {
|
|
194
|
-
hidden: { opacity: 0, y: 10 },
|
|
195
|
-
visible: {
|
|
196
|
-
opacity: 1,
|
|
197
|
-
y: 0,
|
|
198
|
-
transition: { duration: isMobile ? 0.3 : 0.5, ease: "easeOut" },
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
if (authLoading || isLoading) {
|
|
37
|
+
if (authLoading) {
|
|
203
38
|
return <Spinner />;
|
|
204
39
|
}
|
|
205
40
|
|
|
206
41
|
return (
|
|
207
|
-
<div className="min-h-screen w-full bg-
|
|
208
|
-
<
|
|
209
|
-
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&display=swap");
|
|
210
|
-
|
|
211
|
-
:root {
|
|
212
|
-
--jubilee: #F47C7C;
|
|
213
|
-
--natural-white: #F7F7F7;
|
|
214
|
-
--caviar: #2D2D2D;
|
|
215
|
-
--storm-cloud: #4A636E;
|
|
216
|
-
--exuberant-blue: #FF69B4;
|
|
217
|
-
--modern-purple: #D946EF;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
* {
|
|
221
|
-
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
.gradient-text {
|
|
225
|
-
background: linear-gradient(135deg, var(--exuberant-blue), var(--jubilee), var(--modern-purple));
|
|
226
|
-
-webkit-background-clip: text;
|
|
227
|
-
background-clip: text;
|
|
228
|
-
color: transparent;
|
|
229
|
-
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.glassmorphism {
|
|
233
|
-
background: rgba(255, 255, 255, 0.1);
|
|
234
|
-
backdrop-filter: blur(12px);
|
|
235
|
-
-webkit-backdrop-filter: blur(12px);
|
|
236
|
-
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
237
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
|
238
|
-
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
@supports not (backdrop-filter: blur(12px)) {
|
|
242
|
-
.glassmorphism {
|
|
243
|
-
background: rgba(255, 255, 255, 0.3);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.glassmorphism:hover {
|
|
248
|
-
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
@media (max-width: 768px) {
|
|
252
|
-
.glassmorphism {
|
|
253
|
-
padding: 1rem;
|
|
254
|
-
}
|
|
255
|
-
.gradient-text {
|
|
256
|
-
font-size: 2.5rem;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.line-clamp-8 {
|
|
261
|
-
display: -webkit-box;
|
|
262
|
-
-webkit-line-clamp: 8;
|
|
263
|
-
-webkit-box-orient: vertical;
|
|
264
|
-
overflow: hidden;
|
|
265
|
-
}
|
|
266
|
-
`}</style>
|
|
267
|
-
<div className="w-full pt-20 lg:pt-40 flex-grow relative z-10 container-padding bg-white">
|
|
42
|
+
<div className="min-h-screen w-full bg-natural-white relative flex flex-col font-inter">
|
|
43
|
+
<div className="w-full pt-20 lg:pt-40 flex-grow relative z-10 px-4 sm:px-6 lg:px-8 bg-white">
|
|
268
44
|
<motion.h1
|
|
269
45
|
initial={{ opacity: 0, y: -50 }}
|
|
270
46
|
animate={{ opacity: 1, y: 0 }}
|
|
271
|
-
className="text-4xl sm:text-5xl lg:text-7xl font-black mb-20 lg:mb-20 gradient-text text-center uppercase drop-shadow-lg
|
|
47
|
+
className="text-4xl sm:text-5xl lg:text-7xl font-black mb-20 lg:mb-20 gradient-text text-center uppercase drop-shadow-lg pt-36 sm:pt-32"
|
|
272
48
|
>
|
|
273
49
|
{ABOUT_PAGE.UI.MAIN_HEADING}
|
|
274
50
|
</motion.h1>
|
|
275
|
-
<BioSection
|
|
276
|
-
user={user}
|
|
277
|
-
authLoading={authLoading}
|
|
278
|
-
bioImageUrl={bioImageUrl}
|
|
279
|
-
bioText={bioText}
|
|
280
|
-
bioTitle={bioTitle}
|
|
281
|
-
error={error}
|
|
282
|
-
isLoading={isLoading}
|
|
283
|
-
setBioImageUrl={setBioImageUrl}
|
|
284
|
-
setBioText={setBioText}
|
|
285
|
-
setBioTitle={setBioTitle}
|
|
286
|
-
setError={setError}
|
|
287
|
-
handlePatchSubmit={handlePatchSubmit}
|
|
288
|
-
/>
|
|
51
|
+
<BioSection user={user} isSignedIn={isSignedIn} />
|
|
289
52
|
<Testimonials isMobile={isMobile} />
|
|
290
53
|
<div className="px-20 py-52 sm:py-52 pt-32">
|
|
291
54
|
<h1 className="text-2xl sm:text-3xl font-extrabold pb-12 uppercase">
|
package/app/adRequest/page.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/other/card';
|
|
@@ -9,25 +9,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|
|
9
9
|
import { Textarea } from '@/components/other/textarea';
|
|
10
10
|
import { Badge } from '@/components/other/badge';
|
|
11
11
|
import { Separator } from '@/components/other/separator';
|
|
12
|
-
import { AlertCircle, Calendar, CreditCard,
|
|
13
|
-
import { Alert, AlertDescription } from '@/components/other/alert';
|
|
12
|
+
import { AlertCircle, Calendar, CreditCard, Heart, Send, Star } from 'lucide-react';
|
|
14
13
|
import { useAuth, useUser } from '@clerk/nextjs';
|
|
15
|
-
import { useStrapiAuth } from '@/lib/auth-context';
|
|
14
|
+
import { useStrapiAuth } from '@/lib/auth/auth-context';
|
|
15
|
+
import { isAdminUser } from '@/lib/auth/auth-utils';
|
|
16
16
|
import Spinner from '@/components/addOns/non-functional/spinner';
|
|
17
|
-
import { AD_REQUEST_PAGE } from '
|
|
18
|
-
|
|
19
|
-
interface AdRequest {
|
|
20
|
-
id: number;
|
|
21
|
-
documentId: string;
|
|
22
|
-
platform: string;
|
|
23
|
-
adType: string;
|
|
24
|
-
description: string;
|
|
25
|
-
timestamp: string;
|
|
26
|
-
userId: string;
|
|
27
|
-
credits: number;
|
|
28
|
-
lastResetDate: string;
|
|
29
|
-
nextResetDate: string;
|
|
30
|
-
}
|
|
17
|
+
import { AD_REQUEST_PAGE } from '@/lib/constants/adRequest';
|
|
18
|
+
import type { AdRequest } from '@/lib/types';
|
|
31
19
|
|
|
32
20
|
export default function AdRequest() {
|
|
33
21
|
const [creditsData, setCreditsData] = useState<{
|
|
@@ -48,11 +36,50 @@ export default function AdRequest() {
|
|
|
48
36
|
const [isLoading, setIsLoading] = useState(false);
|
|
49
37
|
const [error, setError] = useState<string | null>(null);
|
|
50
38
|
const [showConfirmPopup, setShowConfirmPopup] = useState(false);
|
|
39
|
+
const [isAdmin, setIsAdmin] = useState(false);
|
|
51
40
|
const { user, authLoading, checkSession } = useStrapiAuth();
|
|
52
|
-
const { getToken } = useAuth();
|
|
53
|
-
const {
|
|
41
|
+
const { getToken, isSignedIn } = useAuth();
|
|
42
|
+
const { user: clerkUser } = useUser();
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let isMounted = true;
|
|
46
|
+
const checkAdmin = async (retries = 3, delay = 1000) => {
|
|
47
|
+
if (!isSignedIn || !user?.authId) {
|
|
48
|
+
|
|
49
|
+
if (isMounted) setIsAdmin(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
54
|
+
try {
|
|
55
|
+
const adminStatus = await isAdminUser(isSignedIn, user);
|
|
56
|
+
if (isMounted) {
|
|
57
|
+
|
|
58
|
+
setIsAdmin(adminStatus);
|
|
59
|
+
setError(null);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("AdRequest: Admin check failed:", {
|
|
64
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
65
|
+
attempt,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
});
|
|
68
|
+
if (attempt < retries) {
|
|
69
|
+
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
71
|
+
} else if (isMounted) {
|
|
72
|
+
setIsAdmin(false);
|
|
73
|
+
setError(AD_REQUEST_PAGE.ERRORS.ADMIN_CHECK_FAILED);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
checkAdmin();
|
|
79
|
+
return () => {
|
|
80
|
+
isMounted = false;
|
|
81
|
+
};
|
|
82
|
+
}, [isSignedIn, user]);
|
|
56
83
|
|
|
57
84
|
useEffect(() => {
|
|
58
85
|
window.scrollTo(0, 0);
|
|
@@ -70,10 +97,16 @@ export default function AdRequest() {
|
|
|
70
97
|
}, [checkSession]);
|
|
71
98
|
|
|
72
99
|
const fetchData = async () => {
|
|
73
|
-
if (!isSignedIn || !clerkUser?.id)
|
|
100
|
+
if (!isSignedIn || !clerkUser?.id) {
|
|
101
|
+
setError(AD_REQUEST_PAGE.ERRORS.NOT_SIGNED_IN);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
74
104
|
setIsLoading(true);
|
|
75
105
|
try {
|
|
76
106
|
const token = await getToken();
|
|
107
|
+
if (!token) {
|
|
108
|
+
throw new Error(AD_REQUEST_PAGE.ERRORS.NO_AUTH_TOKEN);
|
|
109
|
+
}
|
|
77
110
|
const response = await fetch('/api/adRequest', {
|
|
78
111
|
headers: {
|
|
79
112
|
'Content-Type': 'application/json',
|
|
@@ -89,7 +122,7 @@ export default function AdRequest() {
|
|
|
89
122
|
setRequests(
|
|
90
123
|
data.adRequests.map((req: AdRequest) => ({
|
|
91
124
|
...req,
|
|
92
|
-
timestamp: new Date(req.timestamp),
|
|
125
|
+
timestamp: new Date(req.timestamp).toISOString(),
|
|
93
126
|
}))
|
|
94
127
|
);
|
|
95
128
|
setCreditsData({
|
|
@@ -98,6 +131,10 @@ export default function AdRequest() {
|
|
|
98
131
|
nextResetDate: data.credits.nextResetDate,
|
|
99
132
|
});
|
|
100
133
|
} catch (err) {
|
|
134
|
+
console.error("AdRequest: Fetch error:", {
|
|
135
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
});
|
|
101
138
|
setError(err instanceof Error ? err.message : AD_REQUEST_PAGE.ERRORS.UNKNOWN_ERROR);
|
|
102
139
|
} finally {
|
|
103
140
|
setIsLoading(false);
|
|
@@ -113,6 +150,14 @@ export default function AdRequest() {
|
|
|
113
150
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
114
151
|
e.preventDefault();
|
|
115
152
|
if (!isSignedIn) {
|
|
153
|
+
console.error("AdRequest: Unauthorized submit attempt", {
|
|
154
|
+
isSignedIn,
|
|
155
|
+
authId: user?.authId,
|
|
156
|
+
businessAdminId: user?.businessAdminId,
|
|
157
|
+
userRole: user?.userRole,
|
|
158
|
+
businessOwner: user?.businessOwner ?? null,
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
});
|
|
116
161
|
setError(AD_REQUEST_PAGE.ERRORS.NOT_SIGNED_IN);
|
|
117
162
|
return;
|
|
118
163
|
}
|
|
@@ -132,6 +177,9 @@ export default function AdRequest() {
|
|
|
132
177
|
setIsSubmitting(true);
|
|
133
178
|
try {
|
|
134
179
|
const token = await getToken();
|
|
180
|
+
if (!token) {
|
|
181
|
+
throw new Error(AD_REQUEST_PAGE.ERRORS.NO_AUTH_TOKEN);
|
|
182
|
+
}
|
|
135
183
|
const response = await fetch('/api/adRequest', {
|
|
136
184
|
method: 'POST',
|
|
137
185
|
headers: {
|
|
@@ -149,6 +197,11 @@ export default function AdRequest() {
|
|
|
149
197
|
|
|
150
198
|
if (!response.ok) {
|
|
151
199
|
const errorData = await response.json();
|
|
200
|
+
console.error("AdRequest: Submit failed", {
|
|
201
|
+
status: response.status,
|
|
202
|
+
errorData,
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
});
|
|
152
205
|
throw new Error(errorData.error || AD_REQUEST_PAGE.ERRORS.SUBMIT_AD_REQUEST_FAILED);
|
|
153
206
|
}
|
|
154
207
|
|
|
@@ -156,7 +209,7 @@ export default function AdRequest() {
|
|
|
156
209
|
setRequests(
|
|
157
210
|
data.adRequests.map((req: AdRequest) => ({
|
|
158
211
|
...req,
|
|
159
|
-
timestamp: new Date(req.timestamp),
|
|
212
|
+
timestamp: new Date(req.timestamp).toISOString(),
|
|
160
213
|
}))
|
|
161
214
|
);
|
|
162
215
|
setCreditsData({
|
|
@@ -170,6 +223,10 @@ export default function AdRequest() {
|
|
|
170
223
|
setShowSuccess(true);
|
|
171
224
|
setTimeout(() => setShowSuccess(false), 3000);
|
|
172
225
|
} catch (err) {
|
|
226
|
+
console.error("AdRequest: Submit error:", {
|
|
227
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
228
|
+
timestamp: new Date().toISOString(),
|
|
229
|
+
});
|
|
173
230
|
setError(err instanceof Error ? err.message : AD_REQUEST_PAGE.ERRORS.UNKNOWN_ERROR);
|
|
174
231
|
} finally {
|
|
175
232
|
setIsSubmitting(false);
|
|
@@ -182,6 +239,14 @@ export default function AdRequest() {
|
|
|
182
239
|
|
|
183
240
|
const resetCredits = async () => {
|
|
184
241
|
if (!isAdmin) {
|
|
242
|
+
console.error("AdRequest: Unauthorized reset attempt", {
|
|
243
|
+
isSignedIn,
|
|
244
|
+
authId: user?.authId,
|
|
245
|
+
businessAdminId: user?.businessAdminId,
|
|
246
|
+
userRole: user?.userRole,
|
|
247
|
+
businessOwner: user?.businessOwner ?? null,
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
});
|
|
185
250
|
setError(AD_REQUEST_PAGE.ERRORS.UNAUTHORIZED_RESET);
|
|
186
251
|
return;
|
|
187
252
|
}
|
|
@@ -189,6 +254,9 @@ export default function AdRequest() {
|
|
|
189
254
|
try {
|
|
190
255
|
setIsLoading(true);
|
|
191
256
|
const token = await getToken();
|
|
257
|
+
if (!token) {
|
|
258
|
+
throw new Error(AD_REQUEST_PAGE.ERRORS.NO_AUTH_TOKEN);
|
|
259
|
+
}
|
|
192
260
|
const response = await fetch('/api/adRequest', {
|
|
193
261
|
method: 'PUT',
|
|
194
262
|
headers: {
|
|
@@ -205,11 +273,20 @@ export default function AdRequest() {
|
|
|
205
273
|
|
|
206
274
|
if (!response.ok) {
|
|
207
275
|
const errorData = await response.json();
|
|
276
|
+
console.error("AdRequest: Reset credits failed", {
|
|
277
|
+
status: response.status,
|
|
278
|
+
errorData,
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
});
|
|
208
281
|
throw new Error(errorData.error || AD_REQUEST_PAGE.ERRORS.RESET_CREDITS_FAILED);
|
|
209
282
|
}
|
|
210
283
|
|
|
211
284
|
await fetchData();
|
|
212
285
|
} catch (err) {
|
|
286
|
+
console.error("AdRequest: Reset credits error:", {
|
|
287
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
288
|
+
timestamp: new Date().toISOString(),
|
|
289
|
+
});
|
|
213
290
|
setError(err instanceof Error ? err.message : AD_REQUEST_PAGE.ERRORS.UNKNOWN_ERROR);
|
|
214
291
|
} finally {
|
|
215
292
|
setIsLoading(false);
|
|
@@ -519,7 +596,6 @@ export default function AdRequest() {
|
|
|
519
596
|
>
|
|
520
597
|
{request.platform === 'facebook' ? AD_REQUEST_PAGE.UI.FACEBOOK_BADGE : AD_REQUEST_PAGE.UI.TIKTOK_BADGE}
|
|
521
598
|
</Badge>
|
|
522
|
-
|
|
523
599
|
<span className="text-xs text-gray-400">
|
|
524
600
|
{new Date(request.timestamp).toLocaleTimeString()}
|
|
525
601
|
</span>
|