From 348bca0b402ea0cc0d73555b8e081c8a737d5832 Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Wed, 27 Mar 2024 23:40:46 +0900 Subject: [PATCH] Refactor projects page's component and UI --- .../{project-dialog.tsx => ProjectDialog.tsx} | 4 +- frontend/app/projects/ProjectsPage.tsx | 108 ++++++++ frontend/app/projects/ProjectsTable.tsx | 162 ++++++++++++ frontend/app/projects/page.tsx | 232 +----------------- frontend/app/projects/project-card.tsx | 62 ----- frontend/app/projects/projectsControl.ts | 117 +++++++++ frontend/package-lock.json | 6 + frontend/package.json | 1 + frontend/types/project.ts | 7 + 9 files changed, 408 insertions(+), 291 deletions(-) rename frontend/app/projects/{project-dialog.tsx => ProjectDialog.tsx} (97%) create mode 100644 frontend/app/projects/ProjectsPage.tsx create mode 100644 frontend/app/projects/ProjectsTable.tsx delete mode 100644 frontend/app/projects/project-card.tsx create mode 100644 frontend/app/projects/projectsControl.ts create mode 100644 frontend/types/project.ts diff --git a/frontend/app/projects/project-dialog.tsx b/frontend/app/projects/ProjectDialog.tsx similarity index 97% rename from frontend/app/projects/project-dialog.tsx rename to frontend/app/projects/ProjectDialog.tsx index dc98493..8aeac1c 100644 --- a/frontend/app/projects/project-dialog.tsx +++ b/frontend/app/projects/ProjectDialog.tsx @@ -11,7 +11,7 @@ import { ModalBody, ModalFooter, } from "@nextui-org/react"; -import { ProjectType } from "./page"; +import { ProjectType } from "@/types/project"; type Props = { isOpen: boolean; @@ -20,7 +20,7 @@ type Props = { onSubmit: (name: string, detail: string) => void; }; -export function ProjectDialog({ +export default function ProjectDialog({ isOpen, editingProject, onCancel, diff --git a/frontend/app/projects/ProjectsPage.tsx b/frontend/app/projects/ProjectsPage.tsx new file mode 100644 index 0000000..f7e9af0 --- /dev/null +++ b/frontend/app/projects/ProjectsPage.tsx @@ -0,0 +1,108 @@ +"use client"; +import { useEffect, useState } from "react"; +import { Button } from "@nextui-org/react"; +import { Plus } from "lucide-react"; +import { ProjectType } from "@/types/project"; +import ProjectsTable from "./ProjectsTable"; +import ProjectDialog from "./ProjectDialog"; +import { + fetchProjects, + createProject, + updateProject, + deleteProject, +} from "./projectsControl"; + +export default function ProjectsPage() { + const [projects, setProjects] = useState([]); + + useEffect(() => { + async function fetchDataEffect() { + try { + const data = await fetchProjects(); + setProjects(data); + } catch (error) { + console.error("Error in effect:", error.message); + } + } + + fetchDataEffect(); + }, []); + + // dialog + const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false); + const [editingProject, setEditingProject] = useState( + null + ); + const openDialogForCreate = () => { + setIsProjectDialogOpen(true); + setEditingProject(null); + }; + + const closeDialog = () => { + setIsProjectDialogOpen(false); + setEditingProject(null); + }; + + const onSubmit = async (name: string, detail: string) => { + if (editingProject) { + const updatedProject = await updateProject( + editingProject.id, + name, + detail + ); + const updatedProjects = projects.map((project) => + project.id === updatedProject.id ? updatedProject : project + ); + setProjects(updatedProjects); + } else { + const newProject = await createProject(name, detail); + setProjects([...projects, newProject]); + } + closeDialog(); + }; + + const onEditClick = (project: ProjectType) => { + setEditingProject(project); + setIsProjectDialogOpen(true); + }; + + const onDeleteClick = async (projectId: number) => { + try { + await deleteProject(projectId); + setProjects(projects.filter((project) => project.id !== projectId)); + } catch (error) { + console.error("Error deleting project:", error); + } + }; + + return ( +
+
+

Projects

+
+ +
+
+ + + + +
+ ); +} diff --git a/frontend/app/projects/ProjectsTable.tsx b/frontend/app/projects/ProjectsTable.tsx new file mode 100644 index 0000000..b3e9499 --- /dev/null +++ b/frontend/app/projects/ProjectsTable.tsx @@ -0,0 +1,162 @@ +import { useState, useMemo, useCallback } from "react"; +import { + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + Button, + DropdownTrigger, + Dropdown, + DropdownMenu, + DropdownItem, + SortDescriptor, + Link, +} from "@nextui-org/react"; +import { MoreVertical } from "lucide-react"; +import { ProjectType } from "@/types/project"; +import dayjs from "dayjs"; + +const headerColumns = [ + { name: "ID", uid: "id", sortable: true }, + { name: "Name", uid: "name", sortable: true }, + { name: "Detail", uid: "detail", sortable: true }, + { name: "Last update", uid: "updatedAt", sortable: true }, + { name: "Actions", uid: "actions" }, +]; + +type Props = { + projects: ProjectType[]; + onEditProject: (project: ProjectType) => void; + onDeleteProject: (projectId: number) => void; +}; + +export default function ProjectsTable({ + projects, + onEditProject, + onDeleteProject, +}: Props) { + const [sortDescriptor, setSortDescriptor] = useState({ + column: "id", + direction: "ascending", + }); + + const sortedItems = useMemo(() => { + return [...projects].sort((a: ProjectType, b: ProjectType) => { + const first = a[sortDescriptor.column as keyof ProjectType] as number; + const second = b[sortDescriptor.column as keyof ProjectType] as number; + const cmp = first < second ? -1 : first > second ? 1 : 0; + + return sortDescriptor.direction === "descending" ? -cmp : cmp; + }); + }, [sortDescriptor, projects]); + + const truncateText = (text: string, maxLength: number) => { + return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; + }; + + const renderCell = useCallback((project: ProjectType, columnKey: Key) => { + const cellValue = project[columnKey as keyof ProjectType]; + + switch (columnKey) { + case "id": + return {cellValue}; + case "name": + return ( + + {cellValue} + + ); + case "detail": + const maxLength = 20; + const truncatedValue = truncateText(cellValue, maxLength); + return ( +
+
{truncatedValue}
+
+ ); + case "updatedAt": + return {dayjs(cellValue).format("YYYY/MM/DD HH:mm")}; + case "actions": + return ( + + + + + + onEditProject(project)}> + Edit project + + onDeleteProject(project.id)} + > + Delete project + + + + ); + default: + return cellValue; + } + }, []); + + const classNames = useMemo( + () => ({ + wrapper: ["max-w-3xl"], + th: ["bg-transparent", "text-default-500", "border-b", "border-divider"], + td: [ + // changing the rows border radius + // first + "group-data-[first=true]:first:before:rounded-none", + "group-data-[first=true]:last:before:rounded-none", + // middle + "group-data-[middle=true]:before:rounded-none", + // last + "group-data-[last=true]:first:before:rounded-none", + "group-data-[last=true]:last:before:rounded-none", + ], + }), + [] + ); + + return ( + <> + + + {(column) => ( + + {column.name} + + )} + + + {(item) => ( + + {(columnKey) => ( + {renderCell(item, columnKey)} + )} + + )} + +
+ + ); +} diff --git a/frontend/app/projects/page.tsx b/frontend/app/projects/page.tsx index 6bc3952..385a95c 100644 --- a/frontend/app/projects/page.tsx +++ b/frontend/app/projects/page.tsx @@ -1,231 +1,9 @@ -"use client"; -import { useEffect, useState } from "react"; -import { title } from "@/components/primitives"; -import { ProjectCard } from "./project-card"; -import { ProjectDialog } from "./project-dialog"; -import { Button } from "@nextui-org/react"; - -import Config from "@/config/config"; -const apiServer = Config.apiServer; - -export type ProjectType = { - id: number; - name: string; - detail: string; - createdAt: string; - updatedAt: string; -}; - -/** - * fetch project records - * - * @param {string} url - API endpoint url - * @returns {Promise} - project record array - * @throws {Error} - */ -async function fetchProjects(url: string) { - try { - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error("Error fetching data:", error.message); - } -} - -/** - * Create project - */ -async function createProject(name: string, detail: string) { - const newProjectData = { - name: name, - detail: detail, - }; - - const fetchOptions = { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(newProjectData), - }; - - const url = `${apiServer}/projects`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error) { - console.error("Error creating new project:", error); - throw error; - } -} - -/** - * Update project - */ -async function updateProject(projectId: number, name: string, detail: string) { - const updatedProjectData = { - name: name, - detail: detail, - }; - - const fetchOptions = { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(updatedProjectData), - }; - - const url = `${apiServer}/projects/${projectId}`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error) { - console.error("Error updating project:", error); - throw error; - } -} - -/** - * Delete project - */ -async function deleteProject(projectId: number) { - const fetchOptions = { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }; - - const url = `${apiServer}/projects/${projectId}`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - } catch (error) { - console.error("Error deleting project:", error); - throw error; - } -} - -export default function ProjectsPage() { - // projects - const [projects, setProjects] = useState([]); - const url = `${apiServer}/projects`; - - useEffect(() => { - async function fetchDataEffect() { - try { - const data = await fetchProjects(url); - setProjects(data); - } catch (error) { - console.error("Error in effect:", error.message); - } - } - - fetchDataEffect(); - }, []); - - // dialog - const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false); - const [editingProject, setEditingProject] = useState( - null - ); - const openDialogForCreate = () => { - setIsProjectDialogOpen(true); - setEditingProject(null); - }; - - const closeDialog = () => { - setIsProjectDialogOpen(false); - setEditingProject(null); - }; - - const onSubmit = async (name: string, detail: string) => { - if (editingProject) { - const updatedProject = await updateProject( - editingProject.id, - name, - detail - ); - const updatedProjects = projects.map((project) => - project.id === updatedProject.id ? updatedProject : project - ); - setProjects(updatedProjects); - } else { - const newProject = await createProject(name, detail); - setProjects([...projects, newProject]); - } - closeDialog(); - }; - - const onEditClick = (project: ProjectType) => { - setEditingProject(project); - setIsProjectDialogOpen(true); - }; - - const onDeleteClick = async (projectId: number) => { - try { - await deleteProject(projectId); - setProjects(projects.filter((project) => project.id !== projectId)); - } catch (error) { - console.error("Error deleting project:", error); - } - }; +import ProjectsPage from "./ProjectsPage"; +export default function Page() { return ( -
-
-

Projects

- -
- -
- {projects.map((project, index) => ( - - ))} -
- - -
+ <> + + ); } diff --git a/frontend/app/projects/project-card.tsx b/frontend/app/projects/project-card.tsx deleted file mode 100644 index 9252019..0000000 --- a/frontend/app/projects/project-card.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from "react"; -import { - Link, - Button, - Dropdown, - DropdownTrigger, - DropdownMenu, - DropdownItem, - Card, - CardHeader, - CardBody, - Divider, -} from "@nextui-org/react"; -import { MoreVertical } from "lucide-react"; - -export function ProjectCard({ project, onEditClick, onDeleteClick }) { - return ( - - -
-
- - {project.name} - -
-
- - - - - - onEditClick(project)}> - Edit project - - onDeleteClick(project.id)} - > - Delete project - - - -
- - -

{project.detail}

-
-
- ); -} diff --git a/frontend/app/projects/projectsControl.ts b/frontend/app/projects/projectsControl.ts new file mode 100644 index 0000000..077620d --- /dev/null +++ b/frontend/app/projects/projectsControl.ts @@ -0,0 +1,117 @@ +import Config from "@/config/config"; +const apiServer = Config.apiServer; + +/** + * fetch project records + */ +async function fetchProjects() { + const url = `${apiServer}/projects`; + + try { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Error fetching data:", error.message); + } +} + +/** + * Create project + */ +async function createProject(name: string, detail: string) { + const newProjectData = { + name: name, + detail: detail, + }; + + const fetchOptions = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newProjectData), + }; + + const url = `${apiServer}/projects`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + console.error("Error creating new project:", error); + throw error; + } +} + +/** + * Update project + */ +async function updateProject(projectId: number, name: string, detail: string) { + const updatedProjectData = { + name: name, + detail: detail, + }; + + const fetchOptions = { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updatedProjectData), + }; + + const url = `${apiServer}/projects/${projectId}`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + console.error("Error updating project:", error); + throw error; + } +} + +/** + * Delete project + */ +async function deleteProject(projectId: number) { + const fetchOptions = { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }; + + const url = `${apiServer}/projects/${projectId}`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + } catch (error) { + console.error("Error deleting project:", error); + throw error; + } +} + +export { fetchProjects, createProject, updateProject, deleteProject }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 841bd7c..5ee59b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@types/react-dom": "18.2.7", "autoprefixer": "10.4.16", "clsx": "^2.0.0", + "dayjs": "^1.11.10", "eslint": "8.48.0", "eslint-config-next": "14.0.2", "framer-motion": "^10.16.4", @@ -3431,6 +3432,11 @@ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 86f274b..85fedd8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@types/react-dom": "18.2.7", "autoprefixer": "10.4.16", "clsx": "^2.0.0", + "dayjs": "^1.11.10", "eslint": "8.48.0", "eslint-config-next": "14.0.2", "framer-motion": "^10.16.4", diff --git a/frontend/types/project.ts b/frontend/types/project.ts new file mode 100644 index 0000000..cad36e3 --- /dev/null +++ b/frontend/types/project.ts @@ -0,0 +1,7 @@ +export type ProjectType = { + id: number; + name: string; + detail: string; + createdAt: string; + updatedAt: string; +}; \ No newline at end of file