Skip to content

Commit

Permalink
feat(Issue Tracker): Add the issue tracker component (#142)
Browse files Browse the repository at this point in the history
* feat(Issue Tracker): Add the issue tracker component which connects through a users ThoriumSim.com account. Closes #83

* Move the logout button to under the dropdown menu.

* Fix some broken types

* Fix a small linting error
  • Loading branch information
alexanderson1993 authored Dec 7, 2021
1 parent 6622ae3 commit ac16ff7
Show file tree
Hide file tree
Showing 12 changed files with 1,100 additions and 97 deletions.
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@mdx-js/react": "^1.6.22",
"@msgpack/msgpack": "^2.7.1",
"@tailwindcss/typography": "^0.4.1",
"@thorium-sim/react-mde": "^11.5.0",
"@uiw/react-monacoeditor": "^3.4.5",
"color": "^4.0.1",
"eventemitter3": "^4.0.7",
Expand All @@ -40,6 +41,7 @@
"remark-frontmatter": "^3.0.0",
"remark-mdx-images": "^1.0.3",
"remark-prism": "^1.3.6",
"remark-rehype": "^10.1.0",
"three": "^0.133.0",
"valtio": "^1.2.4",
"vite-plugin-mdx": "^3.5.6",
Expand Down
273 changes: 273 additions & 0 deletions client/src/components/IssueTracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useEffect,
useState,
} from "react";
import {FaExternalLinkAlt} from "react-icons/fa";
import Button from "@thorium/ui/Button";
import LoginButton from "./LoginButton";
import Modal from "@thorium/ui/Modal";
import {useThoriumAccount} from "../context/ThoriumAccountContext";
import Input from "@thorium/ui/Input";
import MarkdownInput from "@thorium/ui/MarkdownInput";
import randomWords from "@thorium/random-words";
import {toast} from "../context/ToastContext";
import packageJson from "../../../package.json";

const availableLabels = [
"Feature",
"Bug",
"Design",
"Documentation",
"Question",
"Compliment",
];
const IssueTrackerContext = createContext<{
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
}>(null!);

export const IssueTrackerProvider = ({children}: {children: ReactNode}) => {
const [open, setOpen] = useState(false);

return (
<IssueTrackerContext.Provider value={{open, setOpen}}>
{children}
<IssueTracker open={open} setOpen={setOpen} />
</IssueTrackerContext.Provider>
);
};

export const useIssueTracker = () => {
const value = useContext(IssueTrackerContext);
if (!value)
throw new Error("useIssueTracker used outside of context provider");
return value;
};

function IssueTracker({
open,
setOpen,
}: {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
}) {
const {account, refresh} = useThoriumAccount();
const [body, setBody] = useState("");
const [invalid, setInvalid] = useState<string[]>([]);

useEffect(
function revalidateOnVisibilityChange() {
if (!open) return;
function onVisibilityChange() {
refresh();
}
window.addEventListener("visibilitychange", onVisibilityChange);
return () =>
window.removeEventListener("visibilitychange", onVisibilityChange);
},
[refresh, open]
);
useEffect(
function revalidateOnFocus() {
if (!open) return;
function onFocus() {
refresh();
}
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
},
[refresh, open]
);
return (
<Modal
isOpen={open}
setIsOpen={open => setOpen(open)}
title="Submit an Issue"
>
{!account ? (
<div className="text-center flex items-center justify-center flex-col gap-8 my-8">
<h2 className="font-bold text-3xl">Sign in to Submit Issues</h2>
<p className="w-64">
To better track issues and collect feedback, you must be logged in
to a Thorium Nova account.
</p>
<div>
<LoginButton buttonClassName="btn-alert" />
</div>
</div>
) : !account.accounts?.includes("github") ? (
<div className="text-center flex items-center justify-center flex-col gap-8 my-8">
<h2 className="font-bold text-3xl">Connect to Github</h2>
<p className="w-64">
To submit issues, you must connect your Thorium Nova account to
Github.
</p>
<div>
<a
href="https://thoriumsim.com/profile"
target="thoriumsim.com"
className="btn btn-alert"
>
Connect Account <FaExternalLinkAlt className="ml-4" />
</a>
</div>
</div>
) : (
<form
className="flex flex-col gap-4 my-4"
onSubmit={async event => {
event.preventDefault();
const title = event.currentTarget.issueTitle.value;
const label = event.currentTarget.label.value;
let isInvalid = false;
if (!title) {
isInvalid = true;
setInvalid(invalid => invalid.concat("title"));
}
if (!label || label === "Select One") {
isInvalid = true;
setInvalid(invalid => invalid.concat("type"));
}
if (isInvalid) {
return;
}
const requestBody = {
repo: "thorium-nova",
title,
body: `${body}
### Version: ${packageJson.version}`,
labels: [label],
};
setOpen(false);
try {
const response = await fetch(
`${process.env.THORIUMSIM_URL}/api/issues`,
{
method: "POST",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${account.access_token}`,
},
}
);
const result = await response.json();
if (!response.ok || result.error) {
toast({
title: "Error Submitting Issue",
body: result.error || "An unknown error occurred",
color: "error",
});
return;
}

toast({title: "Issue Submitted!", color: "success"});
setBody("");
} catch (err) {
if (err instanceof Error) {
toast({
title: "Error Submitting Issue",
body: err.message || "An unknown error occurred",
color: "error",
});
}
if (typeof err === "string") {
toast({
title: "Error Submitting Issue",
body: err || "An unknown error occurred",
color: "error",
});
}
}
}}
>
<Input
label="Title"
name="issueTitle"
isInvalid={invalid.includes("title")}
invalidMessage="Title is required."
onFocus={() => setInvalid(invalid.filter(i => i !== "title"))}
/>
<label className="flex flex-col">
Body
<MarkdownInput
value={body}
setValue={newBody => {
setBody(newBody);
}}
name="body"
saveImage={async function* saveImage(data: ArrayBuffer) {
const uploadDataRequest = await fetch(
`${process.env.THORIUMSIM_URL}/api/uploadData`,
{
headers: {
Authorization: `Bearer ${account.access_token}`,
},
}
);
const uploadData = await uploadDataRequest.json();
if (!uploadDataRequest.ok) {
toast({
title: "Error Uploading File",
body: uploadData.error,
color: "error",
});
throw new Error(uploadData.error);
}
const nameParts = randomWords(3);
const name = `${
Array.isArray(nameParts) ? nameParts.join("-") : nameParts
}-${Date.now()}`;
const image = new File([new Blob([data])], `${name}.png`, {
type: "image/png",
});
const result = await fetch(uploadData.uploadUrl, {
method: "POST",
body: image,
headers: {
Authorization: uploadData.authorizationToken,
"Content-Type": "b2/x-auto",
"X-Bz-File-Name": `issue_uploads/${image.name.replace(
/\s/gm,
"-"
)}`,
"X-Bz-Content-Sha1": "do_not_verify",
},
}).then(res => res.json());
const {fileName} = result;
const url = `https://files.thoriumsim.com/file/thorium-public/${fileName}`;
yield url;
// returns true meaning that the save was successful
return true;
}}
/>
</label>
<Input
as="select"
label="Type"
className="form-select block w-full select max-w-xs select-sm"
isInvalid={invalid.includes("type")}
invalidMessage="Type is required."
onChange={e =>
e.target.value !== "Select One" &&
setInvalid(invalid.filter(i => i !== "type"))
}
name="label"
>
<option>Select One</option>
{availableLabels.map(label => (
<option key={label}>{label}</option>
))}
</Input>
<Button className="btn-success self-end">Submit</Button>
</form>
)}
</Modal>
);
}
Loading

0 comments on commit ac16ff7

Please sign in to comment.