From da7186f4f4e59ba776b26e7bb73df5a5d4688055 Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Wed, 15 Mar 2023 22:05:27 -0400 Subject: [PATCH 01/10] Allow viewing individual projects --- .../project-preview.js | 78 +++++-------------- pages/projects/[uuid].js | 58 ++++++++++++++ pages/projects/index.js | 58 ++++++++++++++ 3 files changed, 137 insertions(+), 57 deletions(-) rename pages/projects.js => components/project-preview.js (62%) create mode 100644 pages/projects/[uuid].js create mode 100644 pages/projects/index.js diff --git a/pages/projects.js b/components/project-preview.js similarity index 62% rename from pages/projects.js rename to components/project-preview.js index 4e62ba0..8f75f52 100644 --- a/pages/projects.js +++ b/components/project-preview.js @@ -1,15 +1,8 @@ -import Head from "next/head"; -import styled from "styled-components"; -import { useEffect, useState } from "react"; -import { PageTitle, Card } from "../lib/common-style"; +import { useState } from "react"; +import { Card } from "../lib/common-style"; import { makeApiRequest } from "../lib/api"; -import SubmitButton from "../components/submit-button"; - -const ProjectsContainer = styled.div` - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 10px; -`; +import Link from "./link"; +import SubmitButton from "./submit-button"; class MembershipStatus { static Joining = new MembershipStatus("Joining..."); @@ -28,7 +21,21 @@ class MembershipStatus { } } -function ProjectPreview({ project }) { +function ProjectName({ project, summary }) { + const name = project.name || "Unnamed Project"; + + if (summary) { + return ( +

+ {name} +

+ ); + } + + return

{name}

; +} + +function ProjectPreview({ project, summary }) { const initialMembershipStatus = () => { if (typeof project.joined === "boolean") { return project.joined @@ -80,7 +87,7 @@ function ProjectPreview({ project }) { return ( -

{project.name || "Unnamed Project"}

+ {typeof membershipStatus === "object" && (

Membership Status: {`${membershipStatus.text}`}

)} @@ -105,47 +112,4 @@ function ProjectPreview({ project }) { ); } -export default function ProjectsPage() { - const [projects, setProjects] = useState(null); - - useEffect(() => { - makeApiRequest("/y22/projects") - .then((res) => res.json()) - .then((data) => { - if (data.projects) { - setProjects(data.projects); - } - }); - }, []); - - return ( - <> - - Projects - The Snakeroom - - Projects -
- {projects === null ? ( -

Loading projects

- ) : ( - <> -

- {projects.length === 1 - ? "There is 1 project available." - : `There are ${projects.length} projects available.`} -

- - {projects.map((project) => { - return ( - - ); - })} - - - )} - - ); -} +export default ProjectPreview; diff --git a/pages/projects/[uuid].js b/pages/projects/[uuid].js new file mode 100644 index 0000000..80d8114 --- /dev/null +++ b/pages/projects/[uuid].js @@ -0,0 +1,58 @@ +import Head from "next/head"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import { PageTitle } from "../../lib/common-style"; +import { makeApiRequest } from "../../lib/api"; +import ProjectPreview from "../../components/project-preview"; +import NotFoundPage from "../404"; + +export default function ProjectPage() { + const [project, setProject] = useState(null); + const [error, setError] = useState(false); + + const router = useRouter(); + + useEffect(() => { + if (router.isReady) { + const { uuid } = router.query; + + makeApiRequest("/y22/projects") + .then((res) => res.json()) + .then((data) => { + if (data.projects) { + const foundProject = data.projects.find((p) => { + return p.uuid === uuid; + }); + + if (foundProject) { + setProject(foundProject); + return; + } + } + + setError(true); + }) + .catch((err) => { + console.error(err); + setError(true); + }); + } + }, [router.isReady]); + + if (error) { + return NotFoundPage(); + } + + const title = project?.name ? `${project.name} Project` : "Projects"; + + return ( + <> + + {title} - The Snakeroom + + Projects +
+ {project ? :

Loading...

} + + ); +} diff --git a/pages/projects/index.js b/pages/projects/index.js new file mode 100644 index 0000000..1d42322 --- /dev/null +++ b/pages/projects/index.js @@ -0,0 +1,58 @@ +import Head from "next/head"; +import styled from "styled-components"; +import { useEffect, useState } from "react"; +import { PageTitle } from "../../lib/common-style"; +import { makeApiRequest } from "../../lib/api"; +import ProjectPreview from "../../components/project-preview"; + +const ProjectsContainer = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +`; + +export default function ProjectsPage() { + const [projects, setProjects] = useState(null); + + useEffect(() => { + makeApiRequest("/y22/projects") + .then((res) => res.json()) + .then((data) => { + if (data.projects) { + setProjects(data.projects); + } + }); + }, []); + + return ( + <> + + Projects - The Snakeroom + + Projects +
+ {projects === null ? ( +

Loading projects

+ ) : ( + <> +

+ {projects.length === 1 + ? "There is 1 project available." + : `There are ${projects.length} projects available.`} +

+ + {projects.map((project) => { + return ( + + ); + })} + + + )} + + ); +} From a090099058484ca4bdfdd76179303235d3ff3a65 Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Thu, 20 Jul 2023 02:24:58 -0400 Subject: [PATCH 02/10] Allow viewing project divisions --- .eslintrc.json | 1 + components/project-panel.js | 123 ++++++++++++++++++++++++++++++++++ components/project-preview.js | 15 +++-- components/submit-button.js | 6 +- pages/projects/[uuid].js | 18 ++--- 5 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 components/project-panel.js diff --git a/.eslintrc.json b/.eslintrc.json index 34b4f96..d015ca9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,6 +25,7 @@ "error", { "controlComponents": [ + "InlineStyledInput", "StyledInput" ] } diff --git a/components/project-panel.js b/components/project-panel.js new file mode 100644 index 0000000..befe931 --- /dev/null +++ b/components/project-panel.js @@ -0,0 +1,123 @@ +import styled from "styled-components"; +import { API_BASE } from "../lib/api"; +import Card from "./card"; +import ProjectPreview from "./project-preview"; +import { InlineStyledInput } from "./submit-button"; +import Link from "./link"; + +const ProjectPanelContainer = styled.div` + margin: 0 auto; + width: 100%; + + @media (min-width: 1200px) { + max-width: 1200px; + } + + display: flex; + flex-direction: column; + gap: 12px; +`; + +const InputBlock = styled.p` + margin-top: 10px; +`; + +const InputNote = styled.span` + color: ${(props) => props.theme.colors.primaryMuted}; + font-size: 0.8rem; +`; + +function isNonEmptyArray(array) { + return Array.isArray(array) && array.length > 0; +} + +function DivisionCoordinates({ division }) { + const [x, y] = division.origin; + const [width, height] = division.dimensions; + + if (width !== null && height !== null) { + const maxX = Math.max(0, x + width - 1); + const maxY = Math.max(0, y + height - 1); + + return ( + <> + + Coordinates: ({x}, {y}) (top left) to + ({maxX}, {maxY}) (bottom right) + + + Dimensions: {width}×{height} + + + ); + } + + return ( + + Offset: ({x}, {y}) (top left) + + ); +} + +function DivisionCard({ project, division }) { + const [width, height] = division.dimensions; + + const imageUrl = + width !== null && height !== null + ? `${API_BASE}/y22/projects/${project.uuid}/divisions/${division.uuid}/bitmap` + : null; + + return ( + +

{division.name}

+ + + + + + + + {imageUrl !== null && project.can_edit ? ( + + + Download Division Image + + + ) : null} +
+ ); +} + +export default function ProjectPanel({ project }) { + return ( + + + {isNonEmptyArray(project.divisions) ? ( + <> +

Divisions

+ {project.divisions.map((division) => ( + + ))} + + ) : null} +
+ ); +} diff --git a/components/project-preview.js b/components/project-preview.js index 9cc3cb0..c8adb9c 100644 --- a/components/project-preview.js +++ b/components/project-preview.js @@ -85,14 +85,15 @@ function ProjectPreview({ project, summary }) { } }; + const [width, height] = project.dimensions; + + const imageUrl = + width !== null && height !== null + ? `${API_BASE}/y22/projects/${project.uuid}/bitmap` + : null; + return ( - + {typeof membershipStatus === "object" && (

Membership Status: {`${membershipStatus.text}`}

diff --git a/components/submit-button.js b/components/submit-button.js index 513309b..fb0059a 100644 --- a/components/submit-button.js +++ b/components/submit-button.js @@ -1,8 +1,12 @@ import styled from "styled-components"; -export const StyledInput = styled.input` +export const InlineStyledInput = styled.input` border: 1px solid ${(props) => props.theme.colors.primary}; border-radius: 2px; + padding: 2px; +`; + +export const StyledInput = styled(InlineStyledInput)` margin: 10px 0; padding: 10px; display: block; diff --git a/pages/projects/[uuid].js b/pages/projects/[uuid].js index d4214ea..7b88810 100644 --- a/pages/projects/[uuid].js +++ b/pages/projects/[uuid].js @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { PageTitle } from "../../lib/common-style"; import { makeApiRequest } from "../../lib/api"; -import ProjectPreview from "../../components/project-preview"; +import ProjectPanel from "../../components/project-panel"; import NotFoundPage from "../404"; export default function ProjectPage() { @@ -16,18 +16,12 @@ export default function ProjectPage() { if (router.isReady) { const { uuid } = router.query; - makeApiRequest("/y22/projects") + makeApiRequest(`/y22/projects/${uuid}`) .then((res) => res.json()) .then((data) => { - if (data.projects) { - const foundProject = data.projects.find((p) => { - return p.uuid === uuid; - }); - - if (foundProject) { - setProject(foundProject); - return; - } + if (data.project) { + setProject(data.project); + return; } setError(true); @@ -53,7 +47,7 @@ export default function ProjectPage() { Projects
- {project ? :

Loading...

} + {project ? :

Loading...

} ); } From 9029f62d02a04b19dfa69e4d7d6efc8d9233bb31 Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Thu, 20 Jul 2023 04:00:59 -0400 Subject: [PATCH 03/10] Allow editing project divisions --- components/project-panel.js | 164 +++++++++++++++++++++++++++++++++--- components/submit-row.js | 55 ++++++++++++ lib/theme.js | 2 + pages/projects/[uuid].js | 6 +- 4 files changed, 214 insertions(+), 13 deletions(-) create mode 100644 components/submit-row.js diff --git a/components/project-panel.js b/components/project-panel.js index befe931..d2fbe78 100644 --- a/components/project-panel.js +++ b/components/project-panel.js @@ -1,9 +1,11 @@ import styled from "styled-components"; -import { API_BASE } from "../lib/api"; +import { useState, useCallback } from "react"; +import { API_BASE, makeApiRequest } from "../lib/api"; import Card from "./card"; import ProjectPreview from "./project-preview"; import { InlineStyledInput } from "./submit-button"; import Link from "./link"; +import SubmitRow from "./submit-row"; const ProjectPanelContainer = styled.div` margin: 0 auto; @@ -31,7 +33,57 @@ function isNonEmptyArray(array) { return Array.isArray(array) && array.length > 0; } -function DivisionCoordinates({ division }) { +function getNumberOrDefault(value, defaultValue = 0) { + return Number.isNaN(value) ? defaultValue : value; +} + +function DivisionOrigin({ project, division, updateDivision }) { + const [x, y] = division.origin; + + if (!project.can_edit) { + return ( + <> + ({x}, {y}) + + ); + } + + return ( + <> + ( + { + updateDivision({ + ...division, + origin: [ + getNumberOrDefault(event.target.valueAsNumber), + division.origin[1], + ], + }); + }} + /> + ,{" "} + { + updateDivision({ + ...division, + origin: [ + division.origin[0], + getNumberOrDefault(event.target.valueAsNumber), + ], + }); + }} + /> + ) + + ); +} + +function DivisionCoordinates({ project, division, updateDivision }) { const [x, y] = division.origin; const [width, height] = division.dimensions; @@ -42,8 +94,14 @@ function DivisionCoordinates({ division }) { return ( <> - Coordinates: ({x}, {y}) (top left) to - ({maxX}, {maxY}) (bottom right) + Coordinates:{" "} + {" "} + (top left) to ({maxX}, {maxY}){" "} + (bottom right) Dimensions: {width}×{height} @@ -54,12 +112,18 @@ function DivisionCoordinates({ division }) { return ( - Offset: ({x}, {y}) (top left) + Offset:{" "} + {" "} + (top left) ); } -function DivisionCard({ project, division }) { +function DivisionCard({ project, division, updateDivision }) { const [width, height] = division.dimensions; const imageUrl = @@ -69,15 +133,37 @@ function DivisionCard({ project, division }) { return ( -

{division.name}

- + {project.can_edit ? ( + { + updateDivision({ + ...division, + name: event.target.value, + }); + }} + /> + ) : ( +

{division.name}

+ )} + @@ -102,7 +203,21 @@ function DivisionCard({ project, division }) { ); } -export default function ProjectPanel({ project }) { +export default function ProjectPanel({ initialProject }) { + const [project, setProject] = useState(initialProject); + + const updateDivision = useCallback( + (division) => { + setProject({ + ...project, + divisions: project.divisions.map((d) => { + return d.uuid === division.uuid ? division : d; + }), + }); + }, + [project] + ); + return ( @@ -114,10 +229,35 @@ export default function ProjectPanel({ project }) { key={division.uuid} project={project} division={division} + updateDivision={updateDivision} /> ))} ) : null} + {project.can_edit ? ( + + Promise.all( + project.divisions.map((division) => + makeApiRequest( + `/y22/projects/${project.uuid}/divisions/${division.uuid}`, + { + method: "POST", + body: JSON.stringify(division), + } + ) + .then((res) => (res.ok ? {} : res.json())) + .then((json) => { + if (json.error) { + throw new Error(json.error); + } + }) + ) + ) + } + /> + ) : null} ); } diff --git a/components/submit-row.js b/components/submit-row.js new file mode 100644 index 0000000..55f3e66 --- /dev/null +++ b/components/submit-row.js @@ -0,0 +1,55 @@ +import styled from "styled-components"; +import { useCallback, useState } from "react"; +import SubmitButton from "./submit-button"; + +const SubmitRowBox = styled.div` + display: flex; + gap: 12px; + + flex-flow: row-reverse; + text-align: right; +`; + +const SubmitRowNag = styled.div` + margin: 10px 0; + padding: 9px; + + animation: fade-in linear 0.2s; + + @keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + background-color: ${(props) => props.theme.colors.danger}; + color: #121212; + border-radius: 8px; +`; + +export default function SubmitRow({ name, onClick }) { + const [error, setError] = useState(null); + + const callback = useCallback(async (event) => { + try { + await onClick(event); + setError(null); + } catch (err) { + setError(err.message); + + // eslint-disable-next-line no-console + console.error(err); + } + }); + + return ( + + + {error === null ? null : {error}} + + ); +} diff --git a/lib/theme.js b/lib/theme.js index 5b774b7..586d978 100644 --- a/lib/theme.js +++ b/lib/theme.js @@ -8,6 +8,7 @@ export const lightTheme = { accent: "#557528", background: "#ffffff", backgroundMuted: "#eee", + danger: "#ff7777", cardImageBackgroundGradient: "#00000022", primary: "#000", primaryMuted: "#888", @@ -22,6 +23,7 @@ export const darkTheme = { background: "#121212", backgroundMuted: "#1f1f1f", cardImageBackgroundGradient: "#ffffff22", + danger: "#ff7777", primary: "#fff", primaryMuted: "#888", primaryVeryMuted: "#191919", diff --git a/pages/projects/[uuid].js b/pages/projects/[uuid].js index 7b88810..6e05e96 100644 --- a/pages/projects/[uuid].js +++ b/pages/projects/[uuid].js @@ -47,7 +47,11 @@ export default function ProjectPage() { Projects
- {project ? :

Loading...

} + {project ? ( + + ) : ( +

Loading...

+ )} ); } From 09d8aa81d445c5c1d8c7a15345f38019586a84b8 Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Thu, 20 Jul 2023 07:30:47 -0400 Subject: [PATCH 04/10] Allow viewing project members --- components/project-panel.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/components/project-panel.js b/components/project-panel.js index d2fbe78..44d38f0 100644 --- a/components/project-panel.js +++ b/components/project-panel.js @@ -203,6 +203,20 @@ function DivisionCard({ project, division, updateDivision }) { ); } +const roles = new Map() + .set("owner", "Owner") + .set("manager", "Manager") + .set("user", "User"); + +function MemberCard({ member }) { + return ( + +

{member.username}

+ Role: {roles.get(member.role)} +
+ ); +} + export default function ProjectPanel({ initialProject }) { const [project, setProject] = useState(initialProject); @@ -234,6 +248,14 @@ export default function ProjectPanel({ initialProject }) { ))} ) : null} + {isNonEmptyArray(project.members) ? ( + <> +

Members

+ {project.members.map((member) => ( + + ))} + + ) : null} {project.can_edit ? ( Date: Fri, 21 Jul 2023 13:52:21 -0400 Subject: [PATCH 05/10] Allow replacing division images --- components/project-panel.js | 208 ++++++++++++++++++++++++++++++------ components/submit-button.js | 5 +- 2 files changed, 180 insertions(+), 33 deletions(-) diff --git a/components/project-panel.js b/components/project-panel.js index 44d38f0..00c7db7 100644 --- a/components/project-panel.js +++ b/components/project-panel.js @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useRef, useEffect } from "react"; import { API_BASE, makeApiRequest } from "../lib/api"; import Card from "./card"; import ProjectPreview from "./project-preview"; @@ -83,9 +83,14 @@ function DivisionOrigin({ project, division, updateDivision }) { ); } -function DivisionCoordinates({ project, division, updateDivision }) { +function DivisionCoordinates({ + project, + division, + divisionUpload, + updateDivision, +}) { const [x, y] = division.origin; - const [width, height] = division.dimensions; + const [width, height] = divisionUpload ? [null, null] : division.dimensions; if (width !== null && height !== null) { const maxX = Math.max(0, x + width - 1); @@ -123,7 +128,60 @@ function DivisionCoordinates({ project, division, updateDivision }) { ); } -function DivisionCard({ project, division, updateDivision }) { +function ImageManagementBlock({ + imageUrl, + division, + divisionUpload, + setDivisionUpload, +}) { + const uploadRef = useRef(null); + + useEffect(() => { + if (uploadRef.current) { + const dataTransfer = new DataTransfer(); + + if (divisionUpload) { + dataTransfer.items.add(divisionUpload); + } + + uploadRef.current.files = dataTransfer.files; + } + }, [divisionUpload]); + + return ( + + {imageUrl == null ? null : ( + <> + + Download Division Image + {" "} + •{" "} + + )} + Replace Division Image:{" "} + { + const file = event.target.files?.[0]; + + if (file) { + setDivisionUpload(division, file); + } + }} + /> + + ); +} + +function DivisionCard({ + project, + division, + divisionUpload, + updateDivision, + setDivisionUpload, +}) { const [width, height] = division.dimensions; const imageUrl = @@ -131,8 +189,31 @@ function DivisionCard({ project, division, updateDivision }) { ? `${API_BASE}/y22/projects/${project.uuid}/divisions/${division.uuid}/bitmap` : null; + const [uploadUrl, setUploadUrl] = useState(null); + + useEffect(() => { + if (!divisionUpload) { + setUploadUrl(null); + return; + } + + const reader = new FileReader(); + + reader.addEventListener( + "load", + (event) => { + setUploadUrl(event.target.result); + }, + { + once: true, + } + ); + + reader.readAsDataURL(divisionUpload); + }, [divisionUpload]); + return ( - + {project.can_edit ? ( @@ -192,12 +274,13 @@ function DivisionCard({ project, division, updateDivision }) { (a higher number receives priority) - {imageUrl !== null && project.can_edit ? ( - - - Download Division Image - - + {project.can_edit ? ( + ) : null} ); @@ -217,8 +300,73 @@ function MemberCard({ member }) { ); } +function SaveChangesRow({ + project, + divisionUploads, + setProject, + setDivisionUploads, +}) { + const onClick = useCallback(async () => { + // Update division images + const newUploads = { ...divisionUploads }; + + // eslint-disable-next-line no-restricted-syntax + for await (const [division, file] of Object.entries(divisionUploads)) { + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/divisions/${division}/bitmap`, + { + method: "POST", + body: file, + } + ); + + if (res.ok) { + delete newUploads[division]; + } else { + const json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } + } + } + + // Update divisions + const newDivisions = []; + + // eslint-disable-next-line no-restricted-syntax + for await (const division of project.divisions) { + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/divisions/${division.uuid}`, + { + method: "POST", + body: JSON.stringify(division), + } + ); + + const json = await res.json(); + + if (res.ok) { + newDivisions.push(json.division); + } else if (json.error) { + throw new Error(json.error); + } + } + + setProject({ + ...project, + divisions: newDivisions, + }); + + setDivisionUploads(newUploads); + }); + + return ; +} + export default function ProjectPanel({ initialProject }) { const [project, setProject] = useState(initialProject); + const [divisionUploads, setDivisionUploads] = useState({}); const updateDivision = useCallback( (division) => { @@ -232,6 +380,16 @@ export default function ProjectPanel({ initialProject }) { [project] ); + const setDivisionUpload = useCallback( + (division, file) => { + setDivisionUploads({ + ...divisionUploads, + [division.uuid]: file, + }); + }, + [divisionUploads] + ); + return ( @@ -243,7 +401,9 @@ export default function ProjectPanel({ initialProject }) { key={division.uuid} project={project} division={division} + divisionUpload={divisionUploads[division.uuid]} updateDivision={updateDivision} + setDivisionUpload={setDivisionUpload} /> ))} @@ -257,27 +417,11 @@ export default function ProjectPanel({ initialProject }) { ) : null} {project.can_edit ? ( - - Promise.all( - project.divisions.map((division) => - makeApiRequest( - `/y22/projects/${project.uuid}/divisions/${division.uuid}`, - { - method: "POST", - body: JSON.stringify(division), - } - ) - .then((res) => (res.ok ? {} : res.json())) - .then((json) => { - if (json.error) { - throw new Error(json.error); - } - }) - ) - ) - } + ) : null} diff --git a/components/submit-button.js b/components/submit-button.js index fb0059a..2690a59 100644 --- a/components/submit-button.js +++ b/components/submit-button.js @@ -1,7 +1,10 @@ import styled from "styled-components"; export const InlineStyledInput = styled.input` - border: 1px solid ${(props) => props.theme.colors.primary}; + &:not([type="file"]) { + border: 1px solid ${(props) => props.theme.colors.primary}; + } + border-radius: 2px; padding: 2px; `; From 9bdc2d39e1e04a45784144fed73b9ce34ca7e551 Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Fri, 21 Jul 2023 14:45:59 -0400 Subject: [PATCH 06/10] Allow creating new divisions --- components/project-panel.js | 113 ++++++++++++++++++++++++++++++------ package.json | 3 +- 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/components/project-panel.js b/components/project-panel.js index 00c7db7..e460976 100644 --- a/components/project-panel.js +++ b/components/project-panel.js @@ -1,9 +1,10 @@ import styled from "styled-components"; import { useState, useCallback, useRef, useEffect } from "react"; +import { v4 as uuidv4 } from "uuid"; import { API_BASE, makeApiRequest } from "../lib/api"; import Card from "./card"; import ProjectPreview from "./project-preview"; -import { InlineStyledInput } from "./submit-button"; +import SubmitButton, { InlineStyledInput } from "./submit-button"; import Link from "./link"; import SubmitRow from "./submit-row"; @@ -286,6 +287,44 @@ function DivisionCard({ ); } +function DivisionsRow({ + project, + divisionUploads, + updateDivision, + setDivisionUpload, + addDivision, +}) { + if (!Array.isArray(project.divisions)) { + return null; + } + + return ( + <> +

Divisions

+ {project.divisions.map((division) => ( + + ))} + {project.can_edit ? ( +
+ +
+ ) : null} + + ); +} + const roles = new Map() .set("owner", "Owner") .set("manager", "Manager") @@ -307,13 +346,40 @@ function SaveChangesRow({ setDivisionUploads, }) { const onClick = useCallback(async () => { + // Create new divisions + const remappedUuids = new Map(); + + // eslint-disable-next-line no-restricted-syntax + for await (const division of project.divisions) { + if (division.create) { + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/create_division`, + { + method: "POST", + } + ); + + const json = await res.json(); + + if (res.ok) { + remappedUuids.set(division.uuid, json.uuid); + } else if (json.error) { + throw new Error(json.error); + } + } else { + remappedUuids.set(division.uuid, division.uuid); + } + } + // Update division images const newUploads = { ...divisionUploads }; // eslint-disable-next-line no-restricted-syntax for await (const [division, file] of Object.entries(divisionUploads)) { + const uuid = remappedUuids.get(division); + const res = await makeApiRequest( - `/y22/projects/${project.uuid}/divisions/${division}/bitmap`, + `/y22/projects/${project.uuid}/divisions/${uuid}/bitmap`, { method: "POST", body: file, @@ -336,8 +402,10 @@ function SaveChangesRow({ // eslint-disable-next-line no-restricted-syntax for await (const division of project.divisions) { + const uuid = remappedUuids.get(division.uuid); + const res = await makeApiRequest( - `/y22/projects/${project.uuid}/divisions/${division.uuid}`, + `/y22/projects/${project.uuid}/divisions/${uuid}`, { method: "POST", body: JSON.stringify(division), @@ -390,24 +458,33 @@ export default function ProjectPanel({ initialProject }) { [divisionUploads] ); + const addDivision = useCallback(() => { + setProject({ + ...project, + divisions: [ + ...project.divisions, + { + create: true, + uuid: uuidv4(), + origin: [0, 0], + dimensions: [null, null], + priority: 0, + enabled: true, + }, + ], + }); + }, [project]); + return ( - {isNonEmptyArray(project.divisions) ? ( - <> -

Divisions

- {project.divisions.map((division) => ( - - ))} - - ) : null} + {isNonEmptyArray(project.members) ? ( <>

Members

diff --git a/package.json b/package.json index 6a2c1ae..e0a736f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "next-offline": "^5.0.5", "react": "^18.2.0", "react-dom": "^18.2.0", - "styled-components": "^5.3.6" + "styled-components": "^5.3.6", + "uuid": "^9.0.0" }, "devDependencies": { "eslint": "^8.26.0", From b5729ac87047e19334b7802ae6b9f663e75bd150 Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Fri, 21 Jul 2023 16:03:26 -0400 Subject: [PATCH 07/10] Disable the save changes button for unmodified divisions --- components/project-panel.js | 35 ++++++++++++++++++++++++++++++++++- components/submit-button.js | 5 +++++ components/submit-row.js | 9 +++++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/components/project-panel.js b/components/project-panel.js index e460976..22e8b2a 100644 --- a/components/project-panel.js +++ b/components/project-panel.js @@ -342,6 +342,7 @@ function MemberCard({ member }) { function SaveChangesRow({ project, divisionUploads, + dirty, setProject, setDivisionUploads, }) { @@ -429,13 +430,44 @@ function SaveChangesRow({ setDivisionUploads(newUploads); }); - return ; + return ( + + ); +} + +function isDirty(project, initialProject, divisionUploads) { + // If a division image will be uploaded, then there are modifications + if (Object.keys(divisionUploads).length > 0) { + return true; + } + + // If any division has changed, then there are modifications + return project.divisions.some((division) => { + const initialDivision = initialProject.divisions.find( + (d) => d.uuid === division.uuid + ); + + // Newly added division + if (initialDivision === undefined) return true; + + // Division with changed values + if (division.name !== initialDivision.name) return true; + if (division.priority !== initialDivision.priority) return true; + if (division.enabled !== initialDivision.enabled) return true; + + if (division.origin[0] !== initialDivision.origin[0]) return true; + if (division.origin[1] !== initialDivision.origin[1]) return true; + + return false; + }); } export default function ProjectPanel({ initialProject }) { const [project, setProject] = useState(initialProject); const [divisionUploads, setDivisionUploads] = useState({}); + const dirty = isDirty(project, initialProject, divisionUploads); + const updateDivision = useCallback( (division) => { setProject({ @@ -497,6 +529,7 @@ export default function ProjectPanel({ initialProject }) { diff --git a/components/submit-button.js b/components/submit-button.js index 2690a59..92ea22e 100644 --- a/components/submit-button.js +++ b/components/submit-button.js @@ -25,5 +25,10 @@ const SubmitButton = styled(StyledInput)` background-color: ${(props) => props.theme.colors.background}; color: ${(props) => props.theme.colors.primary}; } + + &:disabled { + background-color: ${(props) => props.theme.colors.backgroundMuted}; + color: ${(props) => props.theme.colors.primaryMuted}; + } `; export default SubmitButton; diff --git a/components/submit-row.js b/components/submit-row.js index 55f3e66..8bf9c23 100644 --- a/components/submit-row.js +++ b/components/submit-row.js @@ -31,7 +31,7 @@ const SubmitRowNag = styled.div` border-radius: 8px; `; -export default function SubmitRow({ name, onClick }) { +export default function SubmitRow({ name, onClick, disabled }) { const [error, setError] = useState(null); const callback = useCallback(async (event) => { @@ -48,7 +48,12 @@ export default function SubmitRow({ name, onClick }) { return ( - + {error === null ? null : {error}} ); From 33237d71187e8ff60719f64ec3822f725257ae02 Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Fri, 21 Jul 2023 16:16:43 -0400 Subject: [PATCH 08/10] Split project panel components into separate files --- .../divisions-row.js} | 247 +----------------- components/projects/members-row.js | 35 +++ components/projects/project-panel.js | 116 ++++++++ components/{ => projects}/project-preview.js | 8 +- components/projects/save-changes-row.js | 99 +++++++ pages/projects/[uuid].js | 2 +- pages/projects/index.js | 2 +- 7 files changed, 263 insertions(+), 246 deletions(-) rename components/{project-panel.js => projects/divisions-row.js} (51%) create mode 100644 components/projects/members-row.js create mode 100644 components/projects/project-panel.js rename components/{ => projects}/project-preview.js (94%) create mode 100644 components/projects/save-changes-row.js diff --git a/components/project-panel.js b/components/projects/divisions-row.js similarity index 51% rename from components/project-panel.js rename to components/projects/divisions-row.js index 22e8b2a..a03f993 100644 --- a/components/project-panel.js +++ b/components/projects/divisions-row.js @@ -1,27 +1,11 @@ import styled from "styled-components"; -import { useState, useCallback, useRef, useEffect } from "react"; -import { v4 as uuidv4 } from "uuid"; -import { API_BASE, makeApiRequest } from "../lib/api"; -import Card from "./card"; -import ProjectPreview from "./project-preview"; -import SubmitButton, { InlineStyledInput } from "./submit-button"; -import Link from "./link"; -import SubmitRow from "./submit-row"; +import { useState, useRef, useEffect } from "react"; +import { API_BASE } from "../../lib/api"; +import Card from "../card"; +import SubmitButton, { InlineStyledInput } from "../submit-button"; +import Link from "../link"; -const ProjectPanelContainer = styled.div` - margin: 0 auto; - width: 100%; - - @media (min-width: 1200px) { - max-width: 1200px; - } - - display: flex; - flex-direction: column; - gap: 12px; -`; - -const InputBlock = styled.p` +export const InputBlock = styled.p` margin-top: 10px; `; @@ -30,10 +14,6 @@ const InputNote = styled.span` font-size: 0.8rem; `; -function isNonEmptyArray(array) { - return Array.isArray(array) && array.length > 0; -} - function getNumberOrDefault(value, defaultValue = 0) { return Number.isNaN(value) ? defaultValue : value; } @@ -287,7 +267,7 @@ function DivisionCard({ ); } -function DivisionsRow({ +export default function DivisionsRow({ project, divisionUploads, updateDivision, @@ -324,216 +304,3 @@ function DivisionsRow({ ); } - -const roles = new Map() - .set("owner", "Owner") - .set("manager", "Manager") - .set("user", "User"); - -function MemberCard({ member }) { - return ( - -

{member.username}

- Role: {roles.get(member.role)} -
- ); -} - -function SaveChangesRow({ - project, - divisionUploads, - dirty, - setProject, - setDivisionUploads, -}) { - const onClick = useCallback(async () => { - // Create new divisions - const remappedUuids = new Map(); - - // eslint-disable-next-line no-restricted-syntax - for await (const division of project.divisions) { - if (division.create) { - const res = await makeApiRequest( - `/y22/projects/${project.uuid}/create_division`, - { - method: "POST", - } - ); - - const json = await res.json(); - - if (res.ok) { - remappedUuids.set(division.uuid, json.uuid); - } else if (json.error) { - throw new Error(json.error); - } - } else { - remappedUuids.set(division.uuid, division.uuid); - } - } - - // Update division images - const newUploads = { ...divisionUploads }; - - // eslint-disable-next-line no-restricted-syntax - for await (const [division, file] of Object.entries(divisionUploads)) { - const uuid = remappedUuids.get(division); - - const res = await makeApiRequest( - `/y22/projects/${project.uuid}/divisions/${uuid}/bitmap`, - { - method: "POST", - body: file, - } - ); - - if (res.ok) { - delete newUploads[division]; - } else { - const json = await res.json(); - - if (json.error) { - throw new Error(json.error); - } - } - } - - // Update divisions - const newDivisions = []; - - // eslint-disable-next-line no-restricted-syntax - for await (const division of project.divisions) { - const uuid = remappedUuids.get(division.uuid); - - const res = await makeApiRequest( - `/y22/projects/${project.uuid}/divisions/${uuid}`, - { - method: "POST", - body: JSON.stringify(division), - } - ); - - const json = await res.json(); - - if (res.ok) { - newDivisions.push(json.division); - } else if (json.error) { - throw new Error(json.error); - } - } - - setProject({ - ...project, - divisions: newDivisions, - }); - - setDivisionUploads(newUploads); - }); - - return ( - - ); -} - -function isDirty(project, initialProject, divisionUploads) { - // If a division image will be uploaded, then there are modifications - if (Object.keys(divisionUploads).length > 0) { - return true; - } - - // If any division has changed, then there are modifications - return project.divisions.some((division) => { - const initialDivision = initialProject.divisions.find( - (d) => d.uuid === division.uuid - ); - - // Newly added division - if (initialDivision === undefined) return true; - - // Division with changed values - if (division.name !== initialDivision.name) return true; - if (division.priority !== initialDivision.priority) return true; - if (division.enabled !== initialDivision.enabled) return true; - - if (division.origin[0] !== initialDivision.origin[0]) return true; - if (division.origin[1] !== initialDivision.origin[1]) return true; - - return false; - }); -} - -export default function ProjectPanel({ initialProject }) { - const [project, setProject] = useState(initialProject); - const [divisionUploads, setDivisionUploads] = useState({}); - - const dirty = isDirty(project, initialProject, divisionUploads); - - const updateDivision = useCallback( - (division) => { - setProject({ - ...project, - divisions: project.divisions.map((d) => { - return d.uuid === division.uuid ? division : d; - }), - }); - }, - [project] - ); - - const setDivisionUpload = useCallback( - (division, file) => { - setDivisionUploads({ - ...divisionUploads, - [division.uuid]: file, - }); - }, - [divisionUploads] - ); - - const addDivision = useCallback(() => { - setProject({ - ...project, - divisions: [ - ...project.divisions, - { - create: true, - uuid: uuidv4(), - origin: [0, 0], - dimensions: [null, null], - priority: 0, - enabled: true, - }, - ], - }); - }, [project]); - - return ( - - - - {isNonEmptyArray(project.members) ? ( - <> -

Members

- {project.members.map((member) => ( - - ))} - - ) : null} - {project.can_edit ? ( - - ) : null} -
- ); -} diff --git a/components/projects/members-row.js b/components/projects/members-row.js new file mode 100644 index 0000000..6d559af --- /dev/null +++ b/components/projects/members-row.js @@ -0,0 +1,35 @@ +import Card from "../card"; +import { InputBlock } from "./divisions-row"; + +function isNonEmptyArray(array) { + return Array.isArray(array) && array.length > 0; +} + +const roles = new Map() + .set("owner", "Owner") + .set("manager", "Manager") + .set("user", "User"); + +function MemberCard({ member }) { + return ( + +

{member.username}

+ Role: {roles.get(member.role)} +
+ ); +} + +export default function MembersRow({ members }) { + if (!isNonEmptyArray(members)) { + return null; + } + + return ( + <> +

Members

+ {members.map((member) => ( + + ))} + + ); +} diff --git a/components/projects/project-panel.js b/components/projects/project-panel.js new file mode 100644 index 0000000..6f88515 --- /dev/null +++ b/components/projects/project-panel.js @@ -0,0 +1,116 @@ +import styled from "styled-components"; +import { useCallback, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; +import DivisionsRow from "./divisions-row"; +import MembersRow from "./members-row"; +import ProjectPreview from "./project-preview"; +import SaveChangesRow from "./save-changes-row"; + +const ProjectPanelContainer = styled.div` + margin: 0 auto; + width: 100%; + + @media (min-width: 1200px) { + max-width: 1200px; + } + + display: flex; + flex-direction: column; + gap: 12px; +`; + +function isDirty(project, initialProject, divisionUploads) { + // If a division image will be uploaded, then there are modifications + if (Object.keys(divisionUploads).length > 0) { + return true; + } + + // If any division has changed, then there are modifications + return project.divisions.some((division) => { + const initialDivision = initialProject.divisions.find( + (d) => d.uuid === division.uuid + ); + + // Newly added division + if (initialDivision === undefined) return true; + + // Division with changed values + if (division.name !== initialDivision.name) return true; + if (division.priority !== initialDivision.priority) return true; + if (division.enabled !== initialDivision.enabled) return true; + + if (division.origin[0] !== initialDivision.origin[0]) return true; + if (division.origin[1] !== initialDivision.origin[1]) return true; + + return false; + }); +} + +export default function ProjectPanel({ initialProject }) { + const [project, setProject] = useState(initialProject); + const [divisionUploads, setDivisionUploads] = useState({}); + + const dirty = isDirty(project, initialProject, divisionUploads); + + const updateDivision = useCallback( + (division) => { + setProject({ + ...project, + divisions: project.divisions.map((d) => { + return d.uuid === division.uuid ? division : d; + }), + }); + }, + [project] + ); + + const setDivisionUpload = useCallback( + (division, file) => { + setDivisionUploads({ + ...divisionUploads, + [division.uuid]: file, + }); + }, + [divisionUploads] + ); + + const addDivision = useCallback(() => { + setProject({ + ...project, + divisions: [ + ...project.divisions, + { + create: true, + uuid: uuidv4(), + origin: [0, 0], + dimensions: [null, null], + priority: 0, + enabled: true, + }, + ], + }); + }, [project]); + + return ( + + + + + {project.can_edit ? ( + + ) : null} + + ); +} diff --git a/components/project-preview.js b/components/projects/project-preview.js similarity index 94% rename from components/project-preview.js rename to components/projects/project-preview.js index c8adb9c..deec33e 100644 --- a/components/project-preview.js +++ b/components/projects/project-preview.js @@ -1,8 +1,8 @@ import { useState } from "react"; -import { API_BASE, makeApiRequest } from "../lib/api"; -import Card from "./card"; -import Link from "./link"; -import SubmitButton from "./submit-button"; +import { API_BASE, makeApiRequest } from "../../lib/api"; +import Card from "../card"; +import Link from "../link"; +import SubmitButton from "../submit-button"; class MembershipStatus { static Joining = new MembershipStatus("Joining..."); diff --git a/components/projects/save-changes-row.js b/components/projects/save-changes-row.js new file mode 100644 index 0000000..f577e3d --- /dev/null +++ b/components/projects/save-changes-row.js @@ -0,0 +1,99 @@ +import { useCallback } from "react"; +import { makeApiRequest } from "../../lib/api"; +import SubmitRow from "../submit-row"; + +export default function SaveChangesRow({ + project, + divisionUploads, + dirty, + setProject, + setDivisionUploads, +}) { + const onClick = useCallback(async () => { + // Create new divisions + const remappedUuids = new Map(); + + // eslint-disable-next-line no-restricted-syntax + for await (const division of project.divisions) { + if (division.create) { + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/create_division`, + { + method: "POST", + } + ); + + const json = await res.json(); + + if (res.ok) { + remappedUuids.set(division.uuid, json.uuid); + } else if (json.error) { + throw new Error(json.error); + } + } else { + remappedUuids.set(division.uuid, division.uuid); + } + } + + // Update division images + const newUploads = { ...divisionUploads }; + + // eslint-disable-next-line no-restricted-syntax + for await (const [division, file] of Object.entries(divisionUploads)) { + const uuid = remappedUuids.get(division); + + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/divisions/${uuid}/bitmap`, + { + method: "POST", + body: file, + } + ); + + if (res.ok) { + delete newUploads[division]; + } else { + const json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } + } + } + + // Update divisions + const newDivisions = []; + + // eslint-disable-next-line no-restricted-syntax + for await (const division of project.divisions) { + const uuid = remappedUuids.get(division.uuid); + + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/divisions/${uuid}`, + { + method: "POST", + body: JSON.stringify(division), + } + ); + + const json = await res.json(); + + if (res.ok) { + newDivisions.push(json.division); + } else if (json.error) { + throw new Error(json.error); + } + } + + setProject({ + ...project, + divisions: newDivisions, + }); + + setDivisionUploads(newUploads); + }); + + return ( + + ); +} diff --git a/pages/projects/[uuid].js b/pages/projects/[uuid].js index 6e05e96..1d2d815 100644 --- a/pages/projects/[uuid].js +++ b/pages/projects/[uuid].js @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { PageTitle } from "../../lib/common-style"; import { makeApiRequest } from "../../lib/api"; -import ProjectPanel from "../../components/project-panel"; +import ProjectPanel from "../../components/projects/project-panel"; import NotFoundPage from "../404"; export default function ProjectPage() { diff --git a/pages/projects/index.js b/pages/projects/index.js index 7187444..1468273 100644 --- a/pages/projects/index.js +++ b/pages/projects/index.js @@ -3,7 +3,7 @@ import styled from "styled-components"; import { useEffect, useState } from "react"; import { Box, PageTitle } from "../../lib/common-style"; import { makeApiRequest } from "../../lib/api"; -import ProjectPreview from "../../components/project-preview"; +import ProjectPreview from "../../components/projects/project-preview"; import { StyledInput } from "../../components/submit-button"; import useFilter from "../../lib/hooks/useFilter"; import { isMatchingString, filterArray } from "../../lib/filter"; From bf55d7ef351eb5f82d3284cd30deece03bc4bf5f Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Fri, 21 Jul 2023 17:10:39 -0400 Subject: [PATCH 09/10] Sort project members by role and username --- components/projects/members-row.js | 37 ++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/components/projects/members-row.js b/components/projects/members-row.js index 6d559af..3559880 100644 --- a/components/projects/members-row.js +++ b/components/projects/members-row.js @@ -6,15 +6,26 @@ function isNonEmptyArray(array) { } const roles = new Map() - .set("owner", "Owner") - .set("manager", "Manager") - .set("user", "User"); + .set("owner", { + name: "Owner", + priority: 0, + }) + .set("manager", { + name: "Manager", + priority: 1, + }) + .set("user", { + name: "User", + priority: 2, + }); function MemberCard({ member }) { return (

{member.username}

- Role: {roles.get(member.role)} + + Role: {roles.get(member.role)?.name ?? member.role} +
); } @@ -27,9 +38,21 @@ export default function MembersRow({ members }) { return ( <>

Members

- {members.map((member) => ( - - ))} + {members + .sort((a, b) => { + let sort = + (roles.get(a.role)?.priority ?? 0) - + (roles.get(b.role)?.priority ?? 0); + if (sort !== 0) return sort; + + sort = a.username.localeCompare(b.username); + if (sort !== 0) return sort; + + return a.uid.localeCompare(b.uid); + }) + .map((member) => ( + + ))} ); } From 411467b43f938ebd2d229ffc70761db3b11fb2c9 Mon Sep 17 00:00:00 2001 From: haykam821 <24855774+haykam821@users.noreply.github.com> Date: Fri, 21 Jul 2023 18:38:09 -0400 Subject: [PATCH 10/10] Allow deleting project divisions --- .eslintrc.json | 1 + components/icon-button.js | 70 +++++++++++++++++++ components/projects/divisions-row.js | 66 +++++++++++++----- components/projects/project-panel.js | 3 + components/projects/save-changes-row.js | 89 +++++++++++++++++-------- 5 files changed, 185 insertions(+), 44 deletions(-) create mode 100644 components/icon-button.js diff --git a/.eslintrc.json b/.eslintrc.json index d015ca9..6077d4e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,6 +25,7 @@ "error", { "controlComponents": [ + "HiddenInput", "InlineStyledInput", "StyledInput" ] diff --git a/components/icon-button.js b/components/icon-button.js new file mode 100644 index 0000000..d0aa48b --- /dev/null +++ b/components/icon-button.js @@ -0,0 +1,70 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import styled, { css } from "styled-components"; +import Link from "./link"; + +const HiddenInput = styled.input` + display: none; +`; + +const elementStyle = css` + display: inline-block; + + user-select: none; + cursor: pointer; + + margin: 10px 0; + padding: 10px; + + font-size: calc(40px / 3); + + border-radius: 2px; + border: 1px solid + ${(props) => props.theme.colors[props.danger ? "danger" : "primary"]}; + + transition: background-color 0.25s, color 0.25s; + + background-color: ${(props) => + props.theme.colors[props.danger ? "danger" : "primary"]}; + color: ${(props) => props.theme.colors.background}; + + &:hover { + background-color: ${(props) => props.theme.colors.background}; + color: ${(props) => + props.theme.colors[props.danger ? "danger" : "primary"]}; + } +`; + +const IconButtonLabel = styled.label` + ${elementStyle} +`; + +export function InputIconButton({ + icon, + danger, + children, + inputRef, + ...props +}) { + return ( + + + {children} + + ); +} + +const IconButtonLink = styled(Link)` + ${elementStyle} + + &:hover { + text-decoration: none; + } +`; + +export function LinkIconButton({ icon, danger, children, href }) { + return ( + + {children} + + ); +} diff --git a/components/projects/divisions-row.js b/components/projects/divisions-row.js index a03f993..9563c8a 100644 --- a/components/projects/divisions-row.js +++ b/components/projects/divisions-row.js @@ -1,9 +1,15 @@ import styled from "styled-components"; import { useState, useRef, useEffect } from "react"; +import { + faDownload, + faTrash, + faTrashArrowUp, + faUpload, +} from "@fortawesome/free-solid-svg-icons"; import { API_BASE } from "../../lib/api"; import Card from "../card"; import SubmitButton, { InlineStyledInput } from "../submit-button"; -import Link from "../link"; +import { InputIconButton, LinkIconButton } from "../icon-button"; export const InputBlock = styled.p` margin-top: 10px; @@ -109,10 +115,36 @@ function DivisionCoordinates({ ); } -function ImageManagementBlock({ +function DeleteButton({ division, updateDivision }) { + return ( + { + updateDivision({ + ...division, + delete: !division.delete, + }); + }} + > + {division.delete ? "Cancel Deletion" : "Delete"} + + ); +} + +const ActionRowBlock = styled(InputBlock)` + display: flex; + flex-wrap: wrap; + gap: 0 12px; +`; + +function ActionsBlock({ imageUrl, division, divisionUpload, + updateDivision, setDivisionUpload, }) { const uploadRef = useRef(null); @@ -130,20 +162,20 @@ function ImageManagementBlock({ }, [divisionUpload]); return ( - + {imageUrl == null ? null : ( - <> - - Download Division Image - {" "} - •{" "} - + + Download Image + )} - Replace Division Image:{" "} - { const file = event.target.files?.[0]; @@ -151,8 +183,11 @@ function ImageManagementBlock({ setDivisionUpload(division, file); } }} - /> - + > + {imageUrl || divisionUpload ? "Replace" : "Upload"} Image + + + ); } @@ -256,10 +291,11 @@ function DivisionCard({
{project.can_edit ? ( - ) : null} diff --git a/components/projects/project-panel.js b/components/projects/project-panel.js index 6f88515..a16627d 100644 --- a/components/projects/project-panel.js +++ b/components/projects/project-panel.js @@ -34,6 +34,9 @@ function isDirty(project, initialProject, divisionUploads) { // Newly added division if (initialDivision === undefined) return true; + // Deleted division + if (division.delete) return true; + // Division with changed values if (division.name !== initialDivision.name) return true; if (division.priority !== initialDivision.priority) return true; diff --git a/components/projects/save-changes-row.js b/components/projects/save-changes-row.js index f577e3d..6da49fe 100644 --- a/components/projects/save-changes-row.js +++ b/components/projects/save-changes-row.js @@ -15,7 +15,7 @@ export default function SaveChangesRow({ // eslint-disable-next-line no-restricted-syntax for await (const division of project.divisions) { - if (division.create) { + if (division.create && !division.delete) { const res = await makeApiRequest( `/y22/projects/${project.uuid}/create_division`, { @@ -39,24 +39,53 @@ export default function SaveChangesRow({ const newUploads = { ...divisionUploads }; // eslint-disable-next-line no-restricted-syntax - for await (const [division, file] of Object.entries(divisionUploads)) { - const uuid = remappedUuids.get(division); - - const res = await makeApiRequest( - `/y22/projects/${project.uuid}/divisions/${uuid}/bitmap`, - { - method: "POST", - body: file, - } + for await (const [divisionUuid, file] of Object.entries( + divisionUploads + )) { + const division = project.divisions.find( + (d) => d.uuid === divisionUuid ); - if (res.ok) { - delete newUploads[division]; - } else { - const json = await res.json(); + if (!division.delete) { + const uuid = remappedUuids.get(division.uuid); - if (json.error) { - throw new Error(json.error); + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/divisions/${uuid}/bitmap`, + { + method: "POST", + body: file, + } + ); + + if (res.ok) { + delete newUploads[division]; + } else { + const json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } + } + } + } + + // Delete divisions + // eslint-disable-next-line no-restricted-syntax + for await (const division of project.divisions) { + if (!division.create && division.delete) { + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/divisions/${division.uuid}`, + { + method: "DELETE", + } + ); + + if (!res.ok) { + const json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } } } } @@ -66,22 +95,24 @@ export default function SaveChangesRow({ // eslint-disable-next-line no-restricted-syntax for await (const division of project.divisions) { - const uuid = remappedUuids.get(division.uuid); + if (!division.delete) { + const uuid = remappedUuids.get(division.uuid); - const res = await makeApiRequest( - `/y22/projects/${project.uuid}/divisions/${uuid}`, - { - method: "POST", - body: JSON.stringify(division), - } - ); + const res = await makeApiRequest( + `/y22/projects/${project.uuid}/divisions/${uuid}`, + { + method: "POST", + body: JSON.stringify(division), + } + ); - const json = await res.json(); + const json = await res.json(); - if (res.ok) { - newDivisions.push(json.division); - } else if (json.error) { - throw new Error(json.error); + if (res.ok) { + newDivisions.push(json.division); + } else if (json.error) { + throw new Error(json.error); + } } }