Repository: i4BSolutions/hope-fuel
Pull request #361: Update fundraiser form
Author: login: aye-nyeinSan
Created: 2025-03-27T11:14:47Z | Merged: 2025-03-28T06:21:48Z

I implemented the API+UI for fundraiser update Form . 

**Postman API**
![image](https://github.com/user-attachments/assets/3f7ec6db-1b40-476f-ab8c-85f06a54fca6)

**UI**

![image](https://github.com/user-attachments/assets/7843c4d2-4ee6-4e18-9c27-ad335fbdab34)



Diff:
diff --git a/src/app/api/v1/fundraisers/update/[id]/route.js b/src/app/api/v1/fundraisers/update/[id]/route.js
new file mode 100644
index 00000000..9061068f
--- /dev/null
+++ b/src/app/api/v1/fundraisers/update/[id]/route.js
@@ -0,0 +1,129 @@
+import { NextResponse } from "next/server";
+import db from "../../../../../utilites/db";
+
+async function GetBaseCountryID(BaseCountryName){
+  const query = `SELECT BaseCountryID FROM BaseCountry WHERE BaseCountryName = ?`;
+  const values = [BaseCountryName];
+  try{
+    const result = await db(query,values);
+    return result;
+  }
+  catch(error){
+    console.error("Error getting BaseCountryID:",error);
+    throw new Error("[DB] Error getting BaseCountryID:");
+  }
+
+}
+async function DeleteOldFundraiser_AcceptedCurrencies(id){
+    const query = `DELETE FROM Fundraiser_AcceptedCurrencies WHERE FundraiserID = ?`;
+    const values = [id];
+    try{
+        const result = await db(query,values);
+        return result;
+    }
+    catch(error){
+        console.error("Error deleting old Fundraiser_AcceptedCurrencies:",error);
+        throw new Error("[DB] Error deleting old Fundraiser_AcceptedCurrencies:");
+    }
+}
+async function CreateFundraiser_AcceptedCurrencies(FundraiserID,AcceptedCurrencies){
+    for (const currency of AcceptedCurrencies){
+        const [currencyID]= await db(`SELECT CurrencyId FROM Currency WHERE CurrencyCode = ?`,[currency]);
+        if(!currencyID){
+            throw new Error(`Currency ${currency} not found`);
+        }else{
+            await db(`INSERT INTO Fundraiser_AcceptedCurrencies(FundraiserID,CurrencyID) VALUES (?,?)`,[FundraiserID,currencyID.CurrencyId]);
+        }
+
+    }
+
+}
+async function DeleteOldFundraiser_ContactLinks(id){
+    const query = `DELETE FROM Fundraiser_ContactLinks WHERE FundraiserID = ?`;
+    const values = [id];
+    try{
+        const result = await db(query,values);
+        return result;
+    }
+    catch(error){
+        console.error("Error deleting old Fundraiser_ContactLinks:",error);
+        throw new Error("[DB] Error deleting old Fundraiser_ContactLinks:");
+    }
+}
+async function CreateFundraiser_ContactLinks(FundraiserID,ContactLinks){
+    for (const link of ContactLinks){
+        const safeUrl = link.url ?? "";
+        const [platformID] = await db(`SELECT PlatformID FROM Platform WHERE PlatformName = ?`,[link.platform]);
+        if(!platformID){
+            throw new Error(`Platform ${link.PlatformName} not found`);
+        }else{
+            await db(`INSERT INTO Fundraiser_ContactLinks(FundraiserID,PlatformID,ContactURL) VALUES (?,?,?)`,[FundraiserID,platformID.PlatformID,safeUrl]);
+        }
+
+    }
+
+}
+async function UpdateFundraiser(fundraiserID,data){
+  const BaseCountryName = data.BaseCountryName;
+  const DBBaseCountryName = await GetBaseCountryID(BaseCountryName);
+    if(!DBBaseCountryName){ 
+        throw new Error(`BaseCountry ${BaseCountryName} not found`);
+    }
+  const query = `UPDATE Fundraiser SET
+    FundraiserName = ?,
+    FundraiserEmail = ?,
+    FundraiserLogo = ?, 
+    BaseCountryID = ?, 
+    FundraiserCentralID = ?
+   WHERE FundraiserID = ?`;
+
+ const values = [
+   data.FundraiserName,
+   data.FundraiserEmail,
+   data.FundraiserLogo,
+   DBBaseCountryName[0].BaseCountryID,
+   data.FundraiserCentralID,
+   fundraiserID,
+ ];
+    try{
+        const result = await db(query, values);
+        return result;
+    }
+    catch(error){
+        throw new Error("[DB] Error updating fundraiser:");
+    }
+}
+
+export async function PUT(req, {params}){
+  const { id } = params;
+  const data = await req.json();
+  console.log("PUT::: ", data);
+ 
+  const contactLinks = [
+    { platform: "Facebook", url: data.FacebookLink },
+    { platform: "Telegram", url: data.TelegramLink },
+    { platform: "Others", url: data.OtherLink1 },
+    { platform: "Others", url: data.OtherLink2 },
+  ];
+
+  try {
+    await UpdateFundraiser(id, data);
+    await DeleteOldFundraiser_AcceptedCurrencies(id);
+    await CreateFundraiser_AcceptedCurrencies(id, data.AcceptedCurrencies);
+    await DeleteOldFundraiser_ContactLinks(id);
+    await CreateFundraiser_ContactLinks(id, contactLinks);
+
+   return NextResponse.json({
+     message: "Fundraiser updated successfully",
+     status: 200,
+   
+   });
+
+  } catch (error) {
+    return NextResponse.json(
+      { message: "Failed to update fundraiser", error: error.message },
+      { status: 500 }
+    );
+
+  }
+}
\ No newline at end of file
diff --git a/src/app/fundraisers/[id]/edit/page.js b/src/app/fundraisers/[id]/edit/page.js
new file mode 100644
index 00000000..23a669d8
--- /dev/null
+++ b/src/app/fundraisers/[id]/edit/page.js
@@ -0,0 +1,65 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import FundraisingForm from "../../components/FundraisingForm";
+import { useParams } from "next/navigation";
+import Modal from "../../../components/Modal";
+
+function FundraiserEditPage() {
+  const { id } = useParams();
+
+  const [fundraiser, setFundraiser] = useState(null);
+
+  useEffect(() => {
+    const fetchFundraiser = async () => {
+      const response = await fetch(`/api/v1/fundraisers/details/${id}`);
+      const result = await response.json();
+      if (response.ok && result.data) {
+        const transformed = {
+          ...result.data,
+          ...result.data.ContactLinks,
+        };
+        setFundraiser(transformed);
+      }
+    };
+
+    fetchFundraiser();
+  }, []);
+
+  const handleSubmit = async (data) => {
+    console.log("Data:", data);
+    try {
+      await fetch(`/api/v1/fundraisers/update/${id}`, {
+        method: "PUT",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(data),
+      });
+      
+    } catch (error) {
+      console.log("Error:", error);
+      throw new Error("Failed to update fundraiser");
+      
+    }
+   
+  };
+
+
+  return (
+    <>
+      {fundraiser ? (
+        <Modal maxWidth="sm" maxHeight="100vh">
+          <FundraisingForm
+            defaultValues={fundraiser}
+            onSubmitHandler={handleSubmit}
+          />
+        </Modal>
+      ) : (
+        <p>Loading...</p>
+      )}
+    </>
+  );
+}
+
+export default FundraiserEditPage;
diff --git a/src/app/fundraisers/components/FundraisingForm.js b/src/app/fundraisers/components/FundraisingForm.js
index 715c52f3..4558115e 100644
--- a/src/app/fundraisers/components/FundraisingForm.js
+++ b/src/app/fundraisers/components/FundraisingForm.js
@@ -2,48 +2,76 @@
 
 import { useForm } from "react-hook-form";
 import { zodResolver } from "@hookform/resolvers/zod";
-import { useState } from "react";
+import { useState,useMemo ,useEffect} from "react";
 import { Box, Button, Grid, TextField, Typography ,Alert} from "@mui/material";
 import { LogoUpload } from "./LogoUpload";
 import { FundraisingSchema } from "../schema";
 import BaseCountry from "./BaseCountry";
-import { useRouter } from "next/navigation";
+import {  useRouter } from "next/navigation";
 import AcceptedCurrency from "./AcceptedCurrency";
 
 
-
-const FundraisingForm = () => {
+const FundraisingForm = ({defaultValues={}, onSubmitHandler}) => {
   const router = useRouter();
   
   const [logoFile, setLogoFile] = useState(null);
   const [Completed, setCompleted] = useState(false);
-  const {
-    control,
-    register,
-    handleSubmit,
-    setValue,
-    clearErrors,
-    reset,
-    formState: { errors },
-  } = useForm({
-    resolver: zodResolver(FundraisingSchema),
-    defaultValues: {
+  const initialValues = useMemo(
+    () => ({
       FundraiserName: "",
       FundraiserEmail: "",
       FundraiserCentralID: null,
       BaseCountryName: "",
-      FundraiserLogo: "",
+      FundraiserLogo:  "",
       NewCountry: "",
       AcceptedCurrencies: [],
       FacebookLink: "",
       TelegramLink: "",
       OtherLink1: "",
       OtherLink2: "",
-    },
+      ...defaultValues,
+    }),
+    [defaultValues]
+  );
+  const {
+    control,
+    register,
+    handleSubmit,
+    setValue,
+    clearErrors,
+    reset,
+    formState: { errors },
+  } = useForm({
+    resolver: zodResolver(FundraisingSchema),
+    defaultValues: initialValues,
   });
+
+ 
+useEffect(() => {
+  if (defaultValues?.FundraiserLogo) {
+    setLogoFile(defaultValues.FundraiserLogo);
+    setValue("FundraiserLogo", defaultValues.FundraiserLogo);
+  }
+}, [defaultValues, setValue]);
+
+
   const handleClose = () => router.back();
   const onSubmit = async (data) => {
+      if(onSubmitHandler){
+        try{
+          onSubmitHandler(data);
+          setCompleted(true);
+            setTimeout(() => {
+              setCompleted(false);
+            }, 5000);
+         
+        }catch(error){
+          console.log("Error:", error);
+          throw new Error("Failed to update fundraiser");
+        }
 
+        return ;
+      }
     if (data.BaseCountryName === "other" && data.NewCountry) {
       data.BaseCountryName = data.NewCountry.trim();
       delete data.NewCountry;
@@ -68,7 +96,6 @@ const FundraisingForm = () => {
          setTimeout(() => {
            setCompleted(false);
          }, 3000);
-        
       } else {
         setCompleted(false);
         throw new Error(result.message);
@@ -90,9 +117,8 @@ const FundraisingForm = () => {
   return (
     <Box
       sx={{
-        p: 4,
+        padding: 2,
         maxWidth: 600,
-        margin: "auto",
         bgcolor: "white",
         borderRadius: 3,
       }}
@@ -103,7 +129,7 @@ const FundraisingForm = () => {
             <LogoUpload
               logoFile={logoFile}
               setLogoFile={(url) => {
-                if (url.length === 0) {
+                if (!url) {
                   return;
                 }
                 setLogoFile(url);
@@ -131,7 +157,7 @@ const FundraisingForm = () => {
               label="Fundraiser ID"
               variant="outlined"
               fullWidth
-              {...register("FundraiserCentralID" ,{ valueAsNumber: true })}
+              {...register("FundraiserCentralID", { valueAsNumber: true })}
               error={!!errors.FundraiserCentralID}
               helperText={errors.FundraiserCentralID?.message}
               InputProps={{
@@ -224,7 +250,7 @@ const FundraisingForm = () => {
           </Grid>
           <Grid item xs={6}>
             <Button fullWidth variant="contained" color="primary" type="submit">
-              Create
+              {defaultValues ? "Save Changes" : "Create"}
             </Button>
           </Grid>
           {Completed && (
@@ -237,7 +263,9 @@ const FundraisingForm = () => {
                 marginX: "auto",
               }}
             >
-              Fundraiser created successfully!!
+              {onSubmitHandler
+                ? "Changes saved successfully!"
+                : "Fundraiser created successfully!"}
             </Alert>
           )}
         </Grid>
diff --git a/src/app/fundraisers/components/LogoUpload.js b/src/app/fundraisers/components/LogoUpload.js
index 55c704eb..b05a9882 100644
--- a/src/app/fundraisers/components/LogoUpload.js
+++ b/src/app/fundraisers/components/LogoUpload.js
@@ -12,6 +12,8 @@ export const LogoUpload = ({logoFile, setLogoFile, errors, clearErrors}) => {
    useEffect(() => {
      if (!logoFile) {
        setLogoPreview(null); 
+     }else{
+       setLogoPreview(logoFile);
      }
    }, [logoFile]);
 
diff --git a/src/app/fundraisers/page.js b/src/app/fundraisers/page.js
index 7db35efb..2f4b2b31 100644
--- a/src/app/fundraisers/page.js
+++ b/src/app/fundraisers/page.js
@@ -106,6 +106,9 @@ const FundraisingFormPage = () => {
     () => setOpenFundraiserDetailsModal((prev) => !prev),
     []
   );
+  const handleEdit = (id) => {
+      router.push(`/fundraisers/${id}/edit`);
+  }
 
   const handleOpenFundraiserDeleteModal = useCallback((id) => {
     setFundraiserID(id);
@@ -178,33 +181,39 @@ const FundraisingFormPage = () => {
         }}
       >
         {fundraisers.length > 0 ? (
-          <Container maxWidth="xl">
-            <Grid2
-              container
-              spacing={{ xs: 2, md: 3 }}
-              columns={{ xs: 4, sm: 8, md: 12, lg: 12 }}
-            >
-              {fundraisers.map((fundraiser, index) => (
-                <Grid2 xs={12} sm={6} md={3} lg={3} key={index}>
-                  <FundraiserCard
-                    fundraiser={fundraiser}
-                    onClick={handleOpenFundraiserDetailsModal}
-                  />
-                </Grid2>
-              ))}
-            </Grid2>
-          </Container>
-        ) : (
-          <Box
-            sx={{
-              display: "flex",
-              justifyContent: "center",
-              alignItems: "center",
-            }}
-          >
-            <Typography>There is no fundraisers!</Typography>
-          </Box>
-        )}
+  <Container maxWidth="xl">
+    <Grid2
+      container
+      spacing={{ xs: 2, md: 3 }}
+      columns={{ xs: 4, sm: 8, md: 12, lg: 12 }}
+    >
+      {fundraisers.map((fundraiser, index) => (
+        <Grid2 xs={12} sm={6} md={3} lg={3} key={index}>
+          <FundraiserCard
+            fundraiser={fundraiser}
+            onClick={handleOpenFundraiserDetailsModal}
+            onEdit={handleEdit} 
+          />
+        </Grid2>
+      ))}
+    </Grid2>
+  </Container>
+) : (
+  <Box
+    sx={{
+      display: "flex",
+      justifyContent: "center",
+      alignItems: "center",
+      height: "300px",
+    }}
+  >
+    <Typography variant="h6" color="text.secondary">
+      There are no fundraisers!
+    </Typography>
+  </Box>
+)}
+
+
         <Modal
           open={openFundraiserDetailsModal}
           onClose={handleCloseFundraiserDetailsModal}
@@ -226,7 +235,11 @@ const FundraisingFormPage = () => {
             <FundraiserDetails
               fundraiserDetails={fundraiserDetails}
               onClose={handleCloseFundraiserDetailsModal}
+
+              onEdit={handleEdit}
+
               onDelete={handleOpenFundraiserDeleteModal}
+
             />
           )}
         </Modal>


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request implements an update for the fundraiser form by introducing an edit functionality on the UI and providing a corresponding API endpoint for updating fundraiser details. Key changes include:
- Adding an onEdit handler in the fundraiser list and details modal.
- Refactoring the FundraisingForm component to support pre-filled default values and custom submit handling.
- Creating a dedicated edit page and an API route for fundraiser updates.

### Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/app/fundraisers/page.js | Added onEdit handler and passed it to FundraiserCard and FundraiserDetails components. |
| src/app/fundraisers/components/LogoUpload.js | Updated logo preview logic with an else branch. |
| src/app/fundraisers/components/FundraisingForm.js | Refactored to accept defaultValues/onSubmitHandler, updated UI texts, and adjusted error handling. |
| src/app/fundraisers/[id]/edit/page.js | Created a new client page for editing an existing fundraiser. |
| src/app/api/v1/fundraisers/update/[id]/route.js | Implemented the API endpoint to update fundraiser details with related database operations. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/app/fundraisers/components/FundraisingForm.js:20**
* [nitpick] The state variable 'Completed' uses a capital letter; it is recommended to use camelCase (e.g., 'completed') for consistency with React state naming conventions.
```
const [Completed, setCompleted] = useState(false);
```
**src/app/api/v1/fundraisers/update/[id]/route.js:58**
* The error message references 'link.PlatformName' which may be undefined. It would be clearer to use 'link.platform' (consistent with the SELECT query) in the error message.
```
throw new Error(`Platform ${link.PlatformName} not found`);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T11:15:36Z
user: login: thumin-dev
body: looks good!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T04:39:38Z

Review comments:
diff_hunk: @@ -2,48 +2,79 @@
 
 import { useForm } from "react-hook-form";
 import { zodResolver } from "@hookform/resolvers/zod";
-import { useState } from "react";
+import { useState,useMemo ,useEffect} from "react";
 import { Box, Button, Grid, TextField, Typography ,Alert} from "@mui/material";
 import { LogoUpload } from "./LogoUpload";
 import { FundraisingSchema } from "../schema";
 import BaseCountry from "./BaseCountry";
-import { useRouter } from "next/navigation";
+import {  useRouter } from "next/navigation";
 import AcceptedCurrency from "./AcceptedCurrency";
+import { set } from "date-fns";
 
path: src/app/fundraisers/components/FundraisingForm.js
user: login: Copilot
body: The import of 'set' from date-fns is not used in the component. Consider removing it to clean up the code.
```suggestion

```
created_at: 2025-03-27T11:15:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,48 +2,79 @@
 
 import { useForm } from "react-hook-form";
 import { zodResolver } from "@hookform/resolvers/zod";
-import { useState } from "react";
+import { useState,useMemo ,useEffect} from "react";
 import { Box, Button, Grid, TextField, Typography ,Alert} from "@mui/material";
 import { LogoUpload } from "./LogoUpload";
 import { FundraisingSchema } from "../schema";
 import BaseCountry from "./BaseCountry";
-import { useRouter } from "next/navigation";
+import {  useRouter } from "next/navigation";
 import AcceptedCurrency from "./AcceptedCurrency";
+import { set } from "date-fns";
 
 
 
-const FundraisingForm = () => {
+const FundraisingForm = ({defaultValues={}, onSubmitHandler}) => {
   const router = useRouter();
   
   const [logoFile, setLogoFile] = useState(null);
   const [Completed, setCompleted] = useState(false);
-  const {
-    control,
-    register,
-    handleSubmit,
-    setValue,
-    clearErrors,
-    reset,
-    formState: { errors },
-  } = useForm({
-    resolver: zodResolver(FundraisingSchema),
-    defaultValues: {
+  const initialValues = useMemo(
+    () => ({
       FundraiserName: "",
       FundraiserEmail: "",
       FundraiserCentralID: null,
       BaseCountryName: "",
-      FundraiserLogo: "",
+      FundraiserLogo:  "",
       NewCountry: "",
       AcceptedCurrencies: [],
       FacebookLink: "",
       TelegramLink: "",
       OtherLink1: "",
       OtherLink2: "",
-    },
+      ...defaultValues,
+    }),
+    [defaultValues]
+  );
+  const {
+    control,
+    register,
+    handleSubmit,
+    setValue,
+    clearErrors,
+    reset,
+    formState: { errors },
+  } = useForm({
+    resolver: zodResolver(FundraisingSchema),
+    defaultValues: initialValues,
   });
+
+ 
+useEffect(() => {
+  if (defaultValues?.FundraiserLogo) {
+    setLogoFile(defaultValues.FundraiserLogo);
+    setValue("FundraiserLogo", defaultValues.FundraiserLogo);
+  }
+}, [defaultValues, setValue]);
+
+
   const handleClose = () => router.back();
   const onSubmit = async (data) => {
+      if(onSubmitHandler){
+        try{
+          onSubmitHandler(data);
+          setCompleted(true);
+            setTimeout(() => {
+              setCompleted(false);
+            }, 5000);
+         
+        }catch(error){
+          console.log("Error:", error);
+          throw new Error("Failed to updatefundraiser");
 
path: src/app/fundraisers/components/FundraisingForm.js
user: login: Copilot
body: The error message contains a typo. Consider changing it to 'Failed to update fundraiser' for clarity.
```suggestion
          throw new Error("Failed to update fundraiser");
```
created_at: 2025-03-27T11:15:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-27T18:32:16Z
body: Hey @aye-nyeinSan, your PR has pending review comments for more than 6 hours. Please address them soon!
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-28T01:51:11Z
body: Hey @aye-nyeinSan, your PR has pending review comments for more than 6 hours. Please address them soon!
author_association: NONE
user: login: akm-coding
created_at: 2025-03-28T03:46:04Z
body: LGTM!
author_association: CONTRIBUTOR
user: login: aye-nyeinSan
created_at: 2025-03-28T04:37:07Z
body: I am working on conflicts and merge it into `development`
author_association: CONTRIBUTOR