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);
+ }
}
}