diff --git a/App.js b/App.js
new file mode 100644
index 00000000..171b59f8
--- /dev/null
+++ b/App.js
@@ -0,0 +1,193 @@
+import React, { useEffect, useState } from "react";
+import "../App.css";
+import { connect } from "react-redux";
+import Amplify, { PubSub } from "aws-amplify";
+import { AuthState, onAuthUIStateChange } from "@aws-amplify/ui-components";
+import * as projectsActions from "../actions/projects";
+import * as notesActions from "../actions/notes";
+import * as appActions from "../actions/app";
+import * as userActions from "../actions/user";
+import aws_exports from "../aws-exports";
+import { Route, useHistory, useRouteMatch } from "react-router-dom";
+import ProjectsPanel from "./ProjectsPanel";
+import SidePanel from "./SidePanel";
+import NotesPanel from "./NotesPanel";
+import Loading from "./Loading";
+import Login from "./Login";
+Amplify.configure(aws_exports);
+PubSub.configure(aws_exports);
+import {ToastProvider, DefaultToast} from 'react-toast-notifications'
+const App = (props) => {
+
+
+ const { dispatch, user, projects, app } = props;
+ const [state, setState] = useState({
+ projectPanel: false,
+ sidePanel: false,
+ });
+ const history = useHistory();
+ const routeMatch = useRouteMatch({
+ exact: true,
+ sensitive: true,
+ path: [
+ "/local/:projectPermalink",
+ "/:username/:projectPermalink/:notePermalink",
+ "/:username/:projectPermalink",
+ "/",
+ ],
+ });
+
+ const fetchLocalProjects = () => {
+ if (user.state !== AuthState.SignedIn) {
+ dispatch(projectsActions.handleFetchOwnedProjects());
+ }
+ };
+
+ const setHideShowProjectPanel = () => {
+ setState((state) => {
+ return { ...state, sidePanel: false, projectPanel: !state.projectPanel };
+ });
+ };
+
+ const setHideShowSidePanel = () => {
+ setState((state) => {
+ return { ...state, projectPanel: false, sidePanel: !state.sidePanel };
+ });
+ };
+
+ useEffect(() => {
+ dispatch(appActions.setHistory(history));
+ onAuthUIStateChange(async (nextAuthState, authData) => {
+ dispatch(userActions.handleSetData(authData));
+ dispatch(userActions.setState(nextAuthState));
+ if (nextAuthState === AuthState.SignedIn) {
+ window.removeEventListener("storage", fetchLocalProjects);
+ }
+ });
+ if (user.state !== AuthState.SignedIn) {
+ window.addEventListener("storage", fetchLocalProjects);
+ }
+ }, []);
+ useEffect(() => {
+ if (routeMatch) {
+ const {
+ params: { username, projectPermalink, notePermalink },
+ } = routeMatch;
+ if (history.action === "POP") {
+ if (!app.isLoading) {
+ if (user.state === AuthState.SignedIn) {
+ if (username && projectPermalink) {
+ const allProjects = Object.values({
+ ...projects.owned,
+ ...projects.assigned,
+ });
+ const reqUserProjects = allProjects.filter(
+ (x) => x.owner === username
+ );
+ if (reqUserProjects.length > 0) {
+ const reqProject = reqUserProjects.filter(
+ (x) => x.permalink === projectPermalink
+ )[0];
+ if (reqProject) {
+ dispatch(appActions.handleSetProject(reqProject.id, false));
+ if (notePermalink) {
+ dispatch(notesActions.handleFetchNotes(reqProject.id)).then(
+ (notes) => {
+ const reqNote = Object.values(notes).filter(
+ (x) =>
+ x.permalink === parseInt(params.notePermalink, 10)
+ )[0];
+ if (reqNote) {
+ dispatch(appActions.handleSetNote(reqNote.id, false));
+ }
+ }
+ );
+ } else {
+ history.replace(`/${username}/${projectPermalink}`);
+ }
+ } else {
+ history.replace("/");
+ }
+ } else {
+ history.replace("/");
+ }
+ }
+ } else {
+ if (projectPermalink) {
+ const reqProject = Object.values(projects.owned).filter(
+ (x) => x.permalink === projectPermalink
+ )[0];
+ if (reqProject) {
+ dispatch(appActions.handleSetProject(reqProject.id, false));
+ } else {
+ history.replace("/");
+ }
+ }
+ }
+ }
+ }
+ }
+ }, [routeMatch, app, user]);
+ const MyCustomToast = ({ children, ...props }) => (
+
+
+
+ );
+ return (
+
+
+
+
+
}
+ />
+ (
+ <>
+ {app.isLoading ? (
+
+ ) : (
+
+ )}
+ >
+ )}
+ />
+
+
+ );
+};
+
+export default connect((state) => ({
+ user: state.user,
+ projects: state.projects,
+ notes: state.notes,
+ app: state.app,
+}))(App);
diff --git a/NotesPanel.js b/NotesPanel.js
new file mode 100644
index 00000000..8d879b49
--- /dev/null
+++ b/NotesPanel.js
@@ -0,0 +1,218 @@
+import {
+ sortableContainer,
+ sortableElement,
+ sortableHandle,
+} from "react-sortable-hoc";
+import styledComponents from "styled-components";
+import { connect } from "react-redux";
+import parseLinkedList from "../utils/parseLinkedList";
+import handlerIcon from "../assets/apps.svg";
+import TaskItem from "./TaskItem";
+import NewTask from "./NewTask";
+import ShareBtn from "./ShareBtn";
+import ProjectNotSelected from "./ProjectNotSelected";
+import * as notesActions from "../actions/notes";
+import PasteBtn from "./PasteBtn";
+import * as projectsActions from "../actions/projects";
+import { initProjectState, OK, PENDING, initNoteState } from "../constants";
+import useWindowSize from "../utils/useWindowSize";
+import { useToasts } from 'react-toast-notifications'
+
+
+const DragHandle = sortableHandle(() => (
+
+));
+
+const SortableItem = sortableElement(
+ ({ index, value, readOnly, setHideShowSidePanel }) => (
+ }
+ />
+ )
+);
+
+const SortableContainer = sortableContainer(({ children }) => {
+ return
{children}
;
+});
+
+const NotesPanel = (props) => {
+ const { addToast } = useToasts();
+ const {
+ app,
+ notes,
+ projects,
+ dispatch,
+ setHideShowProjectPanel,
+ setHideShowSidePanel,
+ } = props;
+ let { width } = useWindowSize();
+ const onSortEnd = ({ oldIndex, newIndex }) => {
+ if (oldIndex > newIndex) {
+ const sortedNotes = parseLinkedList(notes, "prevNote", "nextNote");
+ dispatch(
+ notesActions.handleUpdateNote({
+ id: sortedNotes[oldIndex].id,
+ prevNote: sortedNotes[newIndex - 1]?.id || null,
+ nextNote: sortedNotes[newIndex]?.id || null,
+ })
+ );
+ } else if (oldIndex < newIndex) {
+ const sortedNotes = parseLinkedList(notes, "prevNote", "nextNote");
+ dispatch(
+ notesActions.handleUpdateNote({
+ id: sortedNotes[oldIndex].id,
+ prevNote: sortedNotes[newIndex]?.id || null,
+ nextNote: sortedNotes[newIndex + 1]?.id || null,
+ })
+ );
+ }
+ };
+ return (
+
+ {
+ e.target.getAttribute("name") === "NotesPanelContainer" &&
+ Object.keys(projects.owned).includes(app.selectedProject) &&
+ app.noteAddingStatus === OK &&
+ dispatch(
+ notesActions.handleCreateNote(
+ initNoteState(
+ app.selectedProject,
+ parseLinkedList(notes, "prevNote", "nextNote").reverse()[0]?.id
+ )
+ )
+ )
+ }}
+ onKeyDown={(e) => {
+ if (e.code === "Enter") {
+ addToast('added a note')
+ }
+ }}
+ >
+ {app.selectedProject ? (
+ <>
+
+
+ {Object.keys(projects.owned).includes(app.selectedProject) && (
+
+ )}
+
+ {
+ { ...projects.owned, ...projects.assigned }[app.selectedProject]
+ .title
+ }
+
+
+ {parseLinkedList(notes, "prevNote", "nextNote").map(
+ (value, index) => (
+
+ )
+ )}
+
+ >
+ ) : (
+ <>
+
+ {width <= 768 && (
+
+
+ app.projectAddingStatus === OK &&
+ dispatch(
+ projectsActions.handleCreateProject(
+ initProjectState(
+ parseLinkedList(
+ projects["owned"],
+ "prevProject",
+ "nextProject"
+ ).reverse()[0]?.id
+ )
+ )
+ )
+ }
+ >
+ +
+
+
+ )}
+ >
+ )}
+
+ );
+};
+
+const NotesPanelContainer = styledComponents.div`
+ flex: 3;
+ padding: 40px;
+ overflow: auto;
+ max-height: calc(100vh - 80px);
+`;
+
+const NotesTitle = styledComponents.div`
+ font-weight: 600;
+ margin: 0 8px 20px 8px;
+ font-size: 2em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+`;
+
+const Button = styledComponents.button`
+background: none;
+border: none;
+font-size: 14px;
+font-weight: 600;
+display: none;
+@media only screen and (max-width: 768px) {
+display: block;
+}
+`;
+
+const ProjectAdder = styledComponents.div`
+ & > span {
+ height: 20px;
+ float: right;
+ font-size: 1.5em;
+ width: 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 5px;
+ border-radius: 4px;
+ font-weight: bold;
+ transition: background-color 0.3s;
+ ${({ isInactive }) =>
+ isInactive
+ ? `
+ color: #D3D3D3;
+ `
+ : `
+ cursor: pointer;
+ color: #222222;
+ &:hover {
+ background-color: #E4E4E2;
+ }
+ `}
+ }
+`;
+
+export default connect((state) => ({
+ notes: state.notes,
+ app: state.app,
+ projects: state.projects,
+}))(NotesPanel);
diff --git a/ProjectsPanel.js b/ProjectsPanel.js
new file mode 100644
index 00000000..cea6adfa
--- /dev/null
+++ b/ProjectsPanel.js
@@ -0,0 +1,280 @@
+import { useState, useEffect } from "react";
+import styledComponents from "styled-components";
+import {
+ sortableContainer,
+ sortableElement,
+ sortableHandle,
+} from "react-sortable-hoc";
+import { connect } from "react-redux";
+import * as projectsActions from "../actions/projects";
+import ProjectItem from "./ProjectItem";
+import { AuthState } from "@aws-amplify/ui-components";
+import { initProjectState, OK, PENDING } from "../constants";
+import parseLinkedList from "../utils/parseLinkedList";
+import useWindowSize from "../utils/useWindowSize";
+
+const DragHandle = sortableHandle(() => );
+
+const SortableItem = sortableElement(({ index, value }) => (
+ } />
+));
+
+const SortableContainer = sortableContainer(({ children }) => {
+ return {children}
;
+});
+
+const ProjectsPanel = (props) => {
+ const {
+ user,
+ app,
+ projects,
+ dispatch,
+ setHideShowProjectPanel,
+ projectPanel,
+ } = props;
+ let { width } = useWindowSize();
+ const [scope, setScope] = useState("owned");
+ const onSortEnd = ({ oldIndex, newIndex }) => {
+ if (oldIndex > newIndex) {
+ const sortedProjects = parseLinkedList(
+ projects["owned"],
+ "prevProject",
+ "nextProject"
+ );
+ dispatch(
+ projectsActions.handleUpdateProject({
+ id: sortedProjects[oldIndex].id,
+ prevProject: sortedProjects[newIndex - 1]?.id || null,
+ nextProject: sortedProjects[newIndex]?.id || null,
+ })
+ );
+ } else if (oldIndex < newIndex) {
+ const sortedProjects = parseLinkedList(
+ projects["owned"],
+ "prevProject",
+ "nextProject"
+ );
+ dispatch(
+ projectsActions.handleUpdateProject({
+ id: sortedProjects[oldIndex].id,
+ prevProject: sortedProjects[newIndex]?.id || null,
+ nextProject: sortedProjects[newIndex + 1]?.id || null,
+ })
+ );
+ }
+ };
+ const loadDataOnlyOnce = async () => {
+ try {
+ if (
+ app.projectAddingStatus === OK &&
+ Object.keys(projects.owned).length === 0
+ ) {
+ let initProject = initProjectState(
+ parseLinkedList(
+ projects["owned"],
+ "prevProject",
+ "nextProject"
+ ).reverse()[0]?.id
+ );
+ await dispatch(projectsActions.handleCreateProject(initProject));
+ await dispatch(appActions.handleSetProject(initProject.id));
+ }
+ } catch (err) {
+ console.log("err when initial project", err);
+ }
+ };
+ useEffect(() => {
+ loadDataOnlyOnce();
+ }, []);
+
+ return (
+
+ {width <= 768 && (
+
+ )}
+ {user.state === AuthState.SignedIn && (
+
+
+ scope !== "owned" && setScope("owned")}
+ >
+ Owned
+
+ scope !== "assigned" && setScope("assigned")}
+ >
+ Assigned
+
+
+
+ )}
+
+ {scope === "assigned" ? (
+ <>
+ {Object.values(projects[scope]).map((project) => (
+
+ ))}
+ >
+ ) : (
+
+ {parseLinkedList(projects[scope], "prevProject", "nextProject").map(
+ (value, index) => (
+
+ )
+ )}
+
+ )}
+
+ {scope === "owned" && (
+
+
+ app.projectAddingStatus === OK &&
+ dispatch(
+ projectsActions.handleCreateProject(
+ initProjectState(
+ parseLinkedList(
+ projects["owned"],
+ "prevProject",
+ "nextProject"
+ ).reverse()[0]?.id
+ )
+ )
+ )
+ }
+ >
+ +
+
+
+ )}
+
+ );
+};
+
+const Button = styledComponents.button`
+background: none;
+border: none;
+font-size: 14px;
+font-weight: 600;
+position: absolute;
+top: 1%;
+right: 4px;
+`;
+
+const Panel = styledComponents.div`
+ background-color: #FFFFFF;
+ flex: 2;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0px 0px 8px 1px #dadada;
+ top: 0;
+ left: 0;
+ & > span {
+ font-weight: 600;
+ cursor: pointer;
+ }
+ @media only screen and (max-width: 768px) {
+ flex-direction: column;
+ height: 100vh;
+ text-align: left;
+ padding: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ transition: transform 0.3s ease-in-out;
+ transform: ${({ open }) => (open ? "translateX(0)" : "translateX(-100%)")};
+ z-index: 1;
+ min-width: 300px;
+ }
+`;
+
+const ProjectHandler = styledComponents.div`
+ background-color: #00000050;
+ width: 8px;
+`;
+
+const PanelTabs = styledComponents.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 90px;
+ & > div {
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
+ padding: 5px;
+ background-color: #BDBDBD;
+ border-radius: 24px;
+ height: fit-content;
+ width: fit-content;
+ & > span {
+ border-radius: 24px;
+ padding: 2px 10px;
+ font-weight: 600;
+ display: flex;
+ justify-content: center;
+ background-color: transparent;
+ color: #FFFFFF;
+ cursor: pointer;
+ &.active {
+ color: #222222;
+ background-color: #FFFFFF;
+ cursor: default;
+ }
+ }
+ }
+`;
+
+const ProjectItems = styledComponents.div`
+ overflow-y: auto;
+ padding: 10px 0;
+ flex: 1;
+ & > div:last-child > div {
+ border-bottom: 1px solid #E4E4E2;
+ }
+
+ @media only screen and (max-width: 768px) {
+ padding: 30px 0;
+ overflow: unset;
+ }
+`;
+
+const ProjectAdder = styledComponents.div`
+ padding: 10px;
+ & > span {
+ height: 20px;
+ float: right;
+ font-size: 1.5em;
+ width: 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 5px;
+ border-radius: 4px;
+ font-weight: bold;
+ transition: background-color 0.3s;
+ ${({ isInactive }) =>
+ isInactive
+ ? `
+ color: #D3D3D3;
+ `
+ : `
+ cursor: pointer;
+ color: #222222;
+ &:hover {
+ background-color: #E4E4E2;
+ }
+ `}
+ }
+`;
+
+export default connect((state) => ({
+ user: state.user,
+ app: state.app,
+ projects: state.projects,
+}))(ProjectsPanel);
diff --git a/SidePanel.js b/SidePanel.js
new file mode 100644
index 00000000..071a7425
--- /dev/null
+++ b/SidePanel.js
@@ -0,0 +1,363 @@
+import { connect } from "react-redux";
+import { DatePicker } from "./DatePicker";
+import * as notesActions from "../actions/notes";
+import { AuthState } from "@aws-amplify/ui-components";
+import styledComponents from "styled-components";
+import ShareBtn from "./ShareBtn";
+import { Select } from "./Select";
+import { Tag } from "./Tag";
+import Comments from "./Comments";
+import "draft-js/dist/Draft.css";
+import AssigneeField from "./AssigneeField";
+import useWindowSize from "../utils/useWindowSize";
+import { useToasts } from 'react-toast-notifications'
+const SidePanel = (props) => {
+ const { addToast } = useToasts();
+ const {
+ user,
+ notes,
+ app,
+ readOnly,
+ dispatch,
+ sidePanel,
+ setHideShowSidePanel,
+ } = props;
+ let { width } = useWindowSize();
+ const handleChange = (e) => {
+ dispatch(
+ notesActions.handleUpdateNote({
+ id: app.selectedNote,
+ [e.target.name]: e.target.value,
+ })
+ );
+ };
+ return (
+
+ {width <= 768 && (
+
+ )}
+ {app.selectedNote && (
+ <>
+ e.preventDefault()}>
+
+ {
+ if (e.code === "Enter") {
+ addToast('added a note')
+ }
+ }}
+ type="text"
+ name="note"
+ placeholder="Note…"
+ onChange={handleChange}
+ value={notes[app.selectedNote].note || ""}
+ contentEditable={false}
+ readOnly={readOnly}
+ >
+ {user.state === AuthState.SignedIn && (
+
+ )}
+
+
+ {
+ if (e.code === "Enter") {
+ addToast('added a task')
+ }
+ }}
+ type="text"
+ name="task"
+ placeholder="task"
+ onChange={handleChange}
+ value={notes[app.selectedNote].task || ""}
+ contentEditable={false}
+ readOnly={readOnly}
+ >
+
+
+
+ {
+ if (e.code === "Enter") {
+ addToast('added a description')
+ }
+ }}
+ type="text"
+ name="description"
+ placeholder="description"
+ onChange={handleChange}
+ value={notes[app.selectedNote].description || ""}
+ contentEditable={false}
+ readOnly={readOnly}
+ >
+
+
+
+ {
+ if (e.code === "Enter") {
+ addToast('added steps')
+ }
+ }}
+ type="text"
+ name="steps"
+ placeholder="steps"
+ onChange={handleChange}
+ value={notes[app.selectedNote].steps || ""}
+ contentEditable={false}
+ readOnly={readOnly}
+ >
+
+
+
+ {
+ if (e.code === "Enter") {
+ addToast('added a note')
+ }
+ }}
+ name="due"
+ onChange={handleChange}
+ placeholder="due"
+ value={notes[app.selectedNote].due}
+ readOnly={readOnly}
+ />
+
+
+
+ {
+ if (e.code === "Enter") {
+ addToast('added a watcher')
+ }
+ }}
+ type="text"
+ name="watcher"
+ placeholder="watcher"
+ onChange={handleChange}
+ value={notes[app.selectedNote].watcher || ""}
+ contentEditable={false}
+ readOnly={readOnly}
+ >
+
+
+
+ {
+ if (e.code === "Enter") {
+ addToast('tagged something')
+ }
+ }}
+ name="tag"
+ onChange={handleChange}
+ value={notes[app.selectedNote].tag || []}
+ readOnly={readOnly}
+ />
+
+
+
+ {
+ if (e.code === "Enter") {
+ addToast('added this task to a sprint')
+ }
+ }}
+ type="text"
+ name="sprint"
+ placeholder="sprint"
+ onChange={handleChange}
+ value={notes[app.selectedNote].sprint || ""}
+ contentEditable={false}
+ readOnly={readOnly}
+ >
+
+
+
+
+
+
+
+ {user.state === AuthState.SignedIn && }
+ >
+ )}
+
+ );
+};
+
+const Panel = styledComponents.div`
+ background-color: #FFFFFF;
+ height: 100vh;
+ flex: 3;
+ box-shadow: 0px 0px 8px 1px #dadada;
+ top: 0;
+ left: 0;
+ display: flex;
+ flex-direction: column;
+
+ @media only screen and (max-width: 768px) {
+ flex-direction: column;
+ height: 100vh;
+ width: 100vw;
+ text-align: left;
+ position: absolute;
+ top: 0;
+ left: 0;
+ transition: transform 0.3s ease-in-out;
+ transform: ${({ open }) => (open ? "translateX(0)" : "translateX(-100%)")};
+ z-index: 1;
+ box-shadow: unset;
+ }
+`;
+
+const DetailsForm = styledComponents.form`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ overflow: auto;
+ padding: 30px;
+ .ant-picker {
+ font-size: 12px;
+ width: 120px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ &:focus, &:hover {
+ border: 1px solid #d9d9d9;
+ }
+ }
+ @media only screen and (max-width: 768px) {
+ margin-top: 40px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ overflow: unset;
+ padding: unset;
+ & > input {
+ width: unset;
+ padding: 0 20px;
+ }
+ }
+ & > h2 > span {
+ cursor: pointer;
+ }
+ & > input {
+ border: 0.5px solid transparent;
+ border-radius: 4px;
+ padding: 4px 8px;
+ width: 100%;
+ font-weight: 600;
+ font-size: 24px;
+ transition: border 0.3s, box-shadow 0.3s;
+ &:hover {
+ border: 0.5px solid #9198a1;
+ }
+ &:focus {
+ border: 0.5px solid #6F7782;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+ }
+ }
+ @media only screen and (max-width: 768px) {
+ & > input {
+ width: auto;
+ margin: 0 20px;
+ }
+ }
+ & > div {
+ display: flex;
+ width: 100%;
+ flex-direction: row;
+ align-items: center;
+ gap: 20px;
+ & > label {
+ color: #6F7782;
+ margin-bottom: 0;
+ width: 150px;
+ font-size: 12px;
+ }
+ & > input {
+ border: 0.5px solid transparent;
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 12px;
+ transition: border 0.3s, box-shadow 0.3s;
+ &:hover {
+ border: 0.5px solid #9198a1;
+ }
+ &:focus {
+ border: 0.5px solid #6F7782;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+ }
+ }
+ }
+
+ @media only screen and (max-width: 768px) {
+ & > div {
+ align-items: initial;
+ width: auto;
+ flex-direction: column;
+ padding: 0 20px;
+ gap: unset;
+ & > label {
+ color: #6F7782;
+ margin-bottom: 0;
+ font-size: 14px;
+ width: auto;
+ font-weight: 600;
+ }
+ & > input {
+ border: 0.5px solid transparent;
+ border-radius: 4px;
+ padding: 14px 10px;
+ font-size: 14px;
+ transition: border 0.3s, box-shadow 0.3s;
+ border: 0.5px solid #6F7782;
+ margin-top: 5px;
+ &:hover {
+ border: 0.5px solid #9198a1;
+ }
+ &:focus {
+ border: 0.5px solid #6F7782;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+ }
+ }
+ }
+ }
+ & > input[type="submit"] {
+ display: none;
+ }
+`;
+
+const Button = styledComponents.button`
+background: none;
+border: none;
+font-size: 14px;
+font-weight: 600;
+position: absolute;
+top: 4%;
+right: 15px;
+`;
+
+export default connect((state) => ({
+ user: state.user,
+ notes: state.notes,
+ app: state.app,
+ comments: state.comments,
+ users: state.users,
+}))(SidePanel);
diff --git a/Tag.js b/Tag.js
new file mode 100644
index 00000000..3024ecfc
--- /dev/null
+++ b/Tag.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import styledComponents from "styled-components"
+import { useToasts } from 'react-toast-notifications'
+
+export const Tag = (props) => {
+ const { readOnly } = props
+ const { addToast } = useToasts();
+ const handleTagClick = (e) => {
+ const tag = e.target.innerText;
+ }
+ return (
+
+ {props.value.map(x => (
+
+ {x}
+ {
+ const tagsSet = new Set(props.value || [])
+ tagsSet.delete(x)
+ props.onChange({ target: {
+ value: Array.from(tagsSet),
+ name: props.name
+ }})
+ }}>
+ ×
+
+
+ ))}
+ {!readOnly &&
+ {
+ if (e.code === "Enter") {
+ const tagsSet = new Set(props.value || [])
+ tagsSet.add(e.target.value)
+ e.target.value = ""
+ props.onChange({ target: {
+ value: Array.from(tagsSet),
+ name: props.name
+ }}), addToast(`tagged something`, {appereance: 'info'})
+ }
+ }} />
+ }
+
+ )
+}
+
+const TagContainer = styledComponents.div`
+ display: block;
+ width: 50%;
+`
+const TagItem = styledComponents.span`
+ padding: 4px 8px;
+ gap: 5px;
+ border-radius: 4px;
+ width: fit-content;
+ height: fit-content;
+ font-size: 12px;
+ border: 1px solid #d9d9d9;
+ background-color: #fafafa;
+ margin: 2px;
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ & > span {
+ cursor: pointer;
+ &:nth-child(1) {
+ text-decoration: underline;
+ }
+ &:nth-child(2) {
+ border-radius: 100%;
+ height: 12px;
+ width: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ &:hover {
+ background-color: grey;
+ color: #FFFFFF;
+ }
+ }
+ }
+`
+
+const TagInput = styledComponents.span`
+ padding: 4px 8px;
+ border-radius: 4px;
+ width: calc(50px - 16px);
+ height: fit-content;
+ font-size: 12px;
+ border: 1px solid #d9d9d9;
+ background-color: #fafafa;
+ margin: 2px;
+ & > input {
+ border: none;
+ width: 50px;
+ background-color: #fafafa;
+ }
+`
diff --git a/index.css b/index.css
new file mode 100644
index 00000000..b08897af
--- /dev/null
+++ b/index.css
@@ -0,0 +1,16 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
+.css-1rjh2i2-Icon{
+ background-color: #fff !important;
+}
\ No newline at end of file