From 61b4680c6cb82ea35a3c11a576320a304413c894 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 13 Aug 2026 15:34:26 +1200 Subject: [PATCH 01/14] feat:some comments for router.post --- client/package.json | 2 +- pnpm-lock.yaml | 34 ++++++++++++++++++++++-------- server/package.json | 1 + server/src/routes/admin/uploads.ts | 21 ++++++++++++++++++ server/src/schemas/upload.ts | 7 ++++++ server/src/utils/cloudinary.ts | 19 +++++++++++++++++ 6 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 server/src/routes/admin/uploads.ts create mode 100644 server/src/schemas/upload.ts create mode 100644 server/src/utils/cloudinary.ts diff --git a/client/package.json b/client/package.json index 066f94a..5de1cb4 100644 --- a/client/package.json +++ b/client/package.json @@ -15,7 +15,7 @@ "@hookform/resolvers": "^5.4.0", "@tanstack/react-query": "^5.101.2", "axios": "^1.18.1", - "motion": "^12.38.0", + "motion": "^12.42.2", "react": "^19.2.4", "react-dom": "^19.2.4", "react-hook-form": "^7.80.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 092bbc3..392ff5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: specifier: ^1.18.1 version: 1.18.1 motion: - specifier: ^12.38.0 + specifier: ^12.42.2 version: 12.42.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: specifier: ^19.2.4 @@ -139,6 +139,9 @@ importers: bcryptjs: specifier: ^3.0.3 version: 3.0.3 + cloudinary: + specifier: ^2.10.0 + version: 2.10.0 cookie-parser: specifier: ^1.4.7 version: 1.4.7 @@ -1060,6 +1063,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + cloudinary@2.10.0: + resolution: {integrity: sha512-sY09kYg7wprkndAOjZBAYqFZqwL+SxnEGcAvksOvFA+5upnFn949UjkEkHKNSwkBtW/xRDd0p6NgbSXZcxkI3w==} + engines: {node: '>=9'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1395,8 +1402,8 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.42.2: - resolution: {integrity: sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==} + framer-motion@12.43.0: + resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -1837,6 +1844,9 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1923,8 +1933,8 @@ packages: resolution: {integrity: sha512-4rFBWa+/wdBQSfvnOPJBpiSG6UCEbhSQh865dEdaH9Y8WfHBUC+I2XT28dp0IBIGrEwmh+gzrgZgea5PbmrHWA==} engines: {node: '>=20.19.0'} - motion-dom@12.42.2: - resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==} + motion-dom@12.43.0: + resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} motion-utils@12.39.0: resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} @@ -3413,6 +3423,10 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cloudinary@2.10.0: + dependencies: + lodash: 4.18.1 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3884,9 +3898,9 @@ snapshots: fraction.js@5.3.4: {} - framer-motion@12.42.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + framer-motion@12.43.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - motion-dom: 12.42.2 + motion-dom: 12.43.0 motion-utils: 12.39.0 tslib: 2.8.1 optionalDependencies: @@ -4292,6 +4306,8 @@ snapshots: lodash.once@4.1.1: {} + lodash@4.18.1: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -4362,7 +4378,7 @@ snapshots: - snappy - socks - motion-dom@12.42.2: + motion-dom@12.43.0: dependencies: motion-utils: 12.39.0 @@ -4370,7 +4386,7 @@ snapshots: motion@12.42.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - framer-motion: 12.42.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + framer-motion: 12.43.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tslib: 2.8.1 optionalDependencies: react: 19.2.5 diff --git a/server/package.json b/server/package.json index 03d13a8..915e36b 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ "type": "module", "dependencies": { "bcryptjs": "^3.0.3", + "cloudinary": "^2.10.0", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "express": "^5.2.1", diff --git a/server/src/routes/admin/uploads.ts b/server/src/routes/admin/uploads.ts new file mode 100644 index 0000000..a6ac6a2 --- /dev/null +++ b/server/src/routes/admin/uploads.ts @@ -0,0 +1,21 @@ +import { Router, Request, Response } from "express"; +import { sendError } from "../../utils/apiError.js"; +import { validate } from "../../middleware/validate.js"; +import { uploadSignatureSchema } from "../../schemas/upload.js"; + +const router = Router(); + +router.post("/signature", validate(uploadSignatureSchema), async (req: Request, res: Response) => { + try { + // get folder from req + // get all the info from .env + + // import signUpload & run + + // return inside of res folder etc. -> check content.ts + } catch { + sendError(res, 500, "INTERNAL_ERROR", "Missing Cloudinary env vars"); + } +}); + +export default router; diff --git a/server/src/schemas/upload.ts b/server/src/schemas/upload.ts new file mode 100644 index 0000000..0ab0e77 --- /dev/null +++ b/server/src/schemas/upload.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const uploadSignatureSchema = z.object({ + folder: z.enum(["umsa/gallery", "umsa/team", "umsa/events", "umsa/branding"]), + }); + + export type uploadSignatureInput = z.infer; \ No newline at end of file diff --git a/server/src/utils/cloudinary.ts b/server/src/utils/cloudinary.ts new file mode 100644 index 0000000..10e2452 --- /dev/null +++ b/server/src/utils/cloudinary.ts @@ -0,0 +1,19 @@ +import { v2 } from "cloudinary"; + +export const cloudinaryConfig = () => ({ + cloudName: process.env.CLOUDINARY_CLOUD_NAME || "", + apiKey: process.env.CLOUDINARY_API_KEY || "", + apiSecret: process.env.CLOUDINARY_API_SECRET || "", + }); + +export const signUpload = (folder:string) => { + const timeStamp = Math.round(Date.now() / 1000); + const { apiSecret } = cloudinaryConfig(); + const signature = v2.utils.api_sign_request({timeStamp, folder}, apiSecret); + + return {signature, timeStamp}; +}; + +export const destroyAsset = (publicId:string) => { + return v2.uploader.destroy(publicId); +}; \ No newline at end of file From 17153c84278082ce2af3c9a2d0945dfac8f52021 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 24 Aug 2026 17:06:55 +1200 Subject: [PATCH 02/14] fix: cloudinary.ts timestamp fix + detailed comments --- server/src/utils/cloudinary.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/server/src/utils/cloudinary.ts b/server/src/utils/cloudinary.ts index 10e2452..1122927 100644 --- a/server/src/utils/cloudinary.ts +++ b/server/src/utils/cloudinary.ts @@ -6,14 +6,28 @@ export const cloudinaryConfig = () => ({ apiSecret: process.env.CLOUDINARY_API_SECRET || "", }); +//generate signature when requested by the browser export const signUpload = (folder:string) => { - const timeStamp = Math.round(Date.now() / 1000); + const timestamp = Math.round(Date.now() / 1000); const { apiSecret } = cloudinaryConfig(); - const signature = v2.utils.api_sign_request({timeStamp, folder}, apiSecret); + //signature, current timestamp and the folder hashed using the apiSecret + const signature = v2.utils.api_sign_request({timestamp, folder}, apiSecret); - return {signature, timeStamp}; + return {signature, timestamp}; }; +// ^the admin picks image file and folder, browser sends the chosen folder to the express backend +// backend calls the signUpload function with the folder(string) +// the time of request becomes timestamp(integer) + folder(string), hashed using apiSecret +// this hashed string => the signature +// signature and timestamp is returned to the browser +// when the actual image file is sent to cloudinary, signature, timestamp, folder also sent +// cloudinary rehashes timestamp and folder using apiSecret(cloudinary already has) +// verify that sent and generated signature match, therefore nothing was tempered + export const destroyAsset = (publicId:string) => { return v2.uploader.destroy(publicId); -}; \ No newline at end of file +}; +// ^when destroyAsset is called with the publicId +// it passes that publicId straight to destroy function from v2 cloudinary +// let cloudinary know what to delete \ No newline at end of file From 85a7e80992c5b51b31fd49d3a3d10a0872d598f3 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 24 Aug 2026 17:37:18 +1200 Subject: [PATCH 03/14] feat: schema/upload.ts detailed comments --- server/src/schemas/upload.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/schemas/upload.ts b/server/src/schemas/upload.ts index 0ab0e77..21a1e11 100644 --- a/server/src/schemas/upload.ts +++ b/server/src/schemas/upload.ts @@ -1,7 +1,11 @@ import { z } from "zod"; +// schema for valid upload request export const uploadSignatureSchema = z.object({ folder: z.enum(["umsa/gallery", "umsa/team", "umsa/events", "umsa/branding"]), }); +// ^Defines the rule for valid upload request body +// - Using object function, rule: request body must contain a folder field +// - Using enum function, that folder value must be one of those 4 pre-determined strings - export type uploadSignatureInput = z.infer; \ No newline at end of file + export type UploadSignatureInput = z.infer; \ No newline at end of file From ab6191d245d3d57d5122d20207434c9909e770ca Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 25 Aug 2026 12:46:32 +1200 Subject: [PATCH 04/14] feat:admin/uploads.ts done + comments --- server/src/routes/admin/uploads.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/server/src/routes/admin/uploads.ts b/server/src/routes/admin/uploads.ts index a6ac6a2..4a104ff 100644 --- a/server/src/routes/admin/uploads.ts +++ b/server/src/routes/admin/uploads.ts @@ -2,19 +2,34 @@ import { Router, Request, Response } from "express"; import { sendError } from "../../utils/apiError.js"; import { validate } from "../../middleware/validate.js"; import { uploadSignatureSchema } from "../../schemas/upload.js"; +import { cloudinaryConfig, signUpload } from "../../utils/cloudinary.js"; +import { requireAdmin } from "../../middleware/requireAdmin.js"; -const router = Router(); +// create a new router instance +const router = Router(); -router.post("/signature", validate(uploadSignatureSchema), async (req: Request, res: Response) => { +// create a rule saying if incoming request is a post method and its path matches /signature +// first run requireAdmin to check admin cookie to see if its the admin +// after run the validate function with uploadSignatureSchema +// after run the async arrow function with request and response +router.post("/signature", requireAdmin, validate(uploadSignatureSchema), async (req: Request, res: Response) => { try { - // get folder from req - // get all the info from .env + // get folder from req + // .body just grabs everything in the json that was sent by browser, in this case only folder + const { folder } = req.body; + // get all the info from .env and also make sure env vars aren't "" + const { cloudName, apiKey, apiSecret } = cloudinaryConfig(); + if (!cloudName || !apiKey || !apiSecret) { + return sendError(res, 500, "INTERNAL_ERROR", "Missing Cloudinary env vars"); + } - // import signUpload & run + // import signUpload & run to get signature and timestamp + const { signature, timestamp } = signUpload(folder); - // return inside of res folder etc. -> check content.ts + // send json file back with everything the browser now needs to upload to cloudinary + res.json({ signature, timestamp, folder, cloudName, apiKey }); } catch { - sendError(res, 500, "INTERNAL_ERROR", "Missing Cloudinary env vars"); + sendError(res, 500, "INTERNAL_ERROR", "Error during upload signature generation"); } }); From 8ba8e624098059d05a641627dde8ad09edd5d36d Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 25 Aug 2026 12:54:49 +1200 Subject: [PATCH 05/14] feat: upload router added to app.ts + removed requireAdmin check from admin/uploads.ts --- server/src/app.ts | 2 ++ server/src/routes/admin/uploads.ts | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index 9ed7a7e..8ae67d1 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -6,6 +6,7 @@ import healthRoutes from "./routes/health.js"; import contentRoutes from "./routes/content.js"; import adminAuthRoutes from "./routes/admin/auth.js"; import adminContentRoutes from "./routes/admin/content.js"; +import adminUploadRoutes from "./routes/admin/uploads.js"; import { apiRateLimit } from "./middleware/rateLimit.js"; import { requireAdmin } from "./middleware/requireAdmin.js"; import { notFound } from "./middleware/notFound.js"; @@ -37,6 +38,7 @@ app.use("/api/admin/auth", adminAuthRoutes); // every /api/admin/* route below requires a logged-in admin account app.use("/api/admin", requireAdmin); app.use("/api/admin/content", adminContentRoutes); +app.use("/api/admin/uploads", adminUploadRoutes); // fallbacks app.use(notFound); diff --git a/server/src/routes/admin/uploads.ts b/server/src/routes/admin/uploads.ts index 4a104ff..5933468 100644 --- a/server/src/routes/admin/uploads.ts +++ b/server/src/routes/admin/uploads.ts @@ -3,16 +3,14 @@ import { sendError } from "../../utils/apiError.js"; import { validate } from "../../middleware/validate.js"; import { uploadSignatureSchema } from "../../schemas/upload.js"; import { cloudinaryConfig, signUpload } from "../../utils/cloudinary.js"; -import { requireAdmin } from "../../middleware/requireAdmin.js"; // create a new router instance const router = Router(); // create a rule saying if incoming request is a post method and its path matches /signature -// first run requireAdmin to check admin cookie to see if its the admin -// after run the validate function with uploadSignatureSchema +// first run the validate function with uploadSignatureSchema // after run the async arrow function with request and response -router.post("/signature", requireAdmin, validate(uploadSignatureSchema), async (req: Request, res: Response) => { +router.post("/signature", validate(uploadSignatureSchema), async (req: Request, res: Response) => { try { // get folder from req // .body just grabs everything in the json that was sent by browser, in this case only folder From ca4f960a9fce38aa07fdba59e880c97d7dc3bbcd Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 25 Aug 2026 12:58:14 +1200 Subject: [PATCH 06/14] feat: added .env.example --- server/.env.example | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 server/.env.example diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..c086dc3 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,3 @@ +CLOUDINARY_CLOUD_NAME= +CLOUDINARY_API_KEY= +CLOUDINARY_API_SECRET= \ No newline at end of file From 46d844594f202b946c8d3e4cc568d4330e0833fd Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 25 Aug 2026 14:48:26 +1200 Subject: [PATCH 07/14] feat: useImageUpload.ts done + detailed comments --- client/src/hooks/useImageUpload.ts | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 client/src/hooks/useImageUpload.ts diff --git a/client/src/hooks/useImageUpload.ts b/client/src/hooks/useImageUpload.ts new file mode 100644 index 0000000..107da7c --- /dev/null +++ b/client/src/hooks/useImageUpload.ts @@ -0,0 +1,36 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { api } from "../lib/api"; +import axios from "axios"; +import { isAxiosError } from "axios"; +import { ME_KEY } from "./useAuth"; + +export const useImageUpload = (folder: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (file: File) => { + const { data: sig } = await api.post("/admin/uploads/signature", { folder }); + const form = new FormData(); + form.append("file", file); + form.append("api_key", sig.apiKey); + form.append("timestamp", sig.timestamp); + form.append("signature", sig.signature); + form.append("folder", sig.folder); + // ^send a request with post method + /admin/uploads/signature path + // include the folder from when useImageUpload was called with folder in param + // wait for express to respond with apiKey, timestamp, signature and folder + // new formData object and we append file, apiKey, timestamp, signature and folder + + const res = await axios.post(`https://api.cloudinary.com/v1_1/${sig.cloudName}/image/upload`, form); + return { url: res.data.secure_url, publicId: res.data.public_id }; + // ^send request with post method to cloudinary w/ our cloudname we got from backend + all the formdata + // wait for cloudinary to send back response + // return res.data.secure_url and res.data.public_id as url and publicId + }, + onError: (err) => { + if (isAxiosError(err) && err.response?.status === 401) { + // session died mid-edit -> nulls auth cache and AdminLayout redirects + queryClient.setQueryData(ME_KEY, null); + } + }, + }); +}; \ No newline at end of file From d3edd226144263c0f3ee7ed8ffd0b57fde9e88ce Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 25 Aug 2026 14:56:42 +1200 Subject: [PATCH 08/14] feat: created admin folder in components + empty ImageUploadField.tsx --- client/src/components/admin/ImageUploadField.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/src/components/admin/ImageUploadField.tsx diff --git a/client/src/components/admin/ImageUploadField.tsx b/client/src/components/admin/ImageUploadField.tsx new file mode 100644 index 0000000..e69de29 From e87326518994d0b2246dcd22d8023597bec1a08c Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 25 Aug 2026 15:12:13 +1200 Subject: [PATCH 09/14] fix: minor fixes to make code easier to read --- server/src/routes/admin/uploads.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/routes/admin/uploads.ts b/server/src/routes/admin/uploads.ts index 5933468..6b2c59e 100644 --- a/server/src/routes/admin/uploads.ts +++ b/server/src/routes/admin/uploads.ts @@ -13,8 +13,9 @@ const router = Router(); router.post("/signature", validate(uploadSignatureSchema), async (req: Request, res: Response) => { try { // get folder from req - // .body just grabs everything in the json that was sent by browser, in this case only folder + // .body just grabs everything in the json file that was sent by browser, in this case only folder const { folder } = req.body; + // get all the info from .env and also make sure env vars aren't "" const { cloudName, apiKey, apiSecret } = cloudinaryConfig(); if (!cloudName || !apiKey || !apiSecret) { From e8b84266f8ab28aa3505c27ea5da2ca1e5da7d8e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 9 Sep 2026 16:54:12 +1200 Subject: [PATCH 10/14] feat:basic image upload field(not done) --- .../src/components/admin/ImageUploadField.tsx | 21 +++++++++++++++++++ client/src/layouts/AdminLayout.tsx | 15 +++++++++++++ client/src/main.tsx | 2 ++ .../src/pages/admin/tempImageUploadPage.tsx | 15 +++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 client/src/pages/admin/tempImageUploadPage.tsx diff --git a/client/src/components/admin/ImageUploadField.tsx b/client/src/components/admin/ImageUploadField.tsx index e69de29..337a962 100644 --- a/client/src/components/admin/ImageUploadField.tsx +++ b/client/src/components/admin/ImageUploadField.tsx @@ -0,0 +1,21 @@ +import { useState } from "react"; + +export function ImageUploadField() { + const [previewUrl, setPreviewUrl] = useState(); + + function handleFileChange(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + + const localPreview = URL.createObjectURL(file); + setPreviewUrl(localPreview); + } + + return ( +
+

Image Upload

+ + {previewUrl && Preview} +
+ ); +} \ No newline at end of file diff --git a/client/src/layouts/AdminLayout.tsx b/client/src/layouts/AdminLayout.tsx index 5f4668a..5f2d533 100644 --- a/client/src/layouts/AdminLayout.tsx +++ b/client/src/layouts/AdminLayout.tsx @@ -3,6 +3,7 @@ import { useLogout, useMe } from "../hooks/useAuth"; // Adding an admin page = one entry here + one route in main.tsx. const MENU = [{ to: "/admin/home-content", label: "Homepage text" }]; +const ImageUpload = [{ to: "/admin/image-upload", label: "Image Upload(temp)" }];//remove later export default function AdminLayout() { const { data: me, isPending } = useMe(); @@ -41,6 +42,20 @@ export default function AdminLayout() { {item.label} ))} + {ImageUpload.map((item) => (//remve later from here + + `rounded-lg px-3 py-2 text-sm transition ${isActive + ? "bg-blue-500 text-white" + : "text-gray-400 hover:bg-gray-800 hover:text-white" + }` + } + > + {item.label} + //to the line below + ))}