Repository: ICPCPlatform/icpc-platform
Pull request #45: Training edit material
Author: login: m10090
Created: 2025-04-15T16:24:29Z | Merged: 2025-04-26T19:57:20Z

This is a basic edit material page with the backend logic 
the design is not final 
feel free to edit the code 


Diff:
diff --git a/src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/_action.ts b/src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/_action.ts
index 22516713..e17462e2 100644
--- a/src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/_action.ts
+++ b/src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/_action.ts
@@ -3,26 +3,24 @@ import "server-only";
 import { db } from "@/lib/db";
 import { Blocks } from "@/lib/db/schema/training/Blocks";
 import { getUserTrainingPermissions } from "@/lib/permissions/getUserTrainingPermissions";
-import { UserDataJWT } from "@/lib/session";
-import { Material } from "@/lib/types/Training";
+import { getUserData } from "@/lib/session";
+import { Material} from "@/lib/types/Training";
 import { and, eq } from "drizzle-orm";
 
 export async function editMaterial({
   trainingId,
   blockNumber,
-  newMaterial,
+  newMaterials,
 }: {
   trainingId: number;
   blockNumber: number;
-  newMaterial: Material;
+  newMaterials: Material[];
 }) {
-  // get user id from headers
-  const headers = new Headers();
-
-  const { userId } = JSON.parse(
-    headers.get("x-user") ?? "{'userId':''}",
-  ) as UserDataJWT;
-
+  const userData = await getUserData();
+  if (!userData) {
+    return;
+  }
+  const userId = userData.userId;
   if (isNaN(trainingId)) {
     return;
   }
@@ -31,11 +29,13 @@ export async function editMaterial({
   if (!userPermissions.includes("Edit:material")) {
     return;
   }
+  
 
   await db
     .update(Blocks)
     .set({
-      material: newMaterial,
+      material: newMaterials,
+      // material: newMaterial,
     })
     .where(
       and(
diff --git a/src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/page.tsx b/src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/page.tsx
index 5d56728f..a72cbdff 100644
--- a/src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/page.tsx
+++ b/src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/page.tsx
@@ -1,6 +1,10 @@
 "use client";
 import { redirect } from "next/navigation";
 import { editMaterial } from "./_action";
+import { useState } from "react";
+import { Material  } from "@/lib/types/Training";
+import { useTrainingContext } from "@/providers/training";
+import { Button } from "@/components/ui/button";
 
 export default function Page() {
   // get the trainingId and blockId from the url
@@ -9,20 +13,109 @@ export default function Page() {
   const trainingId = Number(urlParts[urlParts.length - 3]);
   const blockId = Number(urlParts[urlParts.length - 1]);
   if (isNaN(trainingId) || isNaN(blockId)) {
-    redirect('not-found');
+    redirect("not-found");
   }
+  const training = useTrainingContext();
+  if (training === null) {
+    return;
+  }
+  
+  const materialData = training.materials[blockId.toString()];
+
+  return (
+    <div>
+      <DynamicForm
+        materialData={materialData}
+        trainingId={trainingId}
+        blockNumber={blockId}
+      />
+    </div>
+  );
+}
+
+function DynamicForm({
+  materialData,
+  trainingId,
+  blockNumber,
+}: {
+  materialData: Material[];
+  trainingId: number;
+  blockNumber: number;
+}) {
+  const [entries, setEntries] = useState<Material[]>(materialData ?? []);
+
+  const addEntry = () => {
+    setEntries([...entries, { title: "", link: "", des: "" }]);
+  };
+
+  const updateEntry = (
+    index: number,
+    field: "link" | "title" | "des",
+    value: string,
+  ) => {
+    const newEntries = [...entries];
+    newEntries[index][field] = value;
+    setEntries(newEntries);
+  };
+
+  const deleteEntry = (index: number) => {
+    const newEntries = [...entries];
+    newEntries.splice(index, 1);
+    setEntries(newEntries);
+  };
+
+  return (
+    <div className="p-6 max-w-2xl mx-auto">
+      <h2 className="text-2xl font-bold mb-4">Dynamic Form</h2>
+
+      {entries.map((entry, index) => (
+        <div key={index} className="border p-4 rounded-xl mb-4 shadow">
+          <input
+            type="text"
+            placeholder="Title"
+            value={entry.title}
+            onChange={(e) => updateEntry(index, "title", e.target.value)}
+            className="w-full mb-2 p-2 border rounded"
+          />
+          <input
+            type="text"
+            placeholder="Link"
+            value={entry.link}
+            onChange={(e) => updateEntry(index, "link", e.target.value)}
+            className="w-full mb-2 p-2 border rounded"
+          />
+          <textarea
+            placeholder="Description"
+            value={entry.des}
+            onChange={(e) => updateEntry(index, "des", e.target.value)}
+            className="w-full mb-2 p-2 border rounded"
+          ></textarea>
+          <button
+            onClick={() => deleteEntry(index)}
+            className="text-red-500 hover:text-red-700 text-sm"
+          >
+            Delete
+          </button>
+        </div>
+      ))}
+
+      <div className="flex gap-4">
+        <button
+          onClick={addEntry}
+          className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
+        >
+          Add
+        </button>
+      </div>
 
-  return <div>
-    <h1>Edit Material</h1>
-    <p>This is the edit material page.</p>
-    <p>Implement the edit material functionality here.</p>
-    <pre>{JSON.stringify({ trainingId, blockId }, null, 2)}</pre>
-    <form action={editMaterial}>
-      <input type="hidden" name="trainingId" value={trainingId} />
-      <input type="hidden" name="blockId" value={blockId} />
-      <input type="text" name="newMaterialName" placeholder="New Material Name" required />
-
-      <button type="submit">Edit Material</button>
-    </form>
-  </div>;
+      <Button
+        onClick={() => {
+          const newMaterials =  entries ;
+          editMaterial({ blockNumber, trainingId, newMaterials });
+        }}
+      >
+        apply changes
+      </Button>
+    </div>
+  );
 }


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

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

<details>
<summary>Files not reviewed (2)</summary>

* **src/app/api/auth/__tests__/auth.rest**: Language not supported
* **src/app/api/training/__tests__/training.rest**: Language not supported
</details>

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

**src/app/protected/trainings/[trainingId]/contests/[contestId]/standing/page.tsx:27**
* Consider cleaning up the dynamically inserted CSS rules upon component unmount to prevent potential duplication if the component is re-mounted.
```
styleSheet.insertRule(`#standingTable  td:nth-child(${idx + 1}) , #standingTable th:nth-child(${idx + 1}) { left: ${offset}px; position: sticky; }`);
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T16:25:08Z
user: login: TryOmar
body: Changes made so far look good, and I’ve accepted them. For the upcoming improvements, we’ve opened a new issue here: [#47](https://github.com/ICPCPlatform/icpc-platform/issues/47).

state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-26T19:53:23Z
user: login: cgmoreda
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-26T19:57:10Z

Review comments:
diff_hunk: @@ -62,32 +62,33 @@ export async function getTrainingFullData({
 
     .execute()) as Trainee[];
   // Map standings to include trainee details
-  const standingWithDetails: TrainingFullData["standing"] = standing.map(
+  const standingWithDetails: TrainingFullDTO["standing"] = standing.map(
     (contest) => {
+      
       return {
         ...contest,
         rankings: contest.rankings
-          .map((s: StandingEntry) => {
+          .map((s: Ranking) => {
             if (s.userId === undefined) {
               return undefined;
             }
             const user = trainees.find((usr) => usr.userId === s.userId);
             if (user) {
-              user.userId = undefined;
               const obj = {
                 ...s,
                 ...user, // Assuming penalty is used as points
+                userId: undefined,
path: src/actions/getTrainingFullData.ts
user: login: Copilot
body: Instead of setting userId to undefined, consider omitting the field entirely (e.g., by destructuring it out) to avoid any accidental misuse of this property.
```suggestion
              const { userId, ...rest } = user;
              const obj = {
                ...s,
                ...rest, // Assuming penalty is used as points
```
created_at: 2025-04-15T16:25:08Z
author_association: NONE
diff_hunk: @@ -0,0 +1,62 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { useTrainingContext } from "@/providers/training";
+import Link from "next/link";
+
+
+
+const itemStyle = "w-full gap-3   text-sm font-medium rounded-md transition-colors whitespace-nowrap hover:bg-accent hover:text-accent-foreground my-2 box-border"
+export default function TrainingNavigation({
+  trainingId,
+}: {
+  trainingId: number;
+}) {
+  const training = useTrainingContext();
+  console.log(training);
path: src/app/protected/trainings/[trainingId]/__trainingNavigation.tsx
user: login: Copilot
body: Remove the debug console.log statement from production code.
```suggestion

```
created_at: 2025-04-15T16:25:08Z
author_association: NONE

PR comments:
user: login: TryOmar
created_at: 2025-04-15T16:55:31Z
body: Tested Adding Material, But not able to preview them after adding:
http://localhost:3000/protected/trainings/1/edit-materials/1

![image](https://github.com/user-attachments/assets/e61f4ad6-599c-4ed9-a4b8-0c9d8e8035e9)
![image](https://github.com/user-attachments/assets/8e08478f-065d-45de-9ce0-a36105650fad)

author_association: COLLABORATOR
user: login: TryOmar
created_at: 2025-04-15T17:05:42Z
body: Hey @m10090,
Could you please let us know what we should test to properly validate the material edit feature before merging?
Also, it would be helpful if you could share a quick summary of what has been completed and what’s still pending. Thanks!
author_association: COLLABORATOR
user: login: m10090
created_at: 2025-04-15T17:25:45Z
body: First: the naming of the url is wrong and this it will work with /materials 

Second: there is a bug in data fetching this is issue issue number #30 witch is assigned to reda 
author_association: COLLABORATOR
user: login: m10090
created_at: 2025-04-15T17:28:19Z
body: You can write an issue on the navigation bar but this will need some a lot of design and consideration this is needed for the different privilege navigation bar 
author_association: COLLABORATOR
user: login: TryOmar
created_at: 2025-04-15T17:50:28Z
body: > This is a basic edit material page with the backend logic the design is not final feel free to edit the code

You're saying that this is just a "edit material" page, but there are 59 files edited It seems like changes have been made to register, validations, types, and middlewares as well:

![image](https://github.com/user-attachments/assets/25e36f33-4d07-4c83-a0c4-35058ec00a63)

I would appreciate some clarification on why 59 files were changed, as this seems like a full project refactor. I'm a bit confused about what exactly was done here. A summary of the changes and how we should test the feature would be really helpful.

> Hey @m10090, Could you please let us know what we should test to properly validate the material edit feature before merging? Also, it would be helpful if you could share a quick summary of what has been completed and what’s still pending. Thanks!


Looking forward to your response, thanks!

author_association: COLLABORATOR
user: login: m10090
created_at: 2025-04-15T17:52:54Z
body: you will need to first look at the other pull request approve it and then you will view only the file change for edit-material page
#40
author_association: COLLABORATOR
user: login: TryOmar
created_at: 2025-04-26T19:36:40Z
body: 
![image](https://github.com/user-attachments/assets/6bf4ac34-6e1e-4fcf-87c0-a106d34af83f)


Summary of Changes:

1. **File:** `src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/_action.ts`
   - **Completed Features:**
     - Replaced manual user ID extraction with `getUserData()` for better session handling.
     - Updated backend logic to support multiple materials (`newMaterials: Material[]`).
     - Removed unused headers-based user ID logic.
     
   - **Incomplete Features:**
     - Missing validation for `newMaterials` (e.g., ensuring all entries are valid).
     - No error handling for `getUserData()` or database operations.

2. **File:** `src/app/protected/trainings/[trainingId]/edit-materials/[blockId]/page.tsx`
   - **Completed Features:**
     - Implemented a dynamic form to handle adding, updating, and deleting materials.
     - Integrated `useTrainingContext()` to fetch materials for the specific block.
     - Added UI components for managing materials (inputs, buttons, etc.).
     
   - **Incomplete Features:**
     - Missing input validation (e.g., ensuring title, link, and description are valid).
     - No error feedback for API errors or failed operations.
     - No handling for empty or missing material data in the UI.

General Notes:
- The backend and frontend changes align with supporting multiple materials but require additional error handling and data validation.

author_association: COLLABORATOR