nicholas/storage-manager#1: new directory structure, rewrite add box, item functionality
This commit is contained in:
@@ -1 +1,3 @@
|
||||
/.vs
|
||||
/.vscode
|
||||
.env
|
||||
+23
-5
@@ -1,4 +1,22 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
@@ -17,7 +35,7 @@
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.local
|
||||
dist
|
||||
dist-ssr
|
||||
*.env
|
||||
@@ -0,0 +1,12 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
@@ -0,0 +1,33 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -12,6 +12,7 @@
|
||||
<body class="scroll-container">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Generated
+2553
-13615
File diff suppressed because it is too large
Load Diff
+21
-35
@@ -1,43 +1,29 @@
|
||||
{
|
||||
"name": "storage",
|
||||
"version": "0.1.0",
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^14.2.1",
|
||||
"axios": "^0.27.2",
|
||||
"body-parser": "^1.20.0",
|
||||
"color": "^4.2.3",
|
||||
"express": "^4.18.1",
|
||||
"mongoose": "^6.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"server": "nodemon ../server/server.js",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"color": "^5.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.16"
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const SERVER_IP_ADDRESS = 'localhost';
|
||||
const SERVER_PORT = '5000';
|
||||
const PROTOCOL = 'http';
|
||||
const databaseName = 'box';
|
||||
const baseApiUrl = `${PROTOCOL}://${SERVER_IP_ADDRESS}:${SERVER_PORT}/${databaseName}`;
|
||||
|
||||
export default {
|
||||
baseApiUrl
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const config = {
|
||||
SERVER_IP_ADDRESS: import.meta.env.VITE_API_HOST || 'localhost',
|
||||
SERVER_PORT: import.meta.env.VITE_API_PORT || '5000',
|
||||
PROTOCOL: import.meta.env.VITE_API_PROTOCOL || 'http',
|
||||
};
|
||||
const baseApiUrl = `${config.PROTOCOL}://${config.SERVER_IP_ADDRESS}:${config.SERVER_PORT}`;
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: baseApiUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
console.error('API error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { api } from './api';
|
||||
export * from './apiResponse.types';
|
||||
@@ -1,465 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import Box from './box';
|
||||
import api from './api';
|
||||
import Modal from './modal';
|
||||
import Button from './button';
|
||||
import InputBox from './inputBox';
|
||||
import TextArea from './textArea';
|
||||
import ColorSelect from './color';
|
||||
import Color from 'color';
|
||||
import './styles/app.css';
|
||||
|
||||
function App() {
|
||||
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
const [boxID, setBoxID] = useState(undefined);
|
||||
const [boxName, setBoxName] = useState('');
|
||||
const [boxDescription, setBoxDescription] = useState('');
|
||||
const [boxColor, setBoxColor] = useState('WHITE');
|
||||
|
||||
const [showModalBox, setShowModalBox] = useState(false);
|
||||
const [showModalItem, setShowModalItem] = useState(false);
|
||||
const [showModalItemReadOnly, setShowModalItemReadOnly] = useState(false);
|
||||
|
||||
const [editBoxBool, setEditBoxBool] = useState(false);
|
||||
const [editItemBool, setEditItemBool] = useState(false);
|
||||
|
||||
const [itemID, setItemID] = useState(undefined);
|
||||
const [itemName, setItemName] = useState('');
|
||||
const [itemDescription, setItemDescription] = useState('');
|
||||
const [itemQuantity, setItemQuantity] = useState(1);
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
const [filteredData, setFilteredData] = useState([]);
|
||||
|
||||
const [dragSource, setDragSource] = useState({});
|
||||
const [dragSourceElement, setDragSourceElement] = useState(undefined);
|
||||
|
||||
const [isItemMoving, setIsItemMoving] = useState(false);
|
||||
const [moveItemID, setMoveItemID] = useState(null);
|
||||
const [moveItemBoxID, setMoveItemBoxID] = useState(null)
|
||||
|
||||
//fetch all box data
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
await fetchDataAsync();
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
//set filtered data on filter or original data change
|
||||
useEffect(() => {
|
||||
const finalFilter = filter.trim().toLowerCase();
|
||||
const filtered = data.filter((box) =>
|
||||
box.name.toLowerCase().includes(finalFilter) ||
|
||||
box.description.toLowerCase().includes(finalFilter) ||
|
||||
box.item.some(item =>
|
||||
(item.name && item.name.toLowerCase().includes(finalFilter)) ||
|
||||
(item.description && item.description.toLowerCase().includes(finalFilter))
|
||||
)
|
||||
)
|
||||
setFilteredData(filtered);
|
||||
}, [filter, data]);
|
||||
|
||||
const fetchDataAsync = async () => {
|
||||
try {
|
||||
const resp = await axios.get(api.baseApiUrl);
|
||||
setData(resp.data);
|
||||
setFilteredData(resp.data);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
// BOX FUNCTIONS ///////////////////////////////////////////////////////////////
|
||||
const createBox = async (boxData) => {
|
||||
try {
|
||||
const response = await axios.post(api.baseApiUrl, boxData);
|
||||
setData(response.data);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
const editBox = async (boxData) => {
|
||||
try {
|
||||
const response = await axios.put(`${api.baseApiUrl}/${boxData.id}`, boxData);
|
||||
setData(response.data);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const removeBox = async (id) => {
|
||||
try {
|
||||
const response = await axios.delete(`${api.baseApiUrl}/${id}`);
|
||||
const storageData = response.data;
|
||||
setData(storageData);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const sortBox = async (target, source) => {
|
||||
try {
|
||||
const response = await axios.put(`${api.baseApiUrl}/${source._id}/sort`, target);
|
||||
setData(response.data);
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
const handleNameBox = (e) => {
|
||||
setBoxName(e.target.value);
|
||||
}
|
||||
|
||||
const handleBoxDescription = (e) => {
|
||||
setBoxDescription(e.target.value);
|
||||
}
|
||||
|
||||
const handleColorBox = (event) => {
|
||||
setBoxColor(event.target.value);
|
||||
}
|
||||
|
||||
const handleSubmitBox = (event) => {
|
||||
event.preventDefault();
|
||||
setShowModalBox(false);
|
||||
setEditBoxBool(false);
|
||||
const boxData = {
|
||||
id: boxID,
|
||||
name: boxName,
|
||||
description: boxDescription,
|
||||
color: boxColor
|
||||
};
|
||||
if (editBoxBool)
|
||||
editBox(boxData);
|
||||
else
|
||||
createBox(boxData);
|
||||
|
||||
clearFormBox();
|
||||
return false;
|
||||
}
|
||||
|
||||
const handleRemoveBox = (id) => {
|
||||
removeBox(id);
|
||||
}
|
||||
const handleAddBox = () => {
|
||||
clearFormBox();
|
||||
setShowModalBox(true);
|
||||
setEditBoxBool(false);
|
||||
}
|
||||
const handleEditBox = (id) => {
|
||||
const box = data.find(box => box._id === id);
|
||||
setEditBoxBool(true);
|
||||
setBoxName(box.name);
|
||||
setBoxDescription(box.description);
|
||||
setBoxColor(box.color);
|
||||
setBoxID(id);
|
||||
setShowModalBox(true);
|
||||
}
|
||||
const clearFormBox = () => {
|
||||
setBoxName('');
|
||||
setBoxDescription('');
|
||||
setBoxID(undefined);
|
||||
}
|
||||
|
||||
const handleDragStartBox = (dragSource, event) => {
|
||||
setDragSource(dragSource);
|
||||
setDragSourceElement(event.currentTarget);
|
||||
event.currentTarget.style.opacity = 0.3;
|
||||
|
||||
var rect = event.currentTarget.getBoundingClientRect();
|
||||
var x = event.clientX - rect.left;
|
||||
var y = event.clientY - rect.top;
|
||||
|
||||
event.dataTransfer.setDragImage(event.currentTarget,x,y);
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
const handleDragOverBox = (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
const handleDropBox = async (dragTarget, event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const sourceElement = dragSourceElement;
|
||||
const targetElement = event.currentTarget;
|
||||
sourceElement.style.opacity = 1;
|
||||
|
||||
const elements = [...event.currentTarget.parentElement.children];
|
||||
const targetIndex = elements.indexOf(targetElement);
|
||||
const sourceIndex = elements.indexOf(sourceElement);
|
||||
if (dragTarget._id !== dragSource._id) {
|
||||
|
||||
const target = Object.assign(dragTarget, { sort: targetIndex });
|
||||
const source = Object.assign(dragSource, { sort: sourceIndex });
|
||||
await sortBox(target, source);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const handleSelectMoveBox = async (boxData, event) => {
|
||||
setMoveItemBoxID(null);
|
||||
setMoveItemID(null);
|
||||
setIsItemMoving(false);
|
||||
await moveItem(moveItemID, boxData._id);
|
||||
|
||||
}
|
||||
const boxHandler = {
|
||||
handleSubmit: handleSubmitBox,
|
||||
handleRemove: handleRemoveBox,
|
||||
handleName: handleNameBox,
|
||||
handleDescription: handleBoxDescription,
|
||||
handleColor: handleColorBox,
|
||||
handleEdit: handleEditBox,
|
||||
handleDragStart: handleDragStartBox,
|
||||
handleDragOver: handleDragOverBox,
|
||||
handleDrop: handleDropBox,
|
||||
handleSelectMove: handleSelectMoveBox,
|
||||
};
|
||||
|
||||
const handleModalCloseBox = () => {
|
||||
setShowModalBox(false);
|
||||
}
|
||||
// ITEM FUNCTIONS ///////////////////////////////////////////////////////////////
|
||||
const moveItem = async (itemID, boxIDTarget) => {
|
||||
try {
|
||||
const response = await axios.post(`${api.baseApiUrl}/item/${itemID}/${boxIDTarget}`);
|
||||
setData(response.data);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const createItem = async (boxID, itemData) => {
|
||||
try {
|
||||
const response = await axios.post(`${api.baseApiUrl}/${boxID}/item`, itemData);
|
||||
const boxData = [...data];
|
||||
boxData.splice(boxData.findIndex(box => box._id === boxID), 1, response.data);
|
||||
setData(boxData);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const editItem = async (boxID, itemID, itemData) => {
|
||||
try {
|
||||
const response = await axios.put(`${api.baseApiUrl}/${boxID}/item/${itemID}`, itemData);
|
||||
const boxData = [...data];
|
||||
boxData.splice(boxData.findIndex(box => box._id === boxID), 1, response.data);
|
||||
setData(boxData);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const removeItem = async (boxID, itemID) => {
|
||||
try {
|
||||
const boxData = [...data];
|
||||
const response = await axios.delete(`${api.baseApiUrl}/${boxID}/item/${itemID}`);
|
||||
boxData.splice(boxData.findIndex(box => box._id === boxID), 1, response.data);
|
||||
setData(boxData);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddItem = (id) => {
|
||||
clearFormItem();
|
||||
setBoxID(id);
|
||||
setEditItemBool(false);
|
||||
setShowModalItem(true);
|
||||
}
|
||||
const setItemInfo = (boxID, itemID) => {
|
||||
const box = data.find(box => box._id === boxID);
|
||||
const item = box.item.find(item => item._id === itemID);
|
||||
setBoxID(boxID);
|
||||
setItemID(itemID);
|
||||
setItemName(item.name || '');
|
||||
setItemDescription(item.description || '');
|
||||
setItemQuantity(item.quantity || 1);
|
||||
}
|
||||
const handleEditItem = (boxID, itemID) => {
|
||||
clearFormItem();
|
||||
setItemInfo(boxID, itemID);
|
||||
setEditItemBool(true);
|
||||
setShowModalItem(true);
|
||||
}
|
||||
const handleMoveItem = (boxID, itemID) => {
|
||||
setMoveItemBoxID(boxID);
|
||||
setMoveItemID(itemID);
|
||||
setIsItemMoving(true);
|
||||
/*highlight boxes available*/
|
||||
/*...*/
|
||||
}
|
||||
const handleViewItem = (boxID, itemID) => {
|
||||
setItemInfo(boxID, itemID);
|
||||
setShowModalItemReadOnly(true);
|
||||
}
|
||||
const handleRemoveItem = (boxID, itemID) => {
|
||||
removeItem(boxID, itemID);
|
||||
}
|
||||
const handleSubmitItem = (event) => {
|
||||
event.preventDefault();
|
||||
setShowModalItem(false);
|
||||
const itemData = {
|
||||
id: itemID,
|
||||
name: itemName,
|
||||
description: itemDescription,
|
||||
quantity: itemQuantity
|
||||
}
|
||||
if (editItemBool)
|
||||
editItem(boxID, itemID, itemData);
|
||||
|
||||
else
|
||||
createItem(boxID, itemData);
|
||||
|
||||
clearFormItem();
|
||||
return false;
|
||||
}
|
||||
|
||||
const handleModalCloseItem = () => {
|
||||
setShowModalItem(false);
|
||||
setShowModalItemReadOnly(false);
|
||||
}
|
||||
const clearFormItem = () => {
|
||||
setItemID(undefined);
|
||||
setItemName('');
|
||||
setItemDescription('');
|
||||
setItemQuantity(1);
|
||||
}
|
||||
const handleNameItem = (e) => {
|
||||
setItemName(e.target.value)
|
||||
}
|
||||
const handleDescriptionItem = (e) => {
|
||||
setItemDescription(e.target.value)
|
||||
}
|
||||
const handleQuantityItem = (e) => {
|
||||
const num = parseInt(e.target.value);
|
||||
if (Number.isInteger(num)) {
|
||||
setItemQuantity(num);
|
||||
}
|
||||
if (e.target.value === '')
|
||||
setItemQuantity('');
|
||||
}
|
||||
const itemHandler = {
|
||||
handleSubmit: handleSubmitItem,
|
||||
handleRemove: handleRemoveItem,
|
||||
handleAdd: handleAddItem,
|
||||
handleEdit: handleEditItem,
|
||||
handleMove: handleMoveItem,
|
||||
handleView: handleViewItem,
|
||||
handleName: handleNameItem,
|
||||
handleDescription: handleDescriptionItem,
|
||||
handleQuantity: handleQuantityItem
|
||||
};
|
||||
|
||||
const handleFilterChange = (event) => {
|
||||
const filterText = event.target.value;
|
||||
setFilter(filterText);
|
||||
}
|
||||
const handleClearFilter = () => {
|
||||
setFilter('');
|
||||
}
|
||||
const baseColors = {
|
||||
'WHITE': 'white',
|
||||
'RED': '#FF9AA2',
|
||||
'PINK': '#FFB7B2',
|
||||
'ORANGE': '#FFDAC1',
|
||||
'GREEN': '#E2F0CB',
|
||||
'EMERALD': '#B5EAD7',
|
||||
'PURPLE': '#C7CEEA'
|
||||
}
|
||||
const colorData = Object.entries(baseColors).map((color) => {
|
||||
|
||||
const data = {
|
||||
title: color[0],
|
||||
defaultStyle: { backgroundColor: color[1] },
|
||||
lightStyle: { backgroundColor: Color(color[1]).lighten(0.05).string() },
|
||||
darkStyle: { backgroundColor: Color(color[1]).darken(0.1).string() }
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
const colorSelector =
|
||||
<>
|
||||
<label>color</label>
|
||||
<div className='color-selector-container'>
|
||||
{colorData.map((data) => {
|
||||
return <ColorSelect colorData={data} handler={boxHandler.handleColor} boxColor={boxColor} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
|
||||
const boxForm =
|
||||
<form id='form'>
|
||||
<InputBox id='id' name='id' value={boxID} readOnly={true} hidden={true} />
|
||||
<h1>Edit Box</h1>
|
||||
<fieldset>
|
||||
<InputBox focus={true} id='box-name' label='name' onChange={boxHandler.handleName} value={boxName} name='name' type='text' />
|
||||
<InputBox id='box-description' label='description' onChange={boxHandler.handleDescription} value={boxDescription} name='description' type='text' />
|
||||
{colorSelector}
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
const itemForm = <form id='form'>
|
||||
<h1>Edit Item</h1>
|
||||
<fieldset>
|
||||
<InputBox id='id' name='id' value={itemID} readOnly={true} hidden={true} />
|
||||
<InputBox focus={true} id='item-name' label='name' onChange={itemHandler.handleName} value={itemName} name='name' type='text' />
|
||||
<TextArea id='item-description' label='description' onChange={itemHandler.handleDescription} value={itemDescription} name='description' />
|
||||
<InputBox id='item-quantity' label='quantity' onChange={itemHandler.handleQuantity} value={itemQuantity} name='quantity' type='text' />
|
||||
</fieldset>
|
||||
</form>
|
||||
//
|
||||
const itemView = <div>
|
||||
<h1>{itemName}</h1>
|
||||
<h2>{itemDescription}</h2>
|
||||
<h3>{itemQuantity}</h3>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<>
|
||||
{<Modal show={showModalItem} content={itemForm} handleSubmit={itemHandler.handleSubmit} handleClose={handleModalCloseItem} />}
|
||||
{<Modal show={showModalBox} content={boxForm} handleSubmit={boxHandler.handleSubmit} handleClose={handleModalCloseBox} />}
|
||||
{<Modal show={showModalItemReadOnly} content={itemView} handleClose={handleModalCloseItem} />}
|
||||
|
||||
<div className='header-container'>
|
||||
<span className='filter-container'>
|
||||
<span className='material-icons icon'>search</span>
|
||||
<input spellCheck='false' className='filter' type='search' id='box-filter' name='filter' value={filter} onChange={handleFilterChange} />
|
||||
<span className='material-icons icon clear' onClick={handleClearFilter}>clear</span>
|
||||
</span>
|
||||
{<Button label='new box' onClick={handleAddBox} />}
|
||||
</div>
|
||||
<div className='body-container scroll-container'>
|
||||
<ul className='list-box'>
|
||||
{filteredData.map((box, index) => {
|
||||
return (
|
||||
<Box
|
||||
style={colorData.find(color => color.title === box.color)}
|
||||
isItemMoving={box._id !== moveItemBoxID && isItemMoving}
|
||||
key={index}
|
||||
boxData={box}
|
||||
boxHandler={boxHandler}
|
||||
itemHandler={itemHandler}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getAllBoxes, Box, BoxCard, BoxFormModal } from './features/box';
|
||||
import './styles/app.css';
|
||||
import './styles/button.css';
|
||||
import './styles/inputBox.css';
|
||||
import './styles/textArea.css';
|
||||
import './index.css';
|
||||
|
||||
const App = () => {
|
||||
const [boxes, setBoxes] = useState<Box[]>([]);
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
|
||||
|
||||
const handleCreateBox = (newBox: Box) => {
|
||||
setBoxes((boxes) => [...boxes, newBox]);
|
||||
};
|
||||
|
||||
|
||||
const toggleModal = () => {
|
||||
setModalOpen(!isModalOpen);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const response = await getAllBoxes();
|
||||
if (response.success) {
|
||||
if (response.data != null) {
|
||||
setBoxes(response.data);
|
||||
} else {
|
||||
console.error('No data returned in the response');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BoxFormModal isOpen={isModalOpen} onCreateBox={handleCreateBox} onClose={handleClose} />
|
||||
<div className="filter-container">
|
||||
<span className='material-icons icon'>search</span>
|
||||
<input spellCheck='false' className='filter' type='search' id='box-filter' name='filter' />
|
||||
<span className='material-icons icon clear'>clear</span>
|
||||
</div>
|
||||
<div className='header-container'>
|
||||
<button className='button' onClick={toggleModal}>new box</button>
|
||||
</div>
|
||||
<div className='body-container scroll-container'>
|
||||
<ul className='list-box'>
|
||||
{boxes.map((item) => (
|
||||
<BoxCard key={item._id} box={item} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -1,76 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import Item from './item';
|
||||
|
||||
const Box = ({ boxData, boxHandler, itemHandler, isItemMoving, style }) => {
|
||||
const menu = useRef(null);
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick);
|
||||
};
|
||||
}, []);
|
||||
const handleClick = (event) => {
|
||||
if (menu.current.contains(event.target)) return;
|
||||
setShowMenu(false);
|
||||
}
|
||||
const handleClickRemove = () => {
|
||||
boxHandler.handleRemove(boxData._id);
|
||||
setShowMenu(false);
|
||||
}
|
||||
const handleClickEdit = () => {
|
||||
boxHandler.handleEdit(boxData._id);
|
||||
setShowMenu(false);
|
||||
}
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const handleBoxMenuClick = () => {
|
||||
setShowMenu(true);
|
||||
}
|
||||
const moreMenu = () => {
|
||||
return (
|
||||
<ul className='menu-more-list box'>
|
||||
<li className='menu-action' onClick={handleClickEdit}>
|
||||
<span className='material-icons icon'>edit</span>
|
||||
<span className='label'>edit</span>
|
||||
</li>
|
||||
<li className='menu-action' onClick={handleClickRemove}>
|
||||
<span className='material-icons icon'>delete</span>
|
||||
<span className='label'>remove</span>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<li className={`${isItemMoving ? 'target ' : ''}box`}
|
||||
style={style.defaultStyle}
|
||||
draggable='true'
|
||||
onDragStart={(event) => boxHandler.handleDragStart(boxData, event)}
|
||||
onDragOver={boxHandler.handleDragOver}
|
||||
onDrop={(event) => boxHandler.handleDrop(boxData, event)}
|
||||
onClick={(event) => isItemMoving && boxHandler.handleSelectMove(boxData, event)}>
|
||||
{false && <span>{boxData.sort}</span>}
|
||||
<div ref={menu} className='menu-more-container'>{showMenu && moreMenu()}</div>
|
||||
<div className='box-header'>
|
||||
<div className='box-title-container'>
|
||||
<h2 className='box-title'>{boxData.name}</h2>
|
||||
<h3 className='box-subtitle'>{boxData.description}</h3>
|
||||
</div>
|
||||
<span className='menu-btn box material-icons' onClick={handleBoxMenuClick}>more_vert</span>
|
||||
</div>
|
||||
<div style={style.darkStyle} className='head-divider'></div>
|
||||
<ul className={`list-box-item scroll-container ${boxData.item.length === 0 ? "empty": ""}`}>
|
||||
{boxData.item.map(((item, index) => {
|
||||
return <Item
|
||||
key={index}
|
||||
data={item}
|
||||
boxID={boxData._id}
|
||||
itemHandler={itemHandler} />
|
||||
}))}
|
||||
</ul>
|
||||
<div className='menu-bottom'>
|
||||
<span style={style.lightStyle} className='material-icons add' onClick={() => itemHandler.handleAdd(boxData._id)}>add</span>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default Box;
|
||||
@@ -1,8 +0,0 @@
|
||||
import React, {useState} from 'react';
|
||||
import './styles/button.css';
|
||||
|
||||
const Button = ({style, form, onClick, label, className}) => {
|
||||
return <button style={style} form={form} onClick={onClick} className={`button ${className}`}>{label}</button>
|
||||
}
|
||||
|
||||
export default Button;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const Color = ({ colorData, handler, boxColor }) => {
|
||||
const ColorSelect = ({ colorData, handler, boxColor }) => {
|
||||
const [style, setStyle] = useState(colorData.defaultStyle);
|
||||
|
||||
const handleHover = (isHover) => {
|
||||
@@ -14,4 +14,4 @@ const Color = ({ colorData, handler, boxColor }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Color;
|
||||
export default ColorSelect;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Item } from '../item/item.types';
|
||||
|
||||
export interface Box {
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
items: Item[];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from 'react';
|
||||
import { Box } from '../';
|
||||
import { Item, ItemCard, ItemFormModal } from '../../item';
|
||||
|
||||
interface BoxCardProps {
|
||||
box: Box;
|
||||
}
|
||||
|
||||
export const BoxCard: React.FC<BoxCardProps> = ({ box }) => {
|
||||
|
||||
const [items, setItems] = useState<Item[]>(box.items);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const handleOpenItemFormModal = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
const handleClose = () => {
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
const handleCreateItem = (newItem: Item) => {
|
||||
setItems((items) => [...items, newItem]);
|
||||
};
|
||||
|
||||
return (
|
||||
<li className="box">
|
||||
<ItemFormModal isOpen={modalOpen} onCreateItem={handleCreateItem} onClose={handleClose} boxId={box._id} />
|
||||
<div className='menu-more-container'></div>
|
||||
<div className="box-header">
|
||||
<div className="box-title-container">
|
||||
<h2 className="box-title">{box.name}</h2>
|
||||
<h3 className="box-subtitle">{box.description}</h3>
|
||||
</div>
|
||||
<span className='menu-btn box material-icons'>more_vert</span>
|
||||
</div>
|
||||
<div className="head-divider" />
|
||||
<ul className={`list-box-item scroll-container ${box.items?.length === 0 ? 'empty' : ''}`}>
|
||||
{items?.map((item) => (
|
||||
<ItemCard key={item._id} item={item}></ItemCard>
|
||||
))}
|
||||
</ul>
|
||||
<div className="menu-bottom">
|
||||
<span
|
||||
className="material-icons add"
|
||||
onClick={handleOpenItemFormModal}>
|
||||
add
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import Modal from '../../../modal';
|
||||
import { Box, createBox } from '..';
|
||||
import { Item } from '../../item';
|
||||
|
||||
interface BoxFormModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreateBox: (box: Box) => void;
|
||||
initialData?: Partial<Box>;
|
||||
|
||||
}
|
||||
export const BoxFormModal: React.FC<BoxFormModalProps> = ({ isOpen, onClose, onCreateBox, initialData }) => {
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const boxData: Box = {
|
||||
name: formData.get('name') as string,
|
||||
description: formData.get('description') as string,
|
||||
items: initialData?.items as Item[]
|
||||
};
|
||||
try {
|
||||
const response = await createBox(boxData);
|
||||
if (response.success) {
|
||||
if (response.data != null) {
|
||||
onCreateBox(response.data);
|
||||
} else {
|
||||
console.error('No data returned in the response');
|
||||
}
|
||||
} else {
|
||||
console.log(response.error || 'Unknown error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
finally {
|
||||
onClose();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}>
|
||||
<form onSubmit={handleSubmit} id='form'>
|
||||
<input value="" id='id' name='id' readOnly={true} hidden={true} />
|
||||
<h1>Edit Box</h1>
|
||||
<fieldset>
|
||||
<label className='input' htmlFor="box-name">name</label>
|
||||
<input className='input' id='box-name' name='name' type='text' />
|
||||
<label className='input' htmlFor="box-description">description</label>
|
||||
<input className='input' id='box-description' name='description' type='text' />
|
||||
<div className="modal-actions">
|
||||
<button className='button' type="submit">Create</button>
|
||||
<button className='button' type="button" onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './box.types';
|
||||
|
||||
export * from './components/BoxCard';
|
||||
export * from './components/BoxFormModal';
|
||||
export * from './services/BoxService';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { api, ApiResponse } from '../../../api';
|
||||
import { Box } from '..';
|
||||
|
||||
const getAllBoxes = async (): Promise<ApiResponse<Box[]>> => {
|
||||
try {
|
||||
const response = await api.get<Box[]>('/boxes');
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Could not fetch boxes';
|
||||
return { success: false, data: [], error: message };
|
||||
}
|
||||
};
|
||||
|
||||
const createBox = async (boxData: Box): Promise<ApiResponse<Box>> => {
|
||||
try {
|
||||
const response = await api.post<Box>(`/boxes`, boxData);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating box:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Could not create box',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { getAllBoxes, createBox }
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Item } from '../item.types';
|
||||
|
||||
interface ItemCardProps {
|
||||
item: Item;
|
||||
}
|
||||
|
||||
export const ItemCard: React.FC<ItemCardProps> = ({ item }) => {
|
||||
return (
|
||||
<li className='item'>
|
||||
<div className='item-menu-more-container'></div>
|
||||
<span className='item-name'>{item.name}</span>
|
||||
<span className='menu-btn item material-icons'>
|
||||
remove_red_eye
|
||||
</span>
|
||||
<span className='menu-btn item material-icons'>
|
||||
more_vert
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import Modal from '../../../modal';
|
||||
import { Item, createItem } from '..';
|
||||
|
||||
interface ItemFormModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreateItem: (item: Item) => void;
|
||||
boxId?: string;
|
||||
initialData?: Partial<Item>;
|
||||
}
|
||||
export const ItemFormModal: React.FC<ItemFormModalProps> = ({ isOpen, onClose, onCreateItem, boxId, initialData }) => {
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const itemData: Item = {
|
||||
name: formData.get('name') as string,
|
||||
description: formData.get('description') as string,
|
||||
quantity: parseInt(formData.get('quantity') as string, 10) as number,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await createItem(itemData, boxId);
|
||||
if (response.success) {
|
||||
onCreateItem(response.data as Item);
|
||||
} else {
|
||||
console.error(response.error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
finally {
|
||||
onClose();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}>
|
||||
<form onSubmit={handleSubmit} id='form'>
|
||||
<input value="" id='id' name='id' readOnly={true} hidden={true} />
|
||||
<h1>Edit Item</h1>
|
||||
<fieldset>
|
||||
|
||||
<label className='input' htmlFor="item-name">name</label>
|
||||
<input className='input' id='item-name' name='name' type='text' />
|
||||
|
||||
<label className='input' htmlFor="item-description">description</label>
|
||||
<input className='input' id='item-description' name='description' type='text' />
|
||||
|
||||
<label className='input' htmlFor="item-quantity">quantity</label>
|
||||
<input className='input' id='item-quantity' name='quantity' type='number' />
|
||||
|
||||
<div className="modal-actions">
|
||||
<button className='button' type="submit">Create</button>
|
||||
<button className='button' type="button" onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './item.types';
|
||||
|
||||
export * from './components/ItemCard';
|
||||
export * from './components/ItemFormModal';
|
||||
export * from './services/ItemService';
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface Item {
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { api, ApiResponse } from '../../../api';
|
||||
import { Item } from '..';
|
||||
|
||||
const createItem = async (itemData: Item, boxId?: string,): Promise<ApiResponse<Item | null>> => {
|
||||
try {
|
||||
const response = await api.post<Item>(`/boxes/${boxId}/items`, itemData);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating item:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: 'Could not create item',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { createItem }
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './app';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './app';
|
||||
import './index.css';
|
||||
|
||||
const rootElement = document.getElementById('root') as HTMLElement;
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -1,17 +0,0 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import './styles/inputBox.css';
|
||||
|
||||
const InputBox = ({ id, name, label, onChange, value, type, readOnly, hidden, focus}) => {
|
||||
const focusInput = useRef(null);
|
||||
useEffect(()=>{
|
||||
if(focus) focusInput.current.focus();
|
||||
},[])
|
||||
return (
|
||||
<>
|
||||
<label className='input' htmlFor={id}>{label}</label>
|
||||
<input ref={focusInput} className='input' readOnly={readOnly} hidden={hidden} onChange={onChange} value={value} id={id} name={name} type={type} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputBox;
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
const Item = ({ data, itemHandler, boxID }) => {
|
||||
const menu = useRef(null);
|
||||
const handleClick = (event) => {
|
||||
if (menu.current.contains(event.target))
|
||||
return;
|
||||
setShowMenu(false);
|
||||
}
|
||||
const handleClickEdit = () => {
|
||||
itemHandler.handleEdit(boxID,data._id);
|
||||
setShowMenu(false);
|
||||
}
|
||||
const handleClickMove = () => {
|
||||
itemHandler.handleMove(boxID,data._id);
|
||||
setShowMenu(false);
|
||||
}
|
||||
const handleClickRemove = () => {
|
||||
itemHandler.handleRemove(boxID, data._id);
|
||||
setShowMenu(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const handleItemMenuClick = () => {
|
||||
setShowMenu(true);
|
||||
}
|
||||
const handleItemViewClick = () => {
|
||||
itemHandler.handleView(boxID,data._id);
|
||||
}
|
||||
const moreMenu = () => {
|
||||
return (
|
||||
<ul className='menu-more-list item'>
|
||||
<li className='menu-action' onClick={handleClickEdit}>
|
||||
<span className='material-icons icon'>edit</span>
|
||||
<span className='label'>edit</span>
|
||||
</li>
|
||||
<li className='menu-action' onClick={handleClickMove}>
|
||||
<span className='material-icons icon'>open_with</span>
|
||||
<span className='label'>move</span>
|
||||
</li>
|
||||
<li className='menu-action' onClick={handleClickRemove}>
|
||||
<span className='material-icons icon'>delete</span>
|
||||
<span className='label'>remove</span>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<li className='item'>
|
||||
<div ref={menu} className='item-menu-more-container'>{showMenu && moreMenu()}</div>
|
||||
<span className='item-name'>{data.name}</span>
|
||||
<span className='menu-btn item material-icons' onClick={handleItemViewClick}>
|
||||
remove_red_eye
|
||||
</span>
|
||||
<span className='menu-btn item material-icons' onClick={handleItemMenuClick}>
|
||||
more_vert
|
||||
</span>
|
||||
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Item;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import Button from './button';
|
||||
import './styles/modal.css';
|
||||
import './styles/app.css';
|
||||
const confirmStyle = {
|
||||
backgroundColor: '#0495FC',
|
||||
color: 'white'
|
||||
}
|
||||
const Modal = ({ show, content, handleSubmit, handleClose }) => {
|
||||
const handleClickOff = (event) => {
|
||||
if (event.target.classList.contains('modal-container'))
|
||||
handleClose();
|
||||
}
|
||||
return show ?
|
||||
<div className='modal-container' onClick={handleClickOff}>
|
||||
<div className='modal-content'>
|
||||
<div className='modal-body'>
|
||||
{content}
|
||||
</div>
|
||||
<div className='modal-action'>
|
||||
<Button label='cancel' onClick={handleClose} className='btn btn-close' />
|
||||
{handleSubmit && <Button style={confirmStyle} form='id' label='submit' onClick={handleSubmit} className='btn btn-submit' />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: null;
|
||||
}
|
||||
|
||||
export default Modal;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ReactNode } from 'react';
|
||||
import './styles/modal.css';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ isOpen, children }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className='modal-container'>
|
||||
<div className='modal-content'>
|
||||
<div className='modal-body'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
+16
-15
@@ -1,26 +1,27 @@
|
||||
.modal-container {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.32)
|
||||
background-color: rgba(0, 0, 0, 0.32);
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: grid;
|
||||
padding: 0.8rem;
|
||||
grid-template-rows: 1fr auto;
|
||||
width: 50%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 11px 15px -7px
|
||||
rgba(0,0,0,.2),
|
||||
0 24px 38px 3px rgba(0,0,0,.14),
|
||||
0 9px 46px 8px rgba(0,0,0,.12);
|
||||
background: #fff;
|
||||
background-color: white;
|
||||
border-radius: 1em;
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.modal-action {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import './styles/textArea.css';
|
||||
|
||||
const TextArea = ({ id, name, label, onChange, value, readOnly, hidden}) => {
|
||||
return (
|
||||
<>
|
||||
<label className='textarea' htmlFor={id}>{label}</label>
|
||||
<textarea className='textarea' readOnly={readOnly} hidden={hidden} onChange={onChange} value={value} id={id} name={name} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextArea;
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
VITE_API_HOST: string;
|
||||
VITE_API_PORT: string;
|
||||
VITE_API_PROTOCOL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
mongo:
|
||||
container_name: mongo-db
|
||||
image: mongo:4.4.6
|
||||
networks:
|
||||
- mongo-network
|
||||
restart: always
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
|
||||
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
|
||||
ports:
|
||||
- 27017:27017
|
||||
|
||||
mongo-express:
|
||||
container_name: mongo-express
|
||||
image: mongo-express
|
||||
networks:
|
||||
- mongo-network
|
||||
restart: always
|
||||
ports:
|
||||
- 8999:8081
|
||||
environment:
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME: ${ME_CONFIG_MONGODB_ADMINUSERNAME}
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: ${ME_CONFIG_MONGODB_ADMINPASSWORD}
|
||||
ME_CONFIG_MONGODB_URL: mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017/
|
||||
ME_CONFIG_BASICAUTH: false
|
||||
|
||||
networks:
|
||||
mongo-network:
|
||||
name: mongo-network
|
||||
@@ -1 +1,2 @@
|
||||
/node_modules
|
||||
.env
|
||||
Generated
+1057
-1175
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -5,15 +5,16 @@
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "nodemon server.js"
|
||||
"start": "nodemon --inspect server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.1",
|
||||
"mongoose": "^6.4.0",
|
||||
"nodemon": "^2.0.16"
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.1.0",
|
||||
"mongoose": "^8.13.2",
|
||||
"nodemon": "^3.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
+49
-46
@@ -1,24 +1,26 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
const app = express();
|
||||
const cors = require('cors');
|
||||
const { response } = require('express');
|
||||
|
||||
app.use(cors(), express.json());
|
||||
|
||||
const port = process.env.PORT || 5000;
|
||||
const MONGO_IP_ADDRESS = '192.168.100.102';
|
||||
mongoose.connect('mongodb://root:exampleok@nas.local.net:27017/?authMechanism=DEFAULT', { useNewUrlParser: true });
|
||||
|
||||
const DB_HOST = 'server';
|
||||
const DB_PORT = 27017;
|
||||
const DB_USERNAME = process.env.DB_USERNAME;
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD;
|
||||
|
||||
const connectionString = `mongodb://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}?authMechanism=DEFAULT`;
|
||||
mongoose.connect(connectionString);
|
||||
const db = mongoose.connection;
|
||||
|
||||
db.on('error', console.error.bind(console, 'connection error:'));
|
||||
db.once('open', function () {
|
||||
console.log(`connected to mongodb: ${MONGO_IP_ADDRESS}`);
|
||||
console.log(`connected to mongodb: ${DB_HOST}`);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
/*------------MODELS------------*/
|
||||
//create schema
|
||||
var itemSchema = new mongoose.Schema({
|
||||
@@ -30,7 +32,7 @@ var itemSchema = new mongoose.Schema({
|
||||
var boxSchema = new mongoose.Schema({
|
||||
name: String,
|
||||
description: String,
|
||||
item: [itemSchema],
|
||||
items: [itemSchema],
|
||||
sort: Number,
|
||||
color: String,
|
||||
});
|
||||
@@ -44,50 +46,47 @@ var Item = mongoose.model('item', itemSchema, 'item');
|
||||
app.listen(port, () => console.log(`Listening on port ${port}`));
|
||||
|
||||
// get all boxes
|
||||
app.get('/box', async (request, response) => {
|
||||
app.get('/boxes', async (request, response) => {
|
||||
const boxes = await getAllBoxes();
|
||||
response.send(boxes);
|
||||
});
|
||||
|
||||
// create box
|
||||
app.post('/box', async (request, response) => {
|
||||
const numBoxes = await Box.countDocuments({});
|
||||
const query = Object.assign({ sort: numBoxes }, request.body);
|
||||
await Box.create(query);
|
||||
const updatedBoxes = await getAllBoxes();
|
||||
response.send(updatedBoxes);
|
||||
app.post('/boxes', async (request, response) => {
|
||||
try {
|
||||
const numBoxes = await Box.countDocuments();
|
||||
const newBox = await Box.create({
|
||||
...request.body,
|
||||
sort: numBoxes,
|
||||
});
|
||||
|
||||
response.status(201).json(newBox);
|
||||
} catch (error) {
|
||||
console.error('Error creating box:', error);
|
||||
response.status(500).json({ error: 'Failed to create box' });
|
||||
}
|
||||
});
|
||||
//edit box
|
||||
app.put('/box/:id', async (request, response) => {
|
||||
const id = request.params.id;
|
||||
await Box.updateOne({ _id: id }, { $set: request.body });
|
||||
const updatedBoxes = await getAllBoxes();
|
||||
response.send(updatedBoxes);
|
||||
});
|
||||
//delete box
|
||||
app.delete('/box/:id', async (request, response) => {
|
||||
await Box.deleteOne({ _id: request.params.id });
|
||||
const updatedBoxes = await getAllBoxes();
|
||||
response.send(updatedBoxes);
|
||||
});
|
||||
//change box sort
|
||||
app.put('/box/:id/sort', async (request, response) => {
|
||||
const source = await getBox(request.params.id);
|
||||
const target = request.body;
|
||||
await Box.updateOne({ _id: source._id }, { $set: { sort: target.sort } });
|
||||
await Box.updateOne({ _id: target._id }, { $set: { sort: source.sort } });
|
||||
const updatedBoxes = await getAllBoxes();
|
||||
response.send(updatedBoxes);
|
||||
})
|
||||
|
||||
|
||||
//create item
|
||||
app.post('/box/:id/item', async (request, response) => {
|
||||
const id = request.params.id;
|
||||
const query = { _id: id };
|
||||
await Box.updateOne(query, {
|
||||
$push: { item: request.body }
|
||||
});
|
||||
const updatedBox = await getBox(id);
|
||||
response.send(updatedBox);
|
||||
app.post('/boxes/:boxId/items', async (request, response) => {
|
||||
try {
|
||||
const item = new Item(request.body);
|
||||
const savedItem = await item.save();
|
||||
console.log(item)
|
||||
const box = await Box.findById(request.params.boxId);
|
||||
if (!box) {
|
||||
throw new Error('Box not found');
|
||||
}
|
||||
|
||||
box.items.push(savedItem);
|
||||
await box.save();
|
||||
response.send(item);
|
||||
} catch (error) {
|
||||
console.error('Error adding item to Box:', error);
|
||||
throw error; // or handle it according to your needs
|
||||
}
|
||||
|
||||
});
|
||||
//edit item
|
||||
app.put('/box/:boxId/item/:itemId', async (request, response) => {
|
||||
@@ -146,3 +145,7 @@ const getBox = async (id) => {
|
||||
const data = await Box.findById(id);
|
||||
return data;
|
||||
}
|
||||
const getItem = async (id) => {
|
||||
const data = await Item.findById(id);
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user