From f518d7222c2241417b10c9f1bf4702001129053a Mon Sep 17 00:00:00 2001 From: Govind Date: Sun, 8 Dec 2024 15:58:47 +0530 Subject: [PATCH 1/5] made changes in pbctfForm and pbctf route --- app/(default)/api/registration/pbctf/route.ts | 104 +++++++++++++++++- components/forms/pbctfForm.tsx | 50 +++++---- 2 files changed, 127 insertions(+), 27 deletions(-) diff --git a/app/(default)/api/registration/pbctf/route.ts b/app/(default)/api/registration/pbctf/route.ts index f13446c..fa9b82c 100644 --- a/app/(default)/api/registration/pbctf/route.ts +++ b/app/(default)/api/registration/pbctf/route.ts @@ -12,12 +12,88 @@ import { } from "firebase/firestore"; import { NextResponse } from "next/server"; -// Add a new registration +//Check if USN exists +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const usn = searchParams.get("usn"); + if (!usn) { + return NextResponse.json({ error: "usn is required" }, { status: 400 }); + } + const q = query( + collection(db, "pbctf_registrations"), + where("participant1.usn", "==", usn) + ); + const querySnapshot = await getDocs(q); + if (!querySnapshot.empty) { + return NextResponse.json( + { message: "usn not registered", isUnique: true }, + { status: 200 } + ); + } + const q2 = query( + collection(db, "pbctf_registrations"), + where("participant2.usn", "==", usn) + ); + const querySnapshot2 = await getDocs(q2); + + if (!querySnapshot2.empty) { + return NextResponse.json( + { message: "usn not unique", isUnique: true }, + { status: 200 } + ); + } else { + return NextResponse.json( + { message: "usn already exists", isUnique: false }, + { status: 403 } + ); + } + } catch (error) { + if (error instanceof Error) { + console.error("Error details:", error.message); + return NextResponse.json( + { error: "An error occurred", details: error.message }, + { status: 500 } + ); + } else { + console.error("Unknown error:", error); + return NextResponse.json( + { error: "An unknown error occurred" }, + { status: 500 } + ); + } + } +} + export async function POST(request: Request) { + try { + const { searchParams } = new URL(request.url); // Extract query parameters + const action = searchParams.get("action"); // Determine the action from query params + + if (action === "validateRecaptcha") { + return validateRecaptcha(request); + } else if (action === "addRegistration") { + return addRegistration(request); + } else { + return NextResponse.json( + { error: "Invalid action specified" }, + { status: 400 } + ); + } + } catch (error) { + console.error("Error processing request:", error); + return NextResponse.json( + { error: "An error occurred", details: error }, + { status: 500 } + ); + } +} + +// Add a new registration +async function validateRecaptcha(request: Request) { const formData = await request.json(); const { recaptcha_token } = formData; - const recaptchaToken = recaptcha_token; const details = { @@ -27,7 +103,6 @@ export async function POST(request: Request) { }, }; - if (!recaptchaToken) { return NextResponse.json( { @@ -61,3 +136,26 @@ export async function POST(request: Request) { // Return a response return NextResponse.json({ message: "Recaptcha validated!" }); } + +async function addRegistration(request: Request) { + + try{ + const data = await request.json(); + if (!data || !data.participant1 || !data.participationType) { + return NextResponse.json( + { error: "Invalid data. Participant1 and participationType are required." }, + { status: 400 } + ); + } + await addDoc(collection(db, "pbctf_registrations"), data); + + return NextResponse.json({ message: "Registration successful!" }); + }catch(error){ + console.error("Error adding registration:", error); + return NextResponse.json( + { error: "Failed to add registration.", details: error}, + { status: 500 } + ); + } + +} diff --git a/components/forms/pbctfForm.tsx b/components/forms/pbctfForm.tsx index 6a9f541..c8ec0df 100644 --- a/components/forms/pbctfForm.tsx +++ b/components/forms/pbctfForm.tsx @@ -3,14 +3,12 @@ import "../../app/css/additional-styles/utility-patterns.css"; import "../../app/css/additional-styles/theme.css"; import { useEffect, useState } from "react"; import { useForm, SubmitHandler } from "react-hook-form"; -import { collection, addDoc, query, where, getDocs } from "firebase/firestore"; -import { db } from "@/Firebase"; import { branches } from "@/lib/constants/dropdownOptions"; import { Press_Start_2P } from "next/font/google"; import toast from "react-hot-toast"; -// NOTE TO ANYONE WHO IS WANDERING HERE IN HOPES OF PBCTF 4.0 +// NOTE TO ANYONE WHO IS WANDERING HERE IN HOPES OF PBCTF 4.0 // THIS FORM DIRECTLY CALLS THE DB FROM THE CLIENT SIDE // THIS IS NOT A GOOD PRACTICE // PLEASE DO NOT USE THIS AS A REFERENCE @@ -110,23 +108,19 @@ const PBCTFForm: React.FC = () => { const checkUsnUniqueness = async (usn: string): Promise => { - const q = query( - collection(db, "pbctf_registrations"), - where("participant1.usn", "==", usn) - ); - const querySnapshot = await getDocs(q); - - if (!querySnapshot.empty) { + if(!usn){ + console.log("USN is required"); + return false; + } + try{ + const resp = await fetch(`/api/registration/pbctf?usn=${usn}`) + const data = await resp.json(); + return Boolean(data.isUnique); + }catch(error){ + console.log("Error getting document:", error) return false; } - - const q2 = query( - collection(db, "pbctf_registrations"), - where("participant2.usn", "==", usn) - ); - const querySnapshot2 = await getDocs(q2); - - return querySnapshot2.empty; + }; @@ -139,19 +133,19 @@ const PBCTFForm: React.FC = () => { try { const recaptcha_token = token; if (recaptcha_token) { - const response = await fetch("/api/registration/pbctf", { + const response1 = await fetch("/api/registration/pbctf?action=validateRecaptcha", { method: "POST", body: JSON.stringify({ recaptcha_token }), }); - const res = await response.json(); + const res = await response1.json(); - if (!response.ok || res.error) { + if (!response1.ok || res.error) { toast.error(res.message); return; } - + console.log(data) // Check if USNs are the same for duo participation if ( data.participationType === "duo" && @@ -182,9 +176,17 @@ const PBCTFForm: React.FC = () => { return; } } - // If all checks pass, submit the form - await addDoc(collection(db, "pbctf_registrations"), data); + const response2 = await fetch("/api/registration/pbctf?action=addRegistration", { + method: "POST", + body: JSON.stringify(data), + }); + + const result = await response2.json(); + if (!response2.ok) { + toast.error(result.error || "Failed to submit registration."); + return; + } setSuccess(true); } } catch (error) { From 73adddb5958a0e32839d7c5a6f46881cf62076ae Mon Sep 17 00:00:00 2001 From: Govind Date: Sun, 8 Dec 2024 16:02:25 +0530 Subject: [PATCH 2/5] applied pretier --- app/(default)/api/registration/pbctf/route.ts | 13 +- components/forms/pbctfForm.tsx | 184 +++++++++--------- 2 files changed, 97 insertions(+), 100 deletions(-) diff --git a/app/(default)/api/registration/pbctf/route.ts b/app/(default)/api/registration/pbctf/route.ts index fa9b82c..7e5675f 100644 --- a/app/(default)/api/registration/pbctf/route.ts +++ b/app/(default)/api/registration/pbctf/route.ts @@ -138,24 +138,25 @@ async function validateRecaptcha(request: Request) { } async function addRegistration(request: Request) { - - try{ + try { const data = await request.json(); if (!data || !data.participant1 || !data.participationType) { return NextResponse.json( - { error: "Invalid data. Participant1 and participationType are required." }, + { + error: + "Invalid data. Participant1 and participationType are required.", + }, { status: 400 } ); } await addDoc(collection(db, "pbctf_registrations"), data); return NextResponse.json({ message: "Registration successful!" }); - }catch(error){ + } catch (error) { console.error("Error adding registration:", error); return NextResponse.json( - { error: "Failed to add registration.", details: error}, + { error: "Failed to add registration.", details: error }, { status: 500 } ); } - } diff --git a/components/forms/pbctfForm.tsx b/components/forms/pbctfForm.tsx index c8ec0df..b76829c 100644 --- a/components/forms/pbctfForm.tsx +++ b/components/forms/pbctfForm.tsx @@ -7,7 +7,6 @@ import { branches } from "@/lib/constants/dropdownOptions"; import { Press_Start_2P } from "next/font/google"; import toast from "react-hot-toast"; - // NOTE TO ANYONE WHO IS WANDERING HERE IN HOPES OF PBCTF 4.0 // THIS FORM DIRECTLY CALLS THE DB FROM THE CLIENT SIDE // THIS IS NOT A GOOD PRACTICE @@ -17,7 +16,6 @@ import toast from "react-hot-toast"; // PARAGATI RAJ ARE YOU READING THIS // I MISS YOU - const pressStart2P = Press_Start_2P({ weight: "400", subsets: ["latin"], @@ -38,7 +36,6 @@ type FormData = { participant2?: ParticipantData; }; - const PBCTFForm: React.FC = () => { const [isSuccess, setSuccess] = useState(false); const [participationType, setParticipationType] = useState<"solo" | "duo">( @@ -80,9 +77,8 @@ const PBCTFForm: React.FC = () => { const Rtoken = await grecaptcha.enterprise.execute( process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ); - setToken(Rtoken); + setToken(Rtoken); }); - }; useEffect(() => { @@ -105,26 +101,21 @@ const PBCTFForm: React.FC = () => { const watchUsn1 = watch("participant1.usn"); const watchUsn2 = watch("participant2.usn"); - - const checkUsnUniqueness = async (usn: string): Promise => { - if(!usn){ - console.log("USN is required"); - return false; - } - try{ - const resp = await fetch(`/api/registration/pbctf?usn=${usn}`) + if (!usn) { + console.log("USN is required"); + return false; + } + try { + const resp = await fetch(`/api/registration/pbctf?usn=${usn}`); const data = await resp.json(); return Boolean(data.isUnique); - }catch(error){ - console.log("Error getting document:", error) + } catch (error) { + console.log("Error getting document:", error); return false; } - }; - - const onSubmit: SubmitHandler = async (data) => { if (isSubmitting) return; setIsSubmitting(true); @@ -133,10 +124,13 @@ const PBCTFForm: React.FC = () => { try { const recaptcha_token = token; if (recaptcha_token) { - const response1 = await fetch("/api/registration/pbctf?action=validateRecaptcha", { - method: "POST", - body: JSON.stringify({ recaptcha_token }), - }); + const response1 = await fetch( + "/api/registration/pbctf?action=validateRecaptcha", + { + method: "POST", + body: JSON.stringify({ recaptcha_token }), + } + ); const res = await response1.json(); @@ -145,7 +139,7 @@ const PBCTFForm: React.FC = () => { return; } - console.log(data) + console.log(data); // Check if USNs are the same for duo participation if ( data.participationType === "duo" && @@ -177,16 +171,19 @@ const PBCTFForm: React.FC = () => { } } // If all checks pass, submit the form - const response2 = await fetch("/api/registration/pbctf?action=addRegistration", { + const response2 = await fetch( + "/api/registration/pbctf?action=addRegistration", + { method: "POST", body: JSON.stringify(data), - }); - - const result = await response2.json(); - if (!response2.ok) { - toast.error(result.error || "Failed to submit registration."); - return; } + ); + + const result = await response2.json(); + if (!response2.ok) { + toast.error(result.error || "Failed to submit registration."); + return; + } setSuccess(true); } } catch (error) { @@ -243,8 +240,6 @@ const PBCTFForm: React.FC = () => { } const renderParticipantFields = (participantNumber: 1 | 2) => ( - -

Participant {participantNumber}

@@ -318,27 +313,29 @@ const PBCTFForm: React.FC = () => {
@@ -392,60 +389,59 @@ const PBCTFForm: React.FC = () => { return ( <> - -
-

- {headingText} -

-
-
- - -
+
+

+ {headingText} +

+ +
+ + +
- {renderParticipantFields(1)} - {participationType === "duo" && renderParticipantFields(2)} + {renderParticipantFields(1)} + {participationType === "duo" && renderParticipantFields(2)} - {usnError && ( -

{usnError}

- )} + {usnError && ( +

{usnError}

+ )} -
- -
- -
+
+ +
+ +
); }; From 22697dc4e097d53213cd8a005778fc0a2087256f Mon Sep 17 00:00:00 2001 From: Govind <66307025+govindup63@users.noreply.github.com> Date: Sun, 8 Dec 2024 16:12:01 +0530 Subject: [PATCH 3/5] removed old comments --- components/forms/pbctfForm.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/components/forms/pbctfForm.tsx b/components/forms/pbctfForm.tsx index b76829c..ef771d5 100644 --- a/components/forms/pbctfForm.tsx +++ b/components/forms/pbctfForm.tsx @@ -7,12 +7,6 @@ import { branches } from "@/lib/constants/dropdownOptions"; import { Press_Start_2P } from "next/font/google"; import toast from "react-hot-toast"; -// NOTE TO ANYONE WHO IS WANDERING HERE IN HOPES OF PBCTF 4.0 -// THIS FORM DIRECTLY CALLS THE DB FROM THE CLIENT SIDE -// THIS IS NOT A GOOD PRACTICE -// PLEASE DO NOT USE THIS AS A REFERENCE -// KINDLY CONTACT SKY SINGH TO FIX THIS -// WHENVEVER THE NEXT CTF IS // PARAGATI RAJ ARE YOU READING THIS // I MISS YOU From 4bbf4ba02823fe3593ef91b614b1bed576f68862 Mon Sep 17 00:00:00 2001 From: Govind Date: Sat, 14 Dec 2024 04:10:52 +0530 Subject: [PATCH 4/5] moved storage call from frontend of leads page to backend --- app/(default)/api/leads/upload/route.ts | 83 +++++++++---------------- components/leadspage.tsx | 49 ++++++++++----- 2 files changed, 62 insertions(+), 70 deletions(-) diff --git a/app/(default)/api/leads/upload/route.ts b/app/(default)/api/leads/upload/route.ts index 576c9fc..ab5af70 100644 --- a/app/(default)/api/leads/upload/route.ts +++ b/app/(default)/api/leads/upload/route.ts @@ -1,60 +1,33 @@ -import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage"; -import { NextResponse } from "next/server"; -import { db, storage} from "@/Firebase"; +import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; +import { NextResponse } from "next/server"; +import { storage } from "@/Firebase"; export async function POST(request: Request) { - try { - const storage = getStorage(); - const selectedLead = await request.json(); - const imageRef = ref(storage, `images/${selectedLead.name}`); - let imageUrl; + try { + const formData = await request.formData(); - try { - const imageBlob = await fetch(selectedLead.imageUrl).then((r) => r.blob()); - console.log("Image blob fetched:", imageBlob); - try { - await uploadBytes(imageRef, imageBlob); - console.log("Image uploaded to storage"); - try { - imageUrl = await getDownloadURL(imageRef); - console.log("Image URL obtained:", imageUrl); - } catch (error: any) { - console.error("Error getting image URL:", error); - return NextResponse.json( - { error: "An error occurred", details: error.message }, - { status: 500 } - ); - } - } catch (error: any) { - console.error("Error uploading image:", error); - return NextResponse.json( - { error: "An error occurred", details: error.message }, - { status: 500 } - ); - } - } catch (error: any) { - console.error("Error fetching image blob:", error); - return NextResponse.json( - { error: "An error occurred", details: error.message }, - { status: 500 } - ); - } - - return NextResponse.json({ imageUrl: imageUrl }, { status: 201 }); - } catch (error) { - if (error instanceof Error) { - console.error("Error details:", error.message); - return NextResponse.json( - { error: "An error occurred", details: error.message }, - { status: 500 } - ); - } else { - console.error("Unknown error:", error); - return NextResponse.json( - { error: "An unknown error occurred" }, - { status: 500 } - ); - } + const Image: File | null = formData.get('file') as File; + if (!Image) { + return NextResponse.json( + { message: "Bad Request", details: "No file uploaded" }, + { status: 400 } + ); } -} \ No newline at end of file + + const name: string = formData.get('name') as string; + const imageRef = ref(storage, `images/${name}`); + await uploadBytes(imageRef, Image); + const imageUrl = await getDownloadURL(imageRef); + + return NextResponse.json({ + imageUrl: imageUrl + }); + } catch (error:any) { + console.error("Error uploading file:", error); + return NextResponse.json( + { message: "Internal Server Error", details: error.message }, + { status: 500 } + ); + } +} diff --git a/components/leadspage.tsx b/components/leadspage.tsx index d38b315..1e3ff7b 100644 --- a/components/leadspage.tsx +++ b/components/leadspage.tsx @@ -1,9 +1,8 @@ "use client"; import React, { useState, useEffect } from "react"; import { onAuthStateChanged } from "firebase/auth"; -import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage"; - import { auth } from "@/Firebase"; +import { useStore } from "@/lib/zustand/store"; interface Lead { id: string; @@ -16,12 +15,12 @@ interface Lead { const Leads: React.FC = () => { const [loading, setLoading] = useState(true); - const [showForm, setShowForm] = useState(false); const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false); const [currentLeads, setCurrentLeads] = useState([]); const [alumniLeads, setAlumniLeads] = useState([]); const [selectedLead, setSelectedLead] = useState(null); // For editing leads + const { image } = useStore(); // Fetch leads from Firestore const fetchLeads = async () => { @@ -69,24 +68,40 @@ const Leads: React.FC = () => { } try { - const storage = getStorage(); let imageUrl = selectedLead.imageUrl; - // If an image is being uploaded, handle that if (selectedLead.imageUrl && selectedLead.imageUrl.startsWith("blob")) { - console.log("Image file detected, uploading to Firebase Storage..."); - const imageRef = ref(storage, `images/${selectedLead.name}`); - - // Fetch the blob file for upload - const imageBlob = await fetch(selectedLead.imageUrl).then((r) => - r.blob() + console.log( + "Image file detected, preparing for upload to Firebase Storage..." ); - await uploadBytes(imageRef, imageBlob); - imageUrl = await getDownloadURL(imageRef); + + // Create a new FormData object + const formData = new FormData(); + if (image) { + formData.append("file", image); + formData.append("name", selectedLead.name); + } + + const response = await fetch("/api/leads/upload", { + method: "POST", + body: formData, // FormData automatically sets the correct headers + }); + + let data; + try { + data = await response.json(); + } catch (error) { + console.error("Failed to parse JSON response:", error); + throw new Error("Unexpected response from the server"); + } + imageUrl = data.imageUrl; + if (!response.ok) { + console.error("Error uploading file:", data); + throw new Error(data.message || "Error uploading file"); + } console.log("Image uploaded successfully, URL:", imageUrl); } - // Create the lead data with the updated (or original) image URL const leadData = { ...selectedLead, imageUrl: imageUrl, // Ensure the Firebase URL is used @@ -344,6 +359,7 @@ const LeadForm: React.FC = ({ imageUrl: "", } ); + const { setImage } = useStore(); const handleChange = ( e: React.ChangeEvent< @@ -359,10 +375,13 @@ const LeadForm: React.FC = ({ const handleFileChange = (e: React.ChangeEvent) => { const files = e.target.files; + if (files && files.length > 0) { + const file = files[0]; + setImage(file); setLead((prevLead) => ({ ...prevLead, - imageUrl: URL.createObjectURL(files[0]), + imageUrl: URL.createObjectURL(file), })); } else { console.error("No file selected or invalid file input"); From 8d38b1e5817758ba041bc4fc4c7650a07b45c781 Mon Sep 17 00:00:00 2001 From: Govind Date: Sat, 14 Dec 2024 04:32:28 +0530 Subject: [PATCH 5/5] moved all the storage calls from frontend to backend in Members page --- app/(default)/api/membersData/route.ts | 473 ++++----- app/(default)/api/membersData/upload/route.ts | 49 + components/Members.tsx | 902 +++++++++--------- lib/zustand/store.ts | 17 + package-lock.json | 56 +- package.json | 3 +- 6 files changed, 772 insertions(+), 728 deletions(-) create mode 100644 app/(default)/api/membersData/upload/route.ts create mode 100644 lib/zustand/store.ts diff --git a/app/(default)/api/membersData/route.ts b/app/(default)/api/membersData/route.ts index b40a8d6..5cc45ac 100644 --- a/app/(default)/api/membersData/route.ts +++ b/app/(default)/api/membersData/route.ts @@ -1,317 +1,202 @@ -import { NextResponse } from 'next/server'; -import { db, storage } from '@/Firebase'; +import { NextResponse } from "next/server"; +import { db, storage } from "@/Firebase"; import { - collection, addDoc, getDocs, DocumentData, - DocumentSnapshot, - doc, - updateDoc, - deleteDoc, - getDoc -} from 'firebase/firestore'; -import { ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage'; - + collection, + addDoc, + getDocs, + DocumentData, + DocumentSnapshot, + doc, + updateDoc, + deleteDoc, + getDoc, +} from "firebase/firestore"; +import { + ref, + uploadBytes, + getDownloadURL, + deleteObject, +} from "firebase/storage"; +// GET handler to retrieve all members export async function GET() { - try { - - const querySnapshot = await getDocs(collection(db, "pbMembers")); - - const membersRaw = querySnapshot.docs.map( - (doc: DocumentSnapshot) => ({ - id: doc.id, - ...doc.data(), - }) - ); - - const members = membersRaw.map((member: any) => { - return { - id: member.id, - name: member.name, - role: member.role, - company: member.company || '', - year: member.year, - linkedInUrl: member.linkedInUrl || '', - imageUrl: member.imageUrl || '' - }; - }); - - return NextResponse.json(members); - } catch (error) { - if (error instanceof Error) { - console.error("Error fetching members:", error.message); - return NextResponse.json( - { - error: "An error occurred while fetching members", - details: error.message, - }, - { status: 500 } - ); - } else { - console.error("Unknown error:", error); - return NextResponse.json( - { error: "An unknown error occurred while fetching members" }, - { status: 500 } - ); - } + try { + // Fetch all member documents from the Firestore collection + const querySnapshot = await getDocs(collection(db, "pbMembers")); + + // Map the documents to extract data and include document ID + const membersRaw = querySnapshot.docs.map( + (doc: DocumentSnapshot) => ({ + id: doc.id, + ...doc.data(), + }) + ); + + // Format the members' data for the response + const members = membersRaw.map((member: any) => ({ + id: member.id, + name: member.name, + role: member.role, + company: member.company || "", + year: member.year, + linkedInUrl: member.linkedInUrl || "", + imageUrl: member.imageUrl || "", + })); + + // Return the formatted member data as JSON + return NextResponse.json(members); + } catch (error) { + // Catch any error during the fetching process + if (error instanceof Error) { + console.error("Error fetching members:", error.message); + return NextResponse.json( + { + error: "An error occurred while fetching members", + details: error.message, + }, + { status: 500 } + ); + } else { + console.error("Unknown error:", error); + return NextResponse.json( + { error: "An unknown error occurred while fetching members" }, + { status: 500 } + ); } + } } +// POST handler to add a new member export async function POST(request: Request) { - try { - const data = await request.json(); - - const { name, year, role, company, imageUrl, linkedInUrl } = data; - - if (!name) { - return NextResponse.json( - { message: 'Missing Name.', error: true }, - { status: 400 } - ); - } - - let downloadURL = ''; - if (imageUrl) { - const response = await fetch(imageUrl); - if (!response.ok) { - return NextResponse.json( - { message: 'Failed to fetch image from the URL.', error: true }, - { status: 400 } - ); - } - const contentType = response.headers.get('content-type'); - - if (!contentType || !contentType.startsWith('image/')) { - return NextResponse.json( - { message: 'The provided URL is not a valid image.', error: true }, - { status: 400 } - ); - } - - const blob = await response.blob(); - - const extension = contentType.split('/')[1]; - - const imageRef = ref(storage, `members/${name}.${extension}`); - await uploadBytes(imageRef, blob, { contentType }); - - downloadURL = await getDownloadURL(imageRef); - } - - const docRef = await addDoc(collection(db, 'pbMembers'), { - name, - year, - role, - company, - linkedInUrl, - imageUrl: downloadURL, - }); - - return NextResponse.json( - { message: 'Member added successfully', id: docRef.id }, - { status: 201 } - ); - } catch (error) { - console.error('Error adding member:', error); - return NextResponse.json( - { message: `Failed to add member: ${error}`, error: true }, - { status: 500 } - ); + try { + // Parse the request body to extract member data + const data = await request.json(); + + const { name, year, role, company, imageUrl, linkedInUrl } = data; + + // Check for missing required fields + if (!name) { + return NextResponse.json( + { message: "Missing Name.", error: true }, + { status: 400 } + ); } -} + // Add the new member document to the Firestore collection + const docRef = await addDoc(collection(db, "pbMembers"), { + name, + year, + role, + company, + linkedInUrl, + imageUrl, + }); + + // Return success response with the new member's document ID + return NextResponse.json( + { message: "Member added successfully", id: docRef.id }, + { status: 201 } + ); + } catch (error) { + // Catch and log any errors during member creation + console.error("Error adding member:", error); + return NextResponse.json( + { message: `Failed to add member: ${error}`, error: true }, + { status: 500 } + ); + } +} +// PUT handler to update an existing member export async function PUT(request: Request) { - try { - const data = await request.json(); - const { id, name, year, role, company, imageUrl, linkedInUrl } = data; - - if (!id || !name) { - return NextResponse.json( - { message: 'Missing required fields or member ID', error: true }, - { status: 400 } - ); - } - - const memberRef = doc(db, "pbMembers", id); - const memberSnapshot = await getDoc(memberRef); - if (!memberSnapshot.exists()) { - return NextResponse.json( - { message: 'Member not found', error: true }, - { status: 404 } - ); - } - - const memberData = memberSnapshot.data(); - let downloadURL = memberData.imageUrl; - - if (imageUrl && imageUrl !== memberData.imageUrl) { - const response = await fetch(imageUrl); - if (!response.ok) { - return NextResponse.json( - { message: 'Failed to fetch image from the URL.', error: true }, - { status: 400 } - ); - } - - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.startsWith('image/')) { - return NextResponse.json( - { message: 'The provided URL is not a valid image.', error: true }, - { status: 400 } - ); - } - - const blob = await response.blob(); - const extension = contentType.split('/')[1]; - - // Use a timestamp to ensure a unique filename - const timestamp = Date.now(); - const imageRef = ref(storage, `members/${name}_${timestamp}.${extension}`); - - await uploadBytes(imageRef, blob, { contentType }); - downloadURL = await getDownloadURL(imageRef); - - if (memberData.imageUrl) { - const oldImageRef = ref(storage, memberData.imageUrl); - await deleteObject(oldImageRef).catch(error => { - console.error('Error deleting old image:', error); - }); - } - } - - const updatedData = { - name, - year, - role, - company, - linkedInUrl, - imageUrl: downloadURL, - }; - - await updateDoc(memberRef, updatedData); - - return NextResponse.json( - { message: 'Member updated successfully', data: updatedData }, - { status: 200 } - ); - } catch (error) { - console.error('Error updating member:', error); - return NextResponse.json( - { message: `Failed to update member: ${error}`, error: true }, - { status: 500 } - ); + try { + // Parse the request body to get the updated member data + const data = await request.json(); + const { id, name, imageUrl } = data; + + // Check for missing required fields + if (!id || !name) { + return NextResponse.json( + { message: "Missing required fields or member ID", error: true }, + { status: 400 } + ); } -} + // Update member data in Firestore + const updatedData = { ...data, imageUrl: imageUrl }; + await updateDoc(doc(db, "pbMembers", id), updatedData); + + // Return success response with the updated member data + return NextResponse.json( + { message: "Member updated successfully", data: updatedData }, + { status: 200 } + ); + } catch (error) { + // Catch and log any errors during member update + console.error("Error updating member:", error); + return NextResponse.json( + { message: `Failed to update member: ${error}`, error: true }, + { status: 500 } + ); + } +} +// DELETE handler to delete a member and their image export async function DELETE(request: Request) { - try { - const { id } = await request.json(); - - if (!id) { - return NextResponse.json( - { message: 'Missing member ID', error: true }, - { status: 400 } - ); - } - - // Reference to the member document - const memberRef = doc(db, 'pbMembers', id); - const memberSnapshot = await getDoc(memberRef); - - if (!memberSnapshot.exists()) { - return NextResponse.json( - { message: 'Member not found', error: true }, - { status: 404 } - ); - } + try { + // Parse the request body to get the member ID + const { id } = await request.json(); + + // Check if the member ID is provided + if (!id) { + return NextResponse.json( + { message: "Missing member ID", error: true }, + { status: 400 } + ); + } - const memberData = memberSnapshot.data(); - const imageUrl = memberData.imageUrl; + // Fetch the member document from Firestore + const memberRef = doc(db, "pbMembers", id); + const memberSnapshot = await getDoc(memberRef); - // Delete the image from Firebase Storage if it exists - if (imageUrl) { - const imageRef = ref(storage, imageUrl); - await deleteObject(imageRef).catch((error) => { - console.error('Error deleting image from Firebase Storage:', error); - return NextResponse.json( - { message: 'Failed to delete image from storage', error: true }, - { status: 500 } - ); - }); - } + // If the member does not exist, return a 404 response + if (!memberSnapshot.exists()) { + return NextResponse.json( + { message: "Member not found", error: true }, + { status: 404 } + ); + } - // Delete the member document from Firestore - await deleteDoc(memberRef); + // Extract member data and image URL + const memberData = memberSnapshot.data(); + const imageUrl = memberData.imageUrl; + // If an image URL exists, delete the image from Firebase Storage + if (imageUrl) { + const imageRef = ref(storage, imageUrl); + await deleteObject(imageRef).catch((error) => { + console.error("Error deleting image from Firebase Storage:", error); return NextResponse.json( - { message: 'Member and associated image deleted successfully' }, - { status: 200 } - ); - } catch (error) { - console.error('Error deleting member:', error); - return NextResponse.json( - { message: 'Failed to delete member', error }, - { status: 500 } + { message: "Failed to delete image from storage", error: true }, + { status: 500 } ); + }); } -} - - -// // Posting a array of members - -// export async function POST(request: Request) { -// try { -// const data = await request.json(); - -// if (!Array.isArray(data)) { -// return NextResponse.json({ error: 'Invalid data format. Expected an array of objects.' }, { status: 400 }); -// } - -// for (const item of data) { -// const { name, year, role, company, imageUrl, linkedInUrl } = item; - -// if (!name) { -// return NextResponse.json({ error: 'Missing Name.' }, { status: 400 }); -// } - -// let downloadURL = ''; -// if (imageUrl) { -// const response = await fetch(imageUrl); - -// if (!response.ok) { -// return NextResponse.json({ error: 'Failed to fetch image from the URL.' }, { status: 400 }); -// } - -// const contentType = response.headers.get('content-type'); - -// if (!contentType || contentType.includes('text/html')) { -// return NextResponse.json({ error: 'Provided link is not a direct image link. Please ensure the Google Drive file is public.' }, { status: 400 }); -// } - -// const blob = await response.blob(); -// const extension = contentType.split('/')[1]; - -// const imageRef = ref(storage, `members/${name}.${extension}`); -// await uploadBytes(imageRef, blob, { contentType }); - -// downloadURL = await getDownloadURL(imageRef); // Assign value to downloadURL -// } - -// await addDoc(collection(db, 'pbMembers'), { -// name, -// year, -// role, -// company, -// linkedInUrl, -// imageUrl: downloadURL || '', -// }); -// } - -// return NextResponse.json({ message: 'Data added successfully' }, { status: 200 }); -// } catch (error) { -// console.error('Error uploading data:', error); -// return NextResponse.json({ error: `Failed to add data: ${error}` }, { status: 500 }); -// } -// } \ No newline at end of file + // Delete the member document from Firestore + await deleteDoc(memberRef); + + // Return success response after deletion + return NextResponse.json( + { message: "Member and associated image deleted successfully" }, + { status: 200 } + ); + } catch (error) { + // Catch and log any errors during the deletion process + console.error("Error deleting member:", error); + return NextResponse.json( + { message: "Failed to delete member", error }, + { status: 500 } + ); + } +} diff --git a/app/(default)/api/membersData/upload/route.ts b/app/(default)/api/membersData/upload/route.ts new file mode 100644 index 0000000..576d11d --- /dev/null +++ b/app/(default)/api/membersData/upload/route.ts @@ -0,0 +1,49 @@ +import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; +import { NextResponse } from "next/server"; +import { storage } from "@/Firebase"; + +// The POST function to handle file uploads to Firebase Storage +export async function POST(request: Request) { + try { + // Parse the form data from the incoming request + const formData = await request.formData(); + + // Retrieve the uploaded file from the form data + const Image: File | null = formData.get('file') as File; + + // If no file is uploaded, return a bad request response + if (!Image) { + return NextResponse.json( + { message: "Bad Request", details: "No file uploaded" }, + { status: 400 } + ); + } + + // Retrieve the name of the image from the form data + const name: string = formData.get('name') as string; + + // Create a reference in Firebase Storage with the image name + const imageRef = ref(storage, `members/${name}`); + + // Upload the image to Firebase Storage + await uploadBytes(imageRef, Image); + + // Get the download URL of the uploaded image + const imageUrl = await getDownloadURL(imageRef); + + // Return the image URL as the response + return NextResponse.json({ + imageUrl: imageUrl + }); + + } catch (e) { + // Log any error that occurs during the process + console.error("Error during file upload:", e); + + // Return a server error response if something goes wrong + return NextResponse.json( + { message: "Internal Server Error", details: e instanceof Error ? e.message : 'Unknown error' }, + { status: 500 } + ); + } +} diff --git a/components/Members.tsx b/components/Members.tsx index 4d37290..562193d 100644 --- a/components/Members.tsx +++ b/components/Members.tsx @@ -5,273 +5,323 @@ import ClipLoader from "react-spinners/ClipLoader"; import { FaRegBell, FaEllipsisV } from "react-icons/fa"; import { onAuthStateChanged } from "firebase/auth"; import { auth } from "@/Firebase"; -import {storage } from "@/Firebase"; -import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'; -import Image from 'next/image'; +import Image from "next/image"; import Card from "./ui/Card"; import CollapsibleSection from "./ui/CollapsibleSection"; - +import { useStoreMember } from "@/lib/zustand/store"; interface Member { - id?: string; - name: string; - role: string; - company?: string; - year: string; - linkedInUrl?: string; - imageUrl?: string; + id?: string; + name: string; + role: string; + company?: string; + year: string; + linkedInUrl?: string; + imageUrl?: string; } const headings = [ - "Alumni", - "Fourth Year", - "Third Year", - "Second Year", - "First Year", + "Alumni", + "Fourth Year", + "Third Year", + "Second Year", + "First Year", ]; export default function Members() { - const [openIndex, setOpenIndex] = useState( - headings.indexOf("Alumni") - ); - const [data, setData] = useState<{ [key: string]: Member[] }>({}); - const [loading, setLoading] = useState(true); - const [showForm, setShowForm] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [editMemberId, setEditMemberId] = useState(null); - const [newMember, setNewMember] = useState({ - name: "", - role: "", - company: "", - year: "", - linkedInUrl: "", - imageUrl: "" - - }); - const [menuVisible, setMenuVisible] = useState<{ [key: string]: boolean }>( - {} - ); - const formRef = useRef(null); - - const handleToggle = (index: number) => { - setOpenIndex(openIndex === index ? -1 : index); - }; - - const [isAdmin, setIsAdmin] = useState(false); - - useEffect(() => { - onAuthStateChanged(auth, async (user) => { - if (user) { - const uid = user.uid; - try { - const resp = await fetch(`/api/admin?uid=${uid}`); - const data = await resp.json(); - if (data.isAdmin) { - setIsAdmin(true); - } - } catch (error) { - console.log("Error getting document:", error); - } + const [openIndex, setOpenIndex] = useState( + headings.indexOf("Alumni") + ); + const [data, setData] = useState<{ [key: string]: Member[] }>({}); + const [loading, setLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editMemberId, setEditMemberId] = useState(null); + const [newMember, setNewMember] = useState({ + name: "", + role: "", + company: "", + year: "", + linkedInUrl: "", + imageUrl: "", + }); + const { image, setImage } = useStoreMember(); + const [menuVisible, setMenuVisible] = useState<{ [key: string]: boolean }>( + {} + ); + const formRef = useRef(null); + + const handleToggle = (index: number) => { + setOpenIndex(openIndex === index ? -1 : index); + }; + + const [isAdmin, setIsAdmin] = useState(false); + + useEffect(() => { + onAuthStateChanged(auth, async (user) => { + if (user) { + const uid = user.uid; + try { + const resp = await fetch(`/api/admin?uid=${uid}`); + const data = await resp.json(); + if (data.isAdmin) { + setIsAdmin(true); } - }); - }); - - - const handleFileChange = async (e: React.ChangeEvent) => { - const files = e.target.files; - - if (files && files.length > 0) { - const file = files[0]; - - const contentType = file.type; - const extension = contentType.split('/')[1]; - - const imageRef = ref(storage, `members/${newMember.name}.${extension}`); - - try { + } catch (error) { + console.log("Error getting document:", error); + } + } + }); + }); + + const handleFileChange = async (e: React.ChangeEvent) => { + const files = e.target.files; + if (files && files.length > 0) { + const file = files[0]; + setImage(file); + setNewMember((newMember) => ({ + ...newMember, + imageUrl: URL.createObjectURL(files[0]), + })); + } else { + console.error("No file selected or invalid file input"); + } + }; + + const handleAddOrEditMember = async () => { + if (!newMember.name || !newMember.role || !newMember.year) { + alert( + "Please fill in all required fields (Name, Domain/Role, and Year)." + ); + return; + } + + try { + const method = isEditing ? "PUT" : "POST"; + let imageUrl = newMember.imageUrl; + + // Handling image URL upload if it's a blob + if (newMember.imageUrl && newMember.imageUrl.startsWith("blob")) { + console.log("Image file detected, uploading to Firebase Storage..."); + const formData = new FormData(); + formData.append("file", image as Blob); + formData.append("name", newMember.name); + try { + const response = await fetch("/api/membersData/upload", { + method: "POST", + body: formData, + }); + + const data = await response.json(); + if (!response.ok) + throw new Error(data.message || "Image upload failed."); + imageUrl = data.imageUrl; + console.log("Image uploaded successfully, URL:", imageUrl); + } catch (error) { + console.error("Error uploading image:", error); + alert("Failed to upload image. Please try again."); + return; + } + } - const snapshot = await uploadBytes(imageRef, file, { contentType }); + // Updating member data with the new image URL + let memberData = isEditing + ? { ...newMember, id: editMemberId } + : newMember; - const downloadURL = await getDownloadURL(snapshot.ref); + memberData = { ...newMember, imageUrl }; - setNewMember((prev) => ({ ...prev, imageUrl: downloadURL })); - } catch (error) { - console.error('Error uploading file:', error); - } - } else { - console.warn('No file selected.'); - } - }; - - const handleAddOrEditMember = async () => { - if (!newMember.name || !newMember.role || !newMember.year) { - alert( - "Please fill in all required fields (Name, Domain/Role, and Year)." - ); - return; - } + if (newMember.id) { + // Update member in Firestore try { - const method = isEditing ? "PUT" : "POST"; - const memberData = isEditing - ? { ...newMember, id: editMemberId } - : newMember; - - const response = await fetch("/api/membersData", { - method: method, - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(memberData), - }); - - const result = await response.json(); + const response = await fetch(`/api/membersData`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(memberData), + }); + + const result = await response.json(); + + if (!response.ok) { + console.error("Network response was not ok.", result); + throw new Error(result.message || "Network response was not ok."); + } - if (!response.ok) { - console.error("Network response was not ok.", result); - throw new Error(result.message || "Network response was not ok."); - } - - alert( - isEditing ? "Member updated successfully" : "Member added successfully" - ); - setShowForm(false); - setIsEditing(false); - setEditMemberId(null); - setNewMember({ - name: "", - role: "", - company: "", - year: "", - linkedInUrl: "", - imageUrl: "" - }); - await fetchData(); + alert("Member updated successfully"); } catch (error) { - console.error("Error adding/updating member:", error); - alert("An error occurred. Check the console for details."); + console.error("Error updating member:", error); + alert("Failed to update member. Please try again."); + return; } - }; - - const handleEditMember = (member: Member) => { - setNewMember(member); - setIsEditing(true); - setShowForm(true); - setEditMemberId(member.id || null); - // Scroll to the form - setTimeout(() => { - formRef.current?.scrollIntoView({ behavior: "smooth" }); - }, 0); - }; - - const handleDeleteMember = async (id: string) => { + } else { + // Add new member to Firestore try { - const response = await fetch("/api/membersData", { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ id }), - }); - - if (!response.ok) { - throw new Error("Network response was not ok."); - } - + const response = await fetch("/api/membersData", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(memberData), + }); + + if (!response.ok) { const result = await response.json(); + console.error("Failed to add member:", result); + throw new Error(result.message || "Failed to add member."); + } - if (result.error) { - console.error("Error deleting member:", result.message); - alert(result.message); - } else { - alert("Member deleted successfully"); - await fetchData(); - } + alert("Member added successfully"); } catch (error) { - console.error("Error deleting member:", error); + console.error("Error adding member:", error); + alert("Failed to add member. Please try again."); + return; } - }; + } - const toggleMenu = (id: string) => { - setMenuVisible((prev) => ({ ...prev, [id]: !prev[id] })); - }; - - useEffect(() => { - fetchData(); - }, []); + // Reset the form and state + setShowForm(false); + setIsEditing(false); + setEditMemberId(null); + setNewMember({ + name: "", + role: "", + company: "", + year: "", + linkedInUrl: "", + imageUrl: "", + }); + await fetchData(); + } catch (error) { + console.error("Error adding/updating member:", error); + alert( + "An unexpected error occurred. Please check the console for details." + ); + } + }; + + const handleEditMember = (member: Member) => { + setNewMember(member); + setIsEditing(true); + setShowForm(true); + setEditMemberId(member.id || null); + // Scroll to the form + setTimeout(() => { + formRef.current?.scrollIntoView({ behavior: "smooth" }); + }, 0); + }; + + const handleDeleteMember = async (id: string) => { + try { + const response = await fetch("/api/membersData", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id }), + }); - const fetchData = async () => { - setLoading(true); - try { - const response = await fetch("/api/membersData", { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const result: Member[] = await response.json(); - const uniqueMembersMap = new Map(); - - result.forEach((member: Member) => { - const key = `${member.name}-${member.role}-${member.year}`; if (!uniqueMembersMap.has(key)) { - uniqueMembersMap.set(key, member); - } - }); + if (!response.ok) { + throw new Error("Network response was not ok."); + } + + const result = await response.json(); + + if (result.error) { + console.error("Error deleting member:", result.message); + alert(result.message); + } else { + alert("Member deleted successfully"); + await fetchData(); + } + } catch (error) { + console.error("Error deleting member:", error); + } + }; + + const toggleMenu = (id: string) => { + setMenuVisible((prev) => ({ ...prev, [id]: !prev[id] })); + }; + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + setLoading(true); + try { + const response = await fetch("/api/membersData", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); - const uniqueMembers = Array.from(uniqueMembersMap.values()).sort((a, b) => - a.name.localeCompare(b.name) - ); - const fetchedData: { [key: string]: Member[] } = {}; + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } - for (const heading of headings) { - fetchedData[heading] = uniqueMembers.filter( - (member: Member) => member.year === heading - ); - } + const result: Member[] = await response.json(); + const uniqueMembersMap = new Map(); - setData(fetchedData); - } catch (error) { - console.error("Error fetching data: ", error); - } finally { - setLoading(false); + result.forEach((member: Member) => { + const key = `${member.name}-${member.role}-${member.year}`; + if (!uniqueMembersMap.has(key)) { + uniqueMembersMap.set(key, member); } - }; - - return ( -
-

- Point Blank's Team -

-
- {loading ? ( -
- -
- ) : ( -
- {headings.map((heading, index) => ( - - {heading === "First Year" && ( -
-

- Recruitment Incoming Soon! -

- - -
- )} -
- {data[heading]?.map((profile, cardIndex) => ( -
- -
- {isAdmin ? ( -
+ )} +
+ {data[heading]?.map((profile, cardIndex) => ( +
+ +
+ {isAdmin ? ( + - ) : null} - - {menuVisible[profile.id || ""] && ( -
- - -
- )} -
-
- ))} -
-
- } - isOpen={openIndex === index} - onToggle={() => handleToggle(index)} - /> - ))} -
- )} - - {isAdmin ? ( -
- -
- ) : null} - - {showForm && ( -
-

- {isEditing ? "Edit Member" : "Add New Member"} -

-
- - - - - - - -
- - - {newMember.imageUrl && ( -
-

Preview:

- Uploaded -
- )} -
- - -
+ Delete + +
+ )} +
+
+ ))}
+
+ } + isOpen={openIndex === index} + onToggle={() => handleToggle(index)} + /> + ))} +
+ )} + + {isAdmin ? ( +
+ +
+ ) : null} + + {showForm && ( +
+

+ {isEditing ? "Edit Member" : "Add New Member"} +

+
+ + + + + + + +
+ + + {newMember.imageUrl && ( +
+

Preview:

+ Uploaded +
)} -
-
- ); +
+ + + + + )} + + + ); } diff --git a/lib/zustand/store.ts b/lib/zustand/store.ts new file mode 100644 index 0000000..e2568d0 --- /dev/null +++ b/lib/zustand/store.ts @@ -0,0 +1,17 @@ +// store/useStore.ts +import {create} from 'zustand'; + +interface SharedState { + image: string | File | null| Blob; // Store image as base64 string or Blob URL + setImage: (image: string | File|Blob) => void; // Function to update the image +} + +export const useStore = create((set) => ({ + image: null, + setImage: (image) => set({ image }), // Set the image in state +})); + +export const useStoreMember = create((set) => ({ + image: null, + setImage: (image) => set({ image }), // Set the image in state +})); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7f44b78..3346898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,8 @@ "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "typescript": "^5.3.3", - "uuid": "^11.0.3" + "uuid": "^11.0.3", + "zustand": "^5.0.2" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7", @@ -5246,6 +5247,17 @@ "node": ">=12.4.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7479,6 +7491,24 @@ "loose-envify": "^1.1.0" } }, + "node_modules/@react-three/fiber/node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@react-types/accordion": { "version": "3.0.0-alpha.21", "resolved": "https://registry.npmjs.org/@react-types/accordion/-/accordion-3.0.0-alpha.21.tgz", @@ -17760,19 +17790,31 @@ "peer": true }, "node_modules/zustand": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", - "peer": true, + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz", + "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==", + "license": "MIT", "engines": { - "node": ">=12.7.0" + "node": ">=12.20.0" }, "peerDependencies": { - "react": ">=16.8" + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, "react": { "optional": true + }, + "use-sync-external-store": { + "optional": true } } } diff --git a/package.json b/package.json index fa030f8..4ac583e 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "typescript": "^5.3.3", - "uuid": "^11.0.3" + "uuid": "^11.0.3", + "zustand": "^5.0.2" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7",