diff --git a/src/api/item.js b/src/api/item.js
index 60dd9a91b..fc2b693df 100644
--- a/src/api/item.js
+++ b/src/api/item.js
@@ -1,6 +1,15 @@
import { API_HOST } from '../config/constants';
import { DEFAULT_DELETE, DEFAULT_GET, DEFAULT_POST } from './utils';
+// payload = {id}
+export const getItem = async (id) => {
+ const req = await fetch(`${API_HOST}/items/${id}`, {
+ ...DEFAULT_GET,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ return req.json();
+};
+
// payload = {email}
export const getOwnItems = async () => {
const req = await fetch(`${API_HOST}/items/own`, {
@@ -38,4 +47,19 @@ export const deleteItem = async (id) => {
};
// we need this function for navigation purposes: when you click on an item, you want to see its 'immediate' children
-export const fetchItemImmediateChildren = () => {};
+export const getChildren = async (id) => {
+ const req = await fetch(`${API_HOST}/items/${id}/children`, DEFAULT_GET);
+ return req.json();
+};
+
+export const getItemTree = async (ownedItems) => {
+ // todo: use parallel promises
+ const items = JSON.parse(JSON.stringify(ownedItems));
+ // eslint-disable-next-line no-restricted-syntax
+ for (const item of items) {
+ // eslint-disable-next-line no-await-in-loop
+ const children = await getChildren(item.id);
+ item.children = children;
+ }
+ return items;
+};
diff --git a/src/components/App.js b/src/components/App.js
index 8b7209937..5cc06f2df 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -10,7 +10,12 @@ import Header from './layout/Header';
import Items from './main/Items';
import items from '../data/sample';
import SignUp from './SignUp';
-import { SIGN_UP_PATH, SIGN_IN_PATH } from '../config/paths';
+import {
+ SIGN_UP_PATH,
+ SIGN_IN_PATH,
+ HOME_PATH,
+ ITEMS_PATH,
+} from '../config/paths';
import SignIn from './SignIn';
import { ItemProvider } from './context/item';
@@ -29,7 +34,7 @@ function App() {
-
+
@@ -41,10 +46,10 @@ function App() {
-
-
+
+
-
+
diff --git a/src/components/context/item.js b/src/components/context/item.js
index c2f643de3..f26d131c7 100644
--- a/src/components/context/item.js
+++ b/src/components/context/item.js
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { getOwnItems, deleteItem } from '../../api/item';
+import { getOwnItems, deleteItem, getChildren, getItem } from '../../api/item';
import sampleItems from '../../data/sample';
const ItemContext = React.createContext();
@@ -42,12 +42,36 @@ class ItemProvider extends Component {
});
};
+ getChildren = async (id) => {
+ if (!id) {
+ return getOwnItems();
+ }
+ return getChildren(id);
+ };
+
+ getNavigation = async (itemId) => {
+ if (!itemId) {
+ return [];
+ }
+ const navigation = [];
+ let currentParentId = itemId;
+ while (currentParentId) {
+ // eslint-disable-next-line no-await-in-loop
+ const parent = await getItem(currentParentId);
+ navigation.push(parent);
+ currentParentId = parent.parentId;
+ }
+ return navigation;
+ };
+
buildValue = () => {
const { items } = this.state;
return {
items,
addItem: this.addItem,
deleteItem: this.deleteItem,
+ getChildren: this.getChildren,
+ getNavigation: this.getNavigation,
};
};
diff --git a/src/components/layout/Navigation.js b/src/components/layout/Navigation.js
new file mode 100644
index 000000000..38362d89a
--- /dev/null
+++ b/src/components/layout/Navigation.js
@@ -0,0 +1,92 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withRouter } from 'react-router';
+import Breadcrumbs from '@material-ui/core/Breadcrumbs';
+import Link from '@material-ui/core/Link';
+import { HOME_PATH } from '../../config/paths';
+import { ItemContext } from '../context/item';
+
+function handleClick(event) {
+ event.preventDefault();
+}
+
+class Navigation extends Component {
+ static contextType = ItemContext;
+
+ static propTypes = {
+ history: PropTypes.shape({
+ push: PropTypes.func.isRequired,
+ }).isRequired,
+ match: PropTypes.shape({
+ params: PropTypes.shape({
+ itemId: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+ };
+
+ state = {
+ navigation: [],
+ };
+
+ async componentDidMount() {
+ this.updateNavigation();
+ }
+
+ async componentDidUpdate({
+ match: {
+ params: { itemId: prevItemId },
+ },
+ }) {
+ const {
+ match: {
+ params: { itemId },
+ },
+ } = this.props;
+
+ if (prevItemId !== itemId) {
+ this.updateNavigation();
+ }
+ }
+
+ updateNavigation = async () => {
+ const { getNavigation } = this.context;
+ const {
+ match: {
+ params: { itemId },
+ },
+ } = this.props;
+
+ this.setState({ navigation: await getNavigation(itemId) });
+ };
+
+ goHome = (event) => {
+ const {
+ history: { push },
+ } = this.props;
+ event.preventDefault();
+ push(HOME_PATH);
+ };
+
+ render() {
+ const { navigation } = this.state;
+ return (
+
+
+ Owned Items
+
+ {navigation.map(({ name, id }) => (
+
+ {name}
+
+ ))}
+
+ );
+ }
+}
+
+export default withRouter(Navigation);
diff --git a/src/components/main/CreateNewItem.js b/src/components/main/CreateNewItem.js
index 0663f201e..40a51037f 100644
--- a/src/components/main/CreateNewItem.js
+++ b/src/components/main/CreateNewItem.js
@@ -2,6 +2,7 @@ import React, { useState, useContext } from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
+import { withRouter } from 'react-router';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
@@ -26,7 +27,13 @@ const useStyles = makeStyles((theme) => ({
},
}));
-const CreateNewItem = ({ open, handleClose }) => {
+const CreateNewItem = ({
+ open,
+ handleClose,
+ match: {
+ params: { itemId },
+ },
+}) => {
const classes = useStyles();
const itemContext = useContext(ItemContext);
const [itemName, setItemName] = useState('');
@@ -52,7 +59,9 @@ const CreateNewItem = ({ open, handleClose }) => {
const submitNewItem = async () => {
const { addItem } = itemContext;
+
const newItem = await createItem({
+ parentId: itemId,
name: itemName,
type: itemType,
description: itemDescription,
@@ -144,10 +153,16 @@ const CreateNewItem = ({ open, handleClose }) => {
CreateNewItem.propTypes = {
open: PropTypes.bool,
handleClose: PropTypes.func.isRequired,
+ match: PropTypes.shape({
+ params: PropTypes.shape({
+ itemId: PropTypes.string,
+ }).isRequired,
+ }),
};
CreateNewItem.defaultProps = {
open: false,
+ match: { params: { itemId: '' } },
};
-export default CreateNewItem;
+export default withRouter(CreateNewItem);
diff --git a/src/components/main/CustomCardHeader.js b/src/components/main/CustomCardHeader.js
index fad354ee3..79481c4f4 100644
--- a/src/components/main/CustomCardHeader.js
+++ b/src/components/main/CustomCardHeader.js
@@ -4,8 +4,7 @@ import { Link } from 'react-router-dom';
import { makeStyles } from '@material-ui/core/styles';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
-import IconButton from '@material-ui/core/IconButton';
-import MoreVertIcon from '@material-ui/icons/MoreVert';
+import ItemMenu from './ItemMenu';
const useStyles = makeStyles((theme) => ({
root: {
@@ -50,9 +49,7 @@ const CustomCardHeader = ({ id, creator, title, type }) => {
-
-
-
+
);
};
diff --git a/src/components/main/ItemMenu.js b/src/components/main/ItemMenu.js
new file mode 100644
index 000000000..bcb89b999
--- /dev/null
+++ b/src/components/main/ItemMenu.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import Menu from '@material-ui/core/Menu';
+import MenuItem from '@material-ui/core/MenuItem';
+import IconButton from '@material-ui/core/IconButton';
+import MoreVertIcon from '@material-ui/icons/MoreVert';
+import MoveItemModal from './MoveItemModal';
+
+const ItemMenu = () => {
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const [isMoveModalOpen, setIsMoveModalOpen] = React.useState(false);
+
+ const handleClick = (event) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const handleMove = () => {
+ setIsMoveModalOpen(true);
+ handleClose();
+ };
+
+ const onModalClose = () => {
+ setIsMoveModalOpen(false);
+ };
+
+ // todo: only display one modal for the whole page
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default ItemMenu;
diff --git a/src/components/main/Items.js b/src/components/main/Items.js
index df3100482..332a4e63c 100644
--- a/src/components/main/Items.js
+++ b/src/components/main/Items.js
@@ -1,5 +1,8 @@
import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withRouter } from 'react-router';
import Grid from '@material-ui/core/Grid';
+import Typography from '@material-ui/core/Typography';
import ItemsHeader from './ItemsHeader';
import CreateNewItemButton from './CreateNewItemButton';
import Item from './Item';
@@ -8,22 +11,82 @@ import { ItemContext } from '../context/item';
class Items extends Component {
static contextType = ItemContext;
+ static propTypes = {
+ match: PropTypes.shape({
+ params: PropTypes.shape({
+ itemId: PropTypes.string,
+ }).isRequired,
+ }),
+ };
+
+ static defaultProps = {
+ match: { params: { itemId: '' } },
+ };
+
+ state = { items: [] };
+
+ async componentDidMount() {
+ this.updateItems();
+ }
+
+ async componentDidUpdate({
+ match: {
+ params: { itemId: prevItemId },
+ },
+ }) {
+ const {
+ match: {
+ params: { itemId },
+ },
+ } = this.props;
+
+ if (prevItemId !== itemId) {
+ this.updateItems();
+ }
+ }
+
+ updateItems = async () => {
+ const {
+ match: {
+ params: { itemId },
+ },
+ } = this.props;
+
+ const { getChildren } = this.context;
+ return this.setState({
+ items: await getChildren(itemId),
+ });
+ };
+
+ renderItems = () => {
+ const { items } = this.state;
+
+ if (!items.length) {
+ return (
+
+ No Item Here
+
+ );
+ }
+
+ return items.reverse().map((item) => (
+
+
+
+ ));
+ };
+
render() {
- const { items } = this.context;
return (
- {items.reverse().map((item) => (
-
-
-
- ))}
+ {this.renderItems()}
);
}
}
-export default Items;
+export default withRouter(Items);
diff --git a/src/components/main/ItemsHeader.js b/src/components/main/ItemsHeader.js
index a05ae99cd..22479f548 100644
--- a/src/components/main/ItemsHeader.js
+++ b/src/components/main/ItemsHeader.js
@@ -1,8 +1,8 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
-import Typography from '@material-ui/core/Typography';
import Tooltip from '@material-ui/core/Tooltip';
import Info from '@material-ui/icons/Info';
+import Navigation from '../layout/Navigation';
const useStyles = makeStyles((theme) => ({
root: {
@@ -17,7 +17,7 @@ const ItemsHeader = () => {
const classes = useStyles();
return (
- Items
+
diff --git a/src/components/main/MoveItemModal.js b/src/components/main/MoveItemModal.js
new file mode 100644
index 000000000..681851515
--- /dev/null
+++ b/src/components/main/MoveItemModal.js
@@ -0,0 +1,96 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from '@material-ui/core/styles';
+import TreeView from '@material-ui/lab/TreeView';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import TreeItem from '@material-ui/lab/TreeItem';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import Dialog from '@material-ui/core/Dialog';
+import { Button } from '@material-ui/core';
+import { ItemContext } from '../context/item';
+import { getItemTree } from '../../api/item';
+
+const styles = () => ({
+ root: {
+ height: 240,
+ flexGrow: 1,
+ maxWidth: 400,
+ },
+});
+
+class MoveItemModal extends Component {
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ open: PropTypes.bool.isRequired,
+ classes: PropTypes.shape({
+ root: PropTypes.string.isRequired,
+ }).isRequired,
+ };
+
+ static contextType = ItemContext;
+
+ state = {
+ items: [],
+ };
+
+ async componentDidMount() {
+ const { items } = this.context;
+ const tree = await getItemTree(items);
+ this.setState({ items: tree });
+ }
+
+ handleClose = () => {
+ const { onClose } = this.props;
+ onClose();
+ };
+
+ onConfirm = () => {
+ const { onClose } = this.props;
+ // eslint-disable-next-line no-console
+ console.log('I choosed');
+ onClose();
+ };
+
+ onSelect = (e, value) => {
+ // eslint-disable-next-line no-console
+ console.log(value);
+ };
+
+ renderItemTreeItem = (items) => {
+ return items?.map(({ id, name, children }) => (
+
+ {this.renderItemTreeItem(children)}
+
+ ));
+ };
+
+ render() {
+ const { items } = this.state;
+ const { open, classes } = this.props;
+ return (
+
+ );
+ }
+}
+
+export default withStyles(styles)(MoveItemModal);
diff --git a/src/config/paths.js b/src/config/paths.js
index 6ebf8ea1d..c76327767 100644
--- a/src/config/paths.js
+++ b/src/config/paths.js
@@ -1,2 +1,4 @@
+export const HOME_PATH = '/';
export const SIGN_IN_PATH = '/signIn';
export const SIGN_UP_PATH = '/signUp';
+export const ITEMS_PATH = '/items';