Skip to content

Commit

Permalink
Merge pull request #40 from cearps/UI-Form-for-create-and-edit-Board
Browse files Browse the repository at this point in the history
Create, edit and delete boards from main boards page
  • Loading branch information
SovannarithHayGH authored Sep 8, 2024
2 parents af0760a + da588b1 commit 94b246c
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 90 deletions.
30 changes: 30 additions & 0 deletions frontend/src/api/kanbanAPI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ export default class KanbanAPI {
return response.data as KanbanBoard[];
}

// Step 1: Create an empty board (no details yet)
static async createKanbanBoard(): Promise<KanbanBoard> {
const token = localStorage.getItem("token");
const response = await axios.post(`${API_URL}/kanbans`, {}, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data as KanbanBoard;
}

// Step 2: Use the returned board ID to populate the details
static async updateKanbanBoard(id: number, data: Partial<KanbanBoard>): Promise<KanbanBoard> {
const token = localStorage.getItem("token");
const response = await axios.post(`${API_URL}/kanbans/${id}`, data, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data as KanbanBoard;
}

static async getKanbanBoard(id: string): Promise<KanbanBoard> {
const token = localStorage.getItem("token");
const response = await axios.get(`${API_URL}/kanbans/${id}`, {
Expand All @@ -25,6 +43,8 @@ export default class KanbanAPI {
return response.data as KanbanBoard;
}



static getKanbanBoardsObservable(): Observable<KanbanBoard[]> {
return concat(of(0), interval(1000)).pipe(
switchMap(() => from(KanbanAPI.getKanbanBoards())),
Expand All @@ -44,4 +64,14 @@ export default class KanbanAPI {
})
);
}

// Add a function to delete a Kanban board
static async deleteKanbanBoard(id: number): Promise<void> {
const token = localStorage.getItem("token");
await axios.delete(`${API_URL}/kanbans/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});

}

}
7 changes: 4 additions & 3 deletions frontend/src/components/buttons/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import React from "react";
import { ButtonType } from "../../utilities/types";

type ButtonProps = {
type: ButtonType;
type?: ButtonType; // Make the type optional for non-submit buttons
children: React.ReactNode;
onClick?: () => void;
className?: string;
};

const Button = ({ type, children, onClick }: ButtonProps) => {
const Button = ({ type = "button", children, onClick, className = "" }: ButtonProps) => {
return (
<button
type={type}
onClick={onClick}
className="w-full py-2 px-4 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-md"
className={`w-full py-2 px-4 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-md ${className}`}
>
{children}
</button>
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/components/forms/add-board-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useState, useEffect } from "react";
import Button from "../buttons/button";

const AddBoardForm = ({
isOpen,
onClose,
onSubmit,
defaultValues,
}: {
isOpen: boolean;
onClose: () => void;
onSubmit: (name: string, dueDate: string, description: string) => void;
defaultValues?: { name: string; dueDate: string; description: string };
}) => {
const [name, setName] = useState(defaultValues?.name || "");
const [dueDate, setDueDate] = useState(defaultValues?.dueDate || "");
const [description, setDescription] = useState(defaultValues?.description || "");

useEffect(() => {
setName(defaultValues?.name || "");
setDueDate(defaultValues?.dueDate || "");
setDescription(defaultValues?.description || "");
}, [defaultValues]);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(name, dueDate, description);
};

if (!isOpen) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white rounded-lg shadow-lg w-full max-w-md p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">{defaultValues ? "Edit Board" : "Create New Board"}</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-700">
&times;
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">Board Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter board name"
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">Due Date</label>
<input
type="date"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter description"
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
required
/>
</div>
<div className="flex justify-end">
<Button type="submit">{defaultValues ? "Update Board" : "Create Board"}</Button>
<Button onClick={onClose} className="ml-2 bg-gray-300 hover:bg-gray-400 text-black">
Cancel
</Button>
</div>
</form>
</div>
</div>
);
};

export default AddBoardForm;
207 changes: 120 additions & 87 deletions frontend/src/components/kanbans/kanban-list-view.tsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,152 @@
import { useEffect, useState } from "react";
import { useState, useEffect } from "react";
import KanbanAPI from "../../api/kanbanAPI";
import { useNavigate } from "react-router-dom";
import { KanbanBoard } from "../../utilities/types";
import AddBoardForm from "../forms/add-board-form";

export default function KanbanListView() {
const [kanbanBoards, setKanbanBoards] = useState([] as KanbanBoard[]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingBoard, setEditingBoard] = useState<KanbanBoard | null>(null);

useEffect(() => {
const subscription = KanbanAPI.getKanbanBoardsObservable().subscribe({
next: (boards) => {
if (boards === null) {
return;
}
const fetchBoards = async () => {
try {
const boards = await KanbanAPI.getKanbanBoards();
setKanbanBoards(boards);
},
error: (error) => {
} catch (error) {
console.error("Error fetching Kanban boards:", error);
},
});

// prevent memory leak
return () => {
subscription.unsubscribe();
}
};

fetchBoards();
}, []);

const handleAddBoard = async (name: string, dueDate: string, description: string) => {
try {
if (editingBoard) {
// If we're editing a board, update it
const updatedBoard = await KanbanAPI.updateKanbanBoard(editingBoard.id, { name, dueDate, description });
setKanbanBoards((prevBoards) => prevBoards.map((board) => (board.id === updatedBoard.id ? updatedBoard : board)));
setEditingBoard(null); // Reset after editing
} else {
// Otherwise, create a new board
const newBoard = await KanbanAPI.createKanbanBoard();
const updatedBoard = await KanbanAPI.updateKanbanBoard(newBoard.id, { name, dueDate, description });
setKanbanBoards((prevBoards) => [...prevBoards, updatedBoard]);
}
setIsModalOpen(false);
} catch (error) {
console.error("Error creating or updating the Kanban board:", error);
}
};

// Function to handle deleting a board
const handleDeleteBoard = async (id: number) => {
try {
await KanbanAPI.deleteKanbanBoard(id);
setKanbanBoards((prevBoards) => prevBoards.filter((board) => board.id !== id));
} catch (error) {
console.error("Error deleting Kanban board:", error);
}
};

// Function to handle editing a board
const handleEditBoard = (board: KanbanBoard) => {
setEditingBoard(board);
setIsModalOpen(true);
};

return (
<div>
<h1 className="text-3xl font-bold mb-6">YOUR PROJECTS</h1>
<div className="bg-yellow-500 rounded-full w-14 h-14 flex items-center justify-center m-2 cursor-pointer">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
className="w-8 h-8"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v12m6-6H6"
/>
<div
className="bg-yellow-500 rounded-full w-14 h-14 flex items-center justify-center m-2 cursor-pointer transition-transform hover:scale-110"
onClick={() => setIsModalOpen(true)}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="w-8 h-8">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
</svg>
</div>
<div className="flex flex-wrap">
{kanbanBoards.map((board: KanbanBoard) => (
<BoardCard
key={board.id}
id={board.id.toString()}
id={board.id}
title={board.name}
dueDate={board.dueDate}
onDelete={handleDeleteBoard}
onEdit={() => handleEditBoard(board)}
/>
))}
</div>
</div>
);
}

const BoardCard = ({
id,
title,
dueDate,
}: {
id: string;
title: string;
dueDate: string;
}) => {
const navigate = useNavigate();
{/* AddBoardForm Modal */}
<AddBoardForm
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setEditingBoard(null);
}}
onSubmit={handleAddBoard}
defaultValues={editingBoard ? { name: editingBoard.name, dueDate: editingBoard.dueDate, description: editingBoard.description } : undefined} // Use undefined when editingBoard is null
/>

const handleCardClick = (id: string) => {
navigate(`/boards/${id}`);
};
</div>
);

return (
<div
className="bg-yellow-500 text-black rounded-lg shadow-md p-4 m-2 w-56 h-36 relative border rounded cursor-pointer hover:bg-gray-100 hover:shadow-lg transition-transform transform hover:scale-105"
onClick={() => handleCardClick(id)}
>
<div className="flex justify-between">
<h3 className="font-bold text-lg">{title}</h3>
<button className="text-black">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
}
const BoardCard = ({
id,
title,
dueDate,
onDelete,
onEdit,
}: {
id: number;
title: string;
dueDate: string;
onDelete: (id: number) => void;
onEdit: () => void;
}) => {
const navigate = useNavigate();

const handleCardClick = (id: number) => {
navigate(`/boards/${id}`);
};

return (
<div
className="bg-yellow-500 text-black rounded-lg shadow-md p-4 m-2 w-56 h-36 relative border rounded cursor-pointer hover:bg-gray-100 hover:shadow-lg transition-transform transform hover:scale-105"
onClick={() => handleCardClick(id)}
>
<div className="flex justify-between">
<h3 className="font-bold text-lg">{title}</h3>

<button className="text-black" onClick={(e) => {
e.stopPropagation();
onEdit();
}}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" />
</svg>
</button>

<button className="text-black" onClick={(e) => {
e.stopPropagation();
onDelete(id);
}}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</div>
<p className="absolute bottom-4 left-4 text-sm flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4 mr-1">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
Due {dueDate}
</p>
</div>
<p className="absolute bottom-4 left-4 text-sm flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4 mr-1"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
Due {dueDate}
</p>
</div>
);
};
);
};
Loading

0 comments on commit 94b246c

Please sign in to comment.