@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,266 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { ProductDescCarousel } from "@/components/addOns/functional/carousels/ProductDescCarousel";
|
|
5
|
+
import Spinner from "@/components/addOns/non-functional/spinner";
|
|
6
|
+
import { useStrapiAuth } from "@/lib/auth/auth-context";
|
|
7
|
+
import { useAuth } from "@clerk/nextjs";
|
|
8
|
+
import ProductHero from "@/components/addOns/non-functional/heros/ProductHero";
|
|
9
|
+
import { StrapiUser, UploadedImage, Category } from "@/lib/types";
|
|
10
|
+
import { compressImage } from "@/lib/utils/compressImage";
|
|
11
|
+
import { PRODUCT_PAGE } from "../constants/product";
|
|
12
|
+
|
|
13
|
+
const PRODUCT_CATEGORIES: Category[] = [
|
|
14
|
+
"indoor",
|
|
15
|
+
"outdoor",
|
|
16
|
+
"commercial",
|
|
17
|
+
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export default function BaggedCementProducts() {
|
|
21
|
+
const { user, authLoading, checkSession } = useStrapiAuth();
|
|
22
|
+
const { isSignedIn, getToken } = useAuth();
|
|
23
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
|
|
26
|
+
const [galleryUser, setUser] = useState<StrapiUser | null>(null);
|
|
27
|
+
const [subCategories, setSubCategories] = useState<string[]>([]);
|
|
28
|
+
|
|
29
|
+
const isAdmin = isSignedIn && !!user?.businessAdminId;
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setUser(user);
|
|
33
|
+
}, [user, authLoading, isSignedIn]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
window.scrollTo(0, 0);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const handleLogin = () => checkSession();
|
|
41
|
+
const handleLogout = () => checkSession();
|
|
42
|
+
|
|
43
|
+
window.addEventListener("user-login", handleLogin);
|
|
44
|
+
window.addEventListener("user-logout", handleLogout);
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
window.removeEventListener("user-login", handleLogin);
|
|
48
|
+
window.removeEventListener("user-logout", handleLogout);
|
|
49
|
+
};
|
|
50
|
+
}, [checkSession]);
|
|
51
|
+
|
|
52
|
+
const fetchInitialData = useCallback(async () => {
|
|
53
|
+
try {
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
|
|
56
|
+
const headers: HeadersInit = { "Content-Type": "application/json" };
|
|
57
|
+
if (isSignedIn) {
|
|
58
|
+
const token = await getToken();
|
|
59
|
+
if (!token) {
|
|
60
|
+
throw new Error(PRODUCT_PAGE.ERRORS.NO_AUTH_TOKEN);
|
|
61
|
+
}
|
|
62
|
+
headers.Authorization = `Bearer ${token}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const response = await fetch("/api/gallery-data", { headers });
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorText = await response.text();
|
|
68
|
+
console.error("Gallery fetch failed:", { status: response.status, errorText });
|
|
69
|
+
throw new Error(PRODUCT_PAGE.ERRORS.FETCH_IMAGES_FAILED.replace("${errorText}", errorText));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { data: images, meta }: { data: UploadedImage[]; meta?: { pagination?: { total: number } } } = await response.json();
|
|
73
|
+
|
|
74
|
+
if (meta && meta.pagination && meta.pagination.total > 100) {
|
|
75
|
+
setError(PRODUCT_PAGE.ERRORS.PAGINATION_WARNING);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setUploadedImages(images || []);
|
|
79
|
+
const subCats: string[] = images
|
|
80
|
+
.filter((img) => img.category === "indoor" && typeof img.subCategory === "string" && img.subCategory !== "")
|
|
81
|
+
.reduce<string[]>((acc, img) => {
|
|
82
|
+
const subCategory = img.subCategory as string;
|
|
83
|
+
return acc.includes(subCategory) ? acc : [...acc, subCategory];
|
|
84
|
+
}, []);
|
|
85
|
+
setSubCategories(subCats);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error("Fetch error details:", err);
|
|
88
|
+
setError(err instanceof Error ? err.message : PRODUCT_PAGE.ERRORS.FETCH_ERROR);
|
|
89
|
+
} finally {
|
|
90
|
+
setIsLoading(false);
|
|
91
|
+
}
|
|
92
|
+
}, [isSignedIn, getToken]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
fetchInitialData();
|
|
96
|
+
}, [fetchInitialData]);
|
|
97
|
+
|
|
98
|
+
const handleImageUpload = async (
|
|
99
|
+
e: React.FormEvent<HTMLFormElement>,
|
|
100
|
+
file: File | null,
|
|
101
|
+
title: string,
|
|
102
|
+
description: string,
|
|
103
|
+
category: Category,
|
|
104
|
+
subCategory: string
|
|
105
|
+
) => {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
|
|
108
|
+
if (!file) {
|
|
109
|
+
setError(PRODUCT_PAGE.ERRORS.NO_FILE_SELECTED);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!["image/jpeg", "image/png", "image/gif"].includes(file.type)) {
|
|
114
|
+
setError(PRODUCT_PAGE.ERRORS.INVALID_FILE_TYPE);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!isAdmin) {
|
|
119
|
+
setError(PRODUCT_PAGE.ERRORS.UNAUTHORIZED_UPLOAD);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!subCategory) {
|
|
124
|
+
setError("Subcategory is required");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
setIsLoading(true);
|
|
130
|
+
|
|
131
|
+
const token = await getToken();
|
|
132
|
+
if (!token) {
|
|
133
|
+
setError(PRODUCT_PAGE.ERRORS.AUTHENTICATION_ERROR);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const compressedFile = await compressImage(file);
|
|
138
|
+
|
|
139
|
+
const formData = new FormData();
|
|
140
|
+
formData.append("file", compressedFile);
|
|
141
|
+
formData.append("title", title || `Image ${new Date().toISOString()}`);
|
|
142
|
+
formData.append("description", description || "");
|
|
143
|
+
formData.append("category", category || "bagged-cement-products");
|
|
144
|
+
formData.append("subCategory", subCategory);
|
|
145
|
+
|
|
146
|
+
const response = await fetch("/api/gallery-data", {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
149
|
+
body: formData,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
const errorData = await response.json();
|
|
154
|
+
console.error("Upload error response:", errorData);
|
|
155
|
+
if (response.status === 401) {
|
|
156
|
+
setError(PRODUCT_PAGE.ERRORS.AUTHENTICATION_ERROR);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
throw new Error(PRODUCT_PAGE.ERRORS.UPLOAD_IMAGE_FAILED.replace("${response.status}", response.status.toString()));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { data }: { data: UploadedImage[] } = await response.json();
|
|
163
|
+
setUploadedImages(data || []);
|
|
164
|
+
if (subCategory && !subCategories.includes(subCategory)) {
|
|
165
|
+
setSubCategories([...subCategories, subCategory]);
|
|
166
|
+
}
|
|
167
|
+
setError(null);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error("Upload error details:", err);
|
|
170
|
+
setError(err instanceof Error ? err.message : PRODUCT_PAGE.ERRORS.UPLOAD_IMAGE_ERROR);
|
|
171
|
+
} finally {
|
|
172
|
+
setIsLoading(false);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleDeleteImage = async (documentId: string) => {
|
|
177
|
+
if (!isAdmin) {
|
|
178
|
+
setError(PRODUCT_PAGE.ERRORS.UNAUTHORIZED_DELETE);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
setIsLoading(true);
|
|
184
|
+
|
|
185
|
+
const token = await getToken();
|
|
186
|
+
if (!token) {
|
|
187
|
+
setError(PRODUCT_PAGE.ERRORS.AUTHENTICATION_ERROR);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const response = await fetch("/api/gallery-data", {
|
|
192
|
+
method: "DELETE",
|
|
193
|
+
headers: {
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
Authorization: `Bearer ${token}`,
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({ documentId }),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
const errorData = await response.json();
|
|
202
|
+
console.error("Delete error response:", errorData);
|
|
203
|
+
if (response.status === 401) {
|
|
204
|
+
setError(PRODUCT_PAGE.ERRORS.AUTHENTICATION_ERROR);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
throw new Error(PRODUCT_PAGE.ERRORS.DELETE_IMAGE_FAILED.replace("${response.status}", response.status.toString()));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { data }: { data: UploadedImage[] } = await response.json();
|
|
211
|
+
setUploadedImages(data || []);
|
|
212
|
+
const subCats: string[] = data
|
|
213
|
+
.filter((img) => img.category === "indoor" && typeof img.subCategory === "string" && img.subCategory !== "")
|
|
214
|
+
.reduce<string[]>((acc, img) => {
|
|
215
|
+
const subCategory = img.subCategory as string;
|
|
216
|
+
return acc.includes(subCategory) ? acc : [...acc, subCategory];
|
|
217
|
+
}, []);
|
|
218
|
+
setSubCategories(subCats);
|
|
219
|
+
setError(null);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error("Delete error details:", err);
|
|
222
|
+
setError(err instanceof Error ? err.message : PRODUCT_PAGE.ERRORS.DELETE_IMAGE_ERROR);
|
|
223
|
+
} finally {
|
|
224
|
+
setIsLoading(false);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if (isLoading || authLoading) {
|
|
229
|
+
return <Spinner />;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const imageSrc = uploadedImages[0]?.url ?? "/placeholder.jpg";
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className="min-h-screen w-full bg-white font-sans">
|
|
236
|
+
<div className="w-full pt-20 lg:pt-40 pb-16 relative z-10 container-padding">
|
|
237
|
+
{error && (
|
|
238
|
+
<div className="text-red-600 text-center p-4 mb-4 max-w-4xl mx-auto bg-red-50 border border-red-200 rounded-lg backdrop-blur-sm">
|
|
239
|
+
{error}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
<ProductHero />
|
|
244
|
+
|
|
245
|
+
<div className="mb-8">
|
|
246
|
+
<ProductDescCarousel
|
|
247
|
+
user={galleryUser}
|
|
248
|
+
uploadedImages={uploadedImages}
|
|
249
|
+
setUploadedImages={setUploadedImages}
|
|
250
|
+
error={error}
|
|
251
|
+
setError={setError}
|
|
252
|
+
isLoading={isLoading}
|
|
253
|
+
setIsLoading={setIsLoading}
|
|
254
|
+
handleImageUpload={handleImageUpload}
|
|
255
|
+
handleDeleteImage={handleDeleteImage}
|
|
256
|
+
visibleCategories={["indoor"]}
|
|
257
|
+
fixedCategory="indoor"
|
|
258
|
+
visibleSubCategories={subCategories}
|
|
259
|
+
setVisibleSubCategories={setSubCategories}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { ProductList } from "@/components/addOns/functional/ProductList";
|
|
5
|
+
import Spinner from "@/components/addOns/non-functional/spinner";
|
|
6
|
+
import { useStrapiAuth } from "@/lib/auth/auth-context";
|
|
7
|
+
import { useAuth } from "@clerk/nextjs";
|
|
8
|
+
import { PRODUCT_PAGE } from "../constants/product";
|
|
9
|
+
import { StrapiUser, UploadedImage, Category } from "@/lib/types";
|
|
10
|
+
import { compressImage } from "@/lib/utils/compressImage";
|
|
11
|
+
import { ArrowLeft } from 'lucide-react';
|
|
12
|
+
import Link from "next/link";
|
|
13
|
+
import ProductHero from "@/components/addOns/non-functional/heros/ProductHero";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export default function SolidSurfaceProducts() {
|
|
17
|
+
const { user, authLoading, checkSession } = useStrapiAuth();
|
|
18
|
+
const { isSignedIn, getToken } = useAuth();
|
|
19
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
|
|
22
|
+
const [galleryUser, setUser] = useState<StrapiUser | null>(null);
|
|
23
|
+
const [subCategories, setSubCategories] = useState<string[]>([]);
|
|
24
|
+
|
|
25
|
+
const isAdmin = isSignedIn && !!user?.businessAdminId;
|
|
26
|
+
const category: Category = "indoor";
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
setUser(user);
|
|
30
|
+
}, [user, authLoading, isSignedIn]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
window.scrollTo(0, 0);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const handleLogin = () => checkSession();
|
|
38
|
+
const handleLogout = () => checkSession();
|
|
39
|
+
|
|
40
|
+
window.addEventListener("user-login", handleLogin);
|
|
41
|
+
window.addEventListener("user-logout", handleLogout);
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener("user-login", handleLogin);
|
|
45
|
+
window.removeEventListener("user-logout", handleLogout);
|
|
46
|
+
};
|
|
47
|
+
}, [checkSession]);
|
|
48
|
+
|
|
49
|
+
const fetchInitialData = useCallback(async () => {
|
|
50
|
+
try {
|
|
51
|
+
setIsLoading(true);
|
|
52
|
+
|
|
53
|
+
const headers: HeadersInit = { "Content-Type": "application/json" };
|
|
54
|
+
if (isSignedIn) {
|
|
55
|
+
const token = await getToken();
|
|
56
|
+
if (!token) {
|
|
57
|
+
throw new Error(PRODUCT_PAGE.ERRORS.NO_AUTH_TOKEN);
|
|
58
|
+
}
|
|
59
|
+
headers.Authorization = `Bearer ${token}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const response = await fetch("/api/gallery-data", { headers });
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const errorText = await response.text();
|
|
65
|
+
console.error("Gallery fetch failed:", { status: response.status, errorText });
|
|
66
|
+
throw new Error(PRODUCT_PAGE.ERRORS.FETCH_IMAGES_FAILED.replace("${errorText}", errorText));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { data: images, meta }: { data: UploadedImage[]; meta?: { pagination?: { total: number } } } = await response.json();
|
|
70
|
+
|
|
71
|
+
if (meta && meta.pagination && meta.pagination.total > 100) {
|
|
72
|
+
setError(PRODUCT_PAGE.ERRORS.PAGINATION_WARNING);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setUploadedImages(images || []);
|
|
76
|
+
const subCats: string[] = images
|
|
77
|
+
.filter((img) => img.category === category && typeof img.subCategory === "string" && img.subCategory !== "")
|
|
78
|
+
.reduce<string[]>((acc, img) => {
|
|
79
|
+
const subCategory = img.subCategory as string;
|
|
80
|
+
return acc.includes(subCategory) ? acc : [...acc, subCategory];
|
|
81
|
+
}, []);
|
|
82
|
+
setSubCategories(subCats);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error("Fetch error details:", err);
|
|
85
|
+
setError(err instanceof Error ? err.message : PRODUCT_PAGE.ERRORS.FETCH_ERROR);
|
|
86
|
+
} finally {
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
}
|
|
89
|
+
}, [isSignedIn, getToken, category]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
fetchInitialData();
|
|
93
|
+
}, [fetchInitialData]);
|
|
94
|
+
|
|
95
|
+
const handleImageUpload = async (
|
|
96
|
+
e: React.FormEvent<HTMLFormElement>,
|
|
97
|
+
file: File | null,
|
|
98
|
+
title: string,
|
|
99
|
+
description: string,
|
|
100
|
+
category: Category,
|
|
101
|
+
subCategory: string
|
|
102
|
+
) => {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
|
|
105
|
+
if (!file) {
|
|
106
|
+
setError(PRODUCT_PAGE.ERRORS.NO_FILE_SELECTED);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!["image/jpeg", "image/png", "image/gif"].includes(file.type)) {
|
|
111
|
+
setError(PRODUCT_PAGE.ERRORS.INVALID_FILE_TYPE);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isAdmin) {
|
|
116
|
+
setError(PRODUCT_PAGE.ERRORS.UNAUTHORIZED_UPLOAD);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!subCategory) {
|
|
121
|
+
setError("Subcategory is required");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
setIsLoading(true);
|
|
127
|
+
|
|
128
|
+
const token = await getToken();
|
|
129
|
+
if (!token) {
|
|
130
|
+
setError(PRODUCT_PAGE.ERRORS.AUTHENTICATION_ERROR);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const compressedFile = await compressImage(file);
|
|
135
|
+
|
|
136
|
+
const formData = new FormData();
|
|
137
|
+
formData.append("file", compressedFile);
|
|
138
|
+
formData.append("title", title || `Image ${new Date().toISOString()}`);
|
|
139
|
+
formData.append("description", description || "");
|
|
140
|
+
formData.append("category", category);
|
|
141
|
+
formData.append("subCategory", subCategory);
|
|
142
|
+
|
|
143
|
+
const response = await fetch("/api/gallery-data", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
146
|
+
body: formData,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const errorData = await response.json();
|
|
151
|
+
console.error("Upload error response:", errorData);
|
|
152
|
+
if (response.status === 401) {
|
|
153
|
+
setError(PRODUCT_PAGE.ERRORS.AUTHENTICATION_ERROR);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
throw new Error(PRODUCT_PAGE.ERRORS.UPLOAD_IMAGE_FAILED.replace("${response.status}", response.status.toString()));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { data }: { data: UploadedImage[] } = await response.json();
|
|
160
|
+
setUploadedImages(data || []);
|
|
161
|
+
if (subCategory && !subCategories.includes(subCategory)) {
|
|
162
|
+
setSubCategories([...subCategories, subCategory]);
|
|
163
|
+
}
|
|
164
|
+
setError(null);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error("Upload error details:", err);
|
|
167
|
+
setError(err instanceof Error ? err.message : PRODUCT_PAGE.ERRORS.UPLOAD_IMAGE_ERROR);
|
|
168
|
+
} finally {
|
|
169
|
+
setIsLoading(false);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleDeleteImage = async (documentId: string) => {
|
|
174
|
+
if (!isAdmin) {
|
|
175
|
+
setError(PRODUCT_PAGE.ERRORS.UNAUTHORIZED_DELETE);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
setIsLoading(true);
|
|
181
|
+
|
|
182
|
+
const token = await getToken();
|
|
183
|
+
if (!token) {
|
|
184
|
+
setError(PRODUCT_PAGE.ERRORS.AUTHENTICATION_ERROR);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const response = await fetch("/api/gallery-data", {
|
|
189
|
+
method: "DELETE",
|
|
190
|
+
headers: {
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
Authorization: `Bearer ${token}`,
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({ documentId }),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const errorData = await response.json();
|
|
199
|
+
console.error("Delete error response:", errorData);
|
|
200
|
+
if (response.status === 401) {
|
|
201
|
+
setError(PRODUCT_PAGE.ERRORS.AUTHENTICATION_ERROR);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
throw new Error(PRODUCT_PAGE.ERRORS.DELETE_IMAGE_FAILED.replace("${response.status}", response.status.toString()));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { data }: { data: UploadedImage[] } = await response.json();
|
|
208
|
+
|
|
209
|
+
setUploadedImages(data || []);
|
|
210
|
+
const subCats: string[] = data
|
|
211
|
+
.filter((img) => img.category === category && typeof img.subCategory === "string" && img.subCategory !== "")
|
|
212
|
+
.reduce<string[]>((acc, img) => {
|
|
213
|
+
const subCategory = img.subCategory as string;
|
|
214
|
+
return acc.includes(subCategory) ? acc : [...acc, subCategory];
|
|
215
|
+
}, []);
|
|
216
|
+
setSubCategories(subCats);
|
|
217
|
+
setError(null);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error("Delete error details:", err);
|
|
220
|
+
setError(err instanceof Error ? err.message : PRODUCT_PAGE.ERRORS.DELETE_IMAGE_ERROR);
|
|
221
|
+
} finally {
|
|
222
|
+
setIsLoading(false);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (isLoading || authLoading) {
|
|
227
|
+
return <Spinner />;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const imageSrc = uploadedImages[0]?.url ?? "/placeholder.jpg";
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div className="min-h-screen w-full bg-white font-sans">
|
|
234
|
+
<div className="w-full pt-20 lg:pt-40 pb-16 relative z-10 container-padding">
|
|
235
|
+
{error && (
|
|
236
|
+
<div className="text-red-600 text-center p-4 mb-4 max-w-4xl mx-auto bg-red-50 border border-red-200 rounded-lg backdrop-blur-sm">
|
|
237
|
+
{error}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
<ProductHero />
|
|
242
|
+
|
|
243
|
+
<div className="mb-8">
|
|
244
|
+
<ProductList
|
|
245
|
+
user={galleryUser}
|
|
246
|
+
uploadedImages={uploadedImages}
|
|
247
|
+
setUploadedImages={setUploadedImages}
|
|
248
|
+
error={error}
|
|
249
|
+
setError={setError}
|
|
250
|
+
isLoading={isLoading}
|
|
251
|
+
setIsLoading={setIsLoading}
|
|
252
|
+
handleImageUpload={handleImageUpload}
|
|
253
|
+
handleDeleteImage={handleDeleteImage}
|
|
254
|
+
visibleCategories={[category]}
|
|
255
|
+
category={category}
|
|
256
|
+
visibleSubCategories={subCategories}
|
|
257
|
+
setVisibleSubCategories={setSubCategories}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="text-center mt-8">
|
|
261
|
+
<Link
|
|
262
|
+
href="/gallery"
|
|
263
|
+
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-600 text-white font-semibold rounded-lg hover:bg-gray-700 transition-colors"
|
|
264
|
+
>
|
|
265
|
+
<ArrowLeft className="w-5 h-5" />
|
|
266
|
+
Back to Services
|
|
267
|
+
</Link>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|