TypeScript rewrite #2
@@ -1 +1,3 @@
|
||||
/.vs
|
||||
/.vscode
|
||||
.env
|
||||
@@ -1,21 +1,54 @@
|
||||
# storage
|
||||
#### purpose
|
||||
# storage-manager
|
||||
|
||||
## purpose
|
||||
This project is supposed to make finding objects inside boxes easier. Translucent storage boxes are ugly. Over time and with the addition of new items, labels become irrelevant to the items they are meant to describe. They are inflexible. Distinctions between which box an item belongs can be arbitrary and confusing. e.g. Scissors belonging to 'Tools' or 'Crafts'.
|
||||
#### features:
|
||||
|
||||
## features:
|
||||
- Record physical location, quantity, description, etc of items
|
||||
- Search by item/box name, description
|
||||
- Move items to and from boxes
|
||||
- Categorize boxes by color
|
||||
#### requirements
|
||||
|
||||
## Stack
|
||||
- mongodb
|
||||
- node.js
|
||||
#### development environment
|
||||
/server/
|
||||
- `npm i`
|
||||
- `MONGO_IP_ADDRESS` change to mongodb host address
|
||||
- create mongodb database named `storage`
|
||||
- `npm run start`
|
||||
- Vite
|
||||
- Typescript
|
||||
- TailwindCSS
|
||||
|
||||
/client/
|
||||
## Dev Environment
|
||||
### Database
|
||||
Located in `database` directory. Configured to use Mongo DB Docker container `mongo-db`. Included is a `docker-compose.yml` file that will host a Mongo DB on port `27017` using credentials configured with environment variables, e.g:
|
||||
|
||||
```env
|
||||
MONGO_INITDB_ROOT_USERNAME=<root username>
|
||||
MONGO_INITDB_ROOT_PASSWORD=<root password>
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME=<mongodb username>
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD=<mongodb admin password>
|
||||
```
|
||||
Also includes Mongo Express container `mongo-express`.
|
||||
|
||||
---
|
||||
|
||||
### Server
|
||||
Located in `server` directory.
|
||||
- Edit `.env` file with credentials for database, e.g:
|
||||
|
||||
```env
|
||||
DB_USERNAME=<username>
|
||||
DB_PASSWORD=<password>
|
||||
DB_HOST=<hostname> //server
|
||||
DB_PORT=<db port> //27017
|
||||
PORT=<server port> //5000
|
||||
```
|
||||
|
||||
- Run `npm i` to install required node packages
|
||||
- Run `npm run start` to start server, uses `nodemon` to watch for server changes and restart server automatically.
|
||||
|
||||
---
|
||||
|
||||
### Client
|
||||
Located in `client/` directory.
|
||||
- `npm i`
|
||||
- `npm run start`
|
||||
- Uses `vite` build and dev server, `npm run dev`
|
||||
|
||||
---
|
||||
+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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Storage management tool" />
|
||||
<link rel="shortcut icon" type="image/jpg" href="./storage_32.ico" />
|
||||
<title>Storage Manager</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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
+3096
-13609
File diff suppressed because it is too large
Load Diff
+24
-35
@@ -1,43 +1,32 @@
|
||||
{
|
||||
"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": {
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"axios": "^1.8.4",
|
||||
"color": "^5.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.1.3"
|
||||
},
|
||||
"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",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Storage management tool" />
|
||||
<link rel="shortcut icon" type="image/jpg" href="./storage_32.ico"/>
|
||||
<title>Storage Manager</title>
|
||||
</head>
|
||||
|
||||
<body class="scroll-container">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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,159 @@
|
||||
import { useEffect, useState, useReducer } from "react";
|
||||
import { getAllBoxes, Box, BoxCard, BoxFormModal } from "./features/box";
|
||||
import { Item, moveItem } from "./features/item";
|
||||
|
||||
import { Plus, Search } from "./icons";
|
||||
|
||||
const App = () => {
|
||||
const [boxes, setBoxes] = useState<Box[]>([]);
|
||||
const [filteredBoxes, setFilteredBoxes] = useState<Box[]>([]);
|
||||
|
||||
const [isItemMoving, setIsItemMoving] = useState(false);
|
||||
|
||||
const [itemToBeMoved, setItemToBeMoved] = useState<Item>();
|
||||
const [itemToBeMovedSourceBox, setItemToBeMovedSourceBox] = useState<string>();
|
||||
|
||||
|
||||
const handleSelectMoveToBox = async (targetBox: Box) => {
|
||||
|
||||
setIsItemMoving(false);
|
||||
|
||||
await moveItem(itemToBeMoved?._id!, targetBox._id!);
|
||||
|
||||
|
||||
setBoxes(prev => {
|
||||
const updatedBoxes = prev.map(box => {
|
||||
if (box._id === itemToBeMovedSourceBox) {
|
||||
return {
|
||||
...box,
|
||||
items: box.items?.filter(item => item._id !== itemToBeMoved?._id) ?? []
|
||||
};
|
||||
}
|
||||
if (box._id === targetBox._id && itemToBeMoved) {
|
||||
return {
|
||||
...box,
|
||||
items: [...(box.items ?? []), itemToBeMoved]
|
||||
};
|
||||
}
|
||||
return box;
|
||||
});
|
||||
return updatedBoxes;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
const handleMoveItem = (item: Item, boxId: string) => {
|
||||
setItemToBeMoved(item);
|
||||
setItemToBeMovedSourceBox(boxId);
|
||||
setIsItemMoving(true);
|
||||
}
|
||||
|
||||
const handleCreateBox = (newBox: Box) => {
|
||||
setBoxes((boxes) => [...boxes, newBox]);
|
||||
};
|
||||
const handleDeleteBox = (id: string) => {
|
||||
setBoxes((boxes) => boxes.filter((box) => box._id !== id));
|
||||
};
|
||||
const [isBoxFormModalOpen, setBoxFormModalOpen] = useState(false);
|
||||
const handleBoxFormModalOpen = () => {
|
||||
setBoxFormModalOpen(true);
|
||||
};
|
||||
const handleBoxFormModalClose = () => {
|
||||
setBoxFormModalOpen(false);
|
||||
};
|
||||
|
||||
// box filter
|
||||
const [searchString, setSearch] = useState("");
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const ss = searchString.toLowerCase();
|
||||
const result = boxes.filter((box) => {
|
||||
|
||||
const matchingItems = box.items?.filter(
|
||||
(item) =>
|
||||
item.name && item.name.toLowerCase().includes(ss) ||
|
||||
item.description && item.description.toLowerCase().includes(ss),
|
||||
);
|
||||
|
||||
return (
|
||||
box.name.toLowerCase().includes(ss) ||
|
||||
box.description.toLowerCase().includes(ss) ||
|
||||
(matchingItems && matchingItems.length > 0)
|
||||
);
|
||||
})
|
||||
setFilteredBoxes(result);
|
||||
}, [boxes, searchString]);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<BoxFormModal
|
||||
mode="create"
|
||||
isOpen={isBoxFormModalOpen}
|
||||
onSubmitBox={handleCreateBox}
|
||||
onClose={handleBoxFormModalClose}
|
||||
/>
|
||||
<div className='page-content'>
|
||||
|
||||
<div className="utility-container">
|
||||
<button onClick={handleBoxFormModalOpen} className="group button-icon">
|
||||
{<Plus height="36" width="36" />}
|
||||
</button>
|
||||
<div className="flex justify-center p-2 align-middle max-w-lg w-full">
|
||||
<div className="search-container group">
|
||||
<input
|
||||
onChange={handleSearchChange}
|
||||
spellCheck="false"
|
||||
type="text"
|
||||
id="box-filter"
|
||||
name="filter"
|
||||
className="input-search"
|
||||
/>
|
||||
<Search viewBox="0 0 24 24" className="search-icon" />
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="box-card-list-container">
|
||||
<div className="box-card-list">
|
||||
{filteredBoxes.map((box) => (
|
||||
<BoxCard
|
||||
key={box._id}
|
||||
initialBox={box}
|
||||
onDeleteBox={handleDeleteBox}
|
||||
onMoveItem={handleMoveItem}
|
||||
onClickMoveToBox={handleSelectMoveToBox}
|
||||
isItemMoving={box._id !== itemToBeMovedSourceBox && isItemMoving}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -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,17 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const Color = ({ colorData, handler, boxColor }) => {
|
||||
const [style, setStyle] = useState(colorData.defaultStyle);
|
||||
|
||||
const handleHover = (isHover) => {
|
||||
setStyle(isHover ? colorData.lightStyle : colorData.defaultStyle);
|
||||
}
|
||||
|
||||
return (
|
||||
<label onMouseEnter={() => handleHover(true)} onMouseLeave={() => handleHover(false)} style={style} className={`color ${boxColor === colorData.title ? 'selected' : ''}`}>
|
||||
<input checked={boxColor === colorData.title} onChange={handler} type='radio' value={colorData.title}></input>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export default Color;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
const ColorSelect = ({ colorData, handler, boxColor }) => {
|
||||
const [style, setStyle] = useState(colorData.defaultStyle);
|
||||
|
||||
const handleHover = (isHover) => {
|
||||
setStyle(isHover ? colorData.lightStyle : colorData.defaultStyle);
|
||||
};
|
||||
|
||||
return (
|
||||
<label
|
||||
onMouseEnter={() => handleHover(true)}
|
||||
onMouseLeave={() => handleHover(false)}
|
||||
style={style}
|
||||
>
|
||||
<input
|
||||
checked={boxColor === colorData.title}
|
||||
onChange={handler}
|
||||
type="radio"
|
||||
value={colorData.title}
|
||||
></input>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorSelect;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
iconProps?: IconProps;
|
||||
onClick: () => void;
|
||||
}
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
interface GenericMenuProps {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
menuItems: MenuItem[];
|
||||
Icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
iconProps?: IconProps;
|
||||
}
|
||||
|
||||
export const GenericMenu = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
menuItems,
|
||||
Icon,
|
||||
iconProps,
|
||||
}: GenericMenuProps) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(event.target as Node) &&
|
||||
menuButtonRef.current &&
|
||||
!menuButtonRef.current.contains(event.target as Node)
|
||||
) {
|
||||
onToggle();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="menu">
|
||||
<button
|
||||
className="group button-icon"
|
||||
ref={menuButtonRef}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Icon {...iconProps} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div ref={menuRef} className="menu-list-container">
|
||||
<ul className={`menu-list`}>
|
||||
{menuItems.map((item, index) => (
|
||||
<li className={`menu-item`} key={index}>
|
||||
<button onClick={item.onClick} className={`menu-button`}>
|
||||
{<item.icon {...item.iconProps} />}
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Modal: React.FC<ModalProps> = ({ isOpen, children }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-container">
|
||||
<div className="modal">
|
||||
<div className="modal-children">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Item } from "../item/item.types";
|
||||
|
||||
export interface Box {
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
items?: Item[];
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Box, BoxFormModal, deleteBox } from "../";
|
||||
import { Item, ItemCard, ItemFormModal } from "../../item";
|
||||
import { Plus, Ellipsis, Garbage, Pencil } from "../../../icons";
|
||||
import { GenericMenu } from '../../../components/GenericMenu';
|
||||
|
||||
interface BoxCardProps {
|
||||
initialBox: Box;
|
||||
onDeleteBox: (id: string) => void;
|
||||
onMoveItem: (item: Item, boxId: string) => void;
|
||||
isItemMoving: boolean;
|
||||
onClickMoveToBox: (box: Box) => void;
|
||||
}
|
||||
|
||||
export const BoxCard: React.FC<BoxCardProps> = ({ initialBox, onDeleteBox, onMoveItem, onClickMoveToBox, isItemMoving }) => {
|
||||
const [box, setBox] = useState<Box>(initialBox);
|
||||
|
||||
const [isBoxFormModalOpen, setBoxFormModalOpen] = useState(false);
|
||||
const [isItemFormModalOpen, setItemFormModalOpen] = useState(false);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setBox(initialBox);
|
||||
}, [initialBox]);
|
||||
|
||||
const toggleMenu = () => {
|
||||
setIsMenuOpen((prevState) => !prevState);
|
||||
};
|
||||
|
||||
const handleOpenItemFormModal = () => {
|
||||
setItemFormModalOpen(true);
|
||||
};
|
||||
const handleCloseItemFormModal = () => {
|
||||
setItemFormModalOpen(false);
|
||||
};
|
||||
|
||||
const handleOpenBoxFormModal = () => {
|
||||
setBoxFormModalOpen(true);
|
||||
};
|
||||
const handleCloseBoxFormModal = () => {
|
||||
setBoxFormModalOpen(false);
|
||||
};
|
||||
|
||||
const handleCreateItem = (newItem: Item) => {
|
||||
const updatedBox: Box = {
|
||||
...box,
|
||||
items: [...(box.items ?? []), newItem],
|
||||
};
|
||||
setBox(updatedBox);
|
||||
};
|
||||
|
||||
const handleClickEditBox = () => {
|
||||
setBoxFormModalOpen(true);
|
||||
};
|
||||
const handleUpdateBox = (updatedBox: Box) => {
|
||||
setBox(updatedBox);
|
||||
};
|
||||
const handleDeleteBox = async () => {
|
||||
if (box._id != null) {
|
||||
const response = await deleteBox(box._id);
|
||||
if (response.success) {
|
||||
onDeleteBox(box._id);
|
||||
} else {
|
||||
console.error("No data returned in the response");
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleDeleteItem = (id: string) => {
|
||||
const updatedItems = (box.items ?? []).filter(item => item._id !== id);
|
||||
const updatedBox: Box = {
|
||||
...box,
|
||||
items: updatedItems,
|
||||
};
|
||||
setBox(updatedBox);
|
||||
};
|
||||
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: "Edit",
|
||||
icon: Pencil,
|
||||
iconProps: { height: 16, width: 16 },
|
||||
onClick: handleClickEditBox,
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
icon: Garbage,
|
||||
iconProps: { height: 16, width: 16 },
|
||||
onClick: handleDeleteBox,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<BoxFormModal
|
||||
mode="edit"
|
||||
initialData={box}
|
||||
isOpen={isBoxFormModalOpen}
|
||||
onSubmitBox={handleUpdateBox}
|
||||
onClose={handleCloseBoxFormModal}
|
||||
/>
|
||||
<ItemFormModal
|
||||
mode="create"
|
||||
isOpen={isItemFormModalOpen}
|
||||
onSubmitItem={handleCreateItem}
|
||||
onClose={handleCloseItemFormModal}
|
||||
boxId={box._id!}
|
||||
/>
|
||||
<div className={`box-card-container relative ${isItemMoving ? 'target' : ''}`} >
|
||||
{isItemMoving && <div onClick={() => onClickMoveToBox(box)} className="overlay"></div>}
|
||||
<div className={`box-card`}>
|
||||
<div className="box-card-header">
|
||||
<div className="box-card-title-container">
|
||||
<h1 className="box-card-title">{box.name}</h1>
|
||||
<h2 className="box-card-subtitle">{box.description}</h2>
|
||||
</div>
|
||||
<GenericMenu
|
||||
onToggle={toggleMenu}
|
||||
menuItems={menuItems}
|
||||
isOpen={isMenuOpen}
|
||||
Icon={Ellipsis}
|
||||
iconProps={{ height: 24, width: 24 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="item-card-list-container scrollbar-minimal">
|
||||
<ul className="item-card-list">
|
||||
{box.items?.map((item) => (
|
||||
<ItemCard
|
||||
key={item._id}
|
||||
boxId={box._id!}
|
||||
initialItem={item}
|
||||
onDeleteItem={handleDeleteItem}
|
||||
onMoveItem={onMoveItem}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button
|
||||
className="group button-icon"
|
||||
onClick={handleOpenItemFormModal}
|
||||
>
|
||||
<Plus height="24" width="24" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useState } from "react";
|
||||
import { Modal } from '../../../components/Modal';
|
||||
import { Box, createBox, updateBox } from "..";
|
||||
|
||||
interface BoxFormModalProps {
|
||||
mode: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmitBox: (box: Box) => void;
|
||||
initialData?: Partial<Box>;
|
||||
}
|
||||
export const BoxFormModal: React.FC<BoxFormModalProps> = ({
|
||||
mode,
|
||||
initialData,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmitBox,
|
||||
}) => {
|
||||
const [boxName, setBoxName] = useState(initialData?.name ?? "");
|
||||
const [boxDescription, setBoxDescription] = useState(
|
||||
initialData?.description ?? "",
|
||||
);
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const boxData: Box = {
|
||||
name: boxName as string,
|
||||
description: boxDescription as string,
|
||||
};
|
||||
|
||||
const isEdit = mode === "edit";
|
||||
|
||||
try {
|
||||
const response = isEdit
|
||||
? await updateBox(initialData?._id!, boxData)
|
||||
: await createBox(boxData);
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
console.error(
|
||||
"Box operation failed:",
|
||||
response.error ?? "Unknown error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmitBox(response.data);
|
||||
} catch (err) {
|
||||
console.error(`Error during ${isEdit ? "update" : "create"}:`, err);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}>
|
||||
<form onSubmit={handleSubmit} id="form">
|
||||
<input value="" id="id" name="id" readOnly={true} hidden={true} />
|
||||
<h1 className="modal-header">Edit Box</h1>
|
||||
<fieldset>
|
||||
<label className="input-label" htmlFor="box-name">
|
||||
name
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={boxName}
|
||||
onChange={(e) => setBoxName(e.target.value)}
|
||||
id="box-name"
|
||||
name="name"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<label className="input-label" htmlFor="box-description">
|
||||
description
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={boxDescription}
|
||||
onChange={(e) => setBoxDescription(e.target.value)}
|
||||
id="box-description"
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
<div className='actions'>
|
||||
<button className="button" type="submit">
|
||||
Submit
|
||||
</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,64 @@
|
||||
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",
|
||||
};
|
||||
}
|
||||
};
|
||||
const updateBox = async (
|
||||
boxId: string,
|
||||
boxData: Box,
|
||||
): Promise<ApiResponse<Box>> => {
|
||||
try {
|
||||
const response = await api.put<Box>(`/boxes/${boxId}`, boxData);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating box:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Could not update box",
|
||||
};
|
||||
}
|
||||
};
|
||||
const deleteBox = async (id: string): Promise<ApiResponse<null>> => {
|
||||
try {
|
||||
const response = await api.delete<null>(`boxes/${id}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error deleting box:", error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Could not delete box",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export { getAllBoxes, createBox, updateBox, deleteBox };
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useState } from "react";
|
||||
import { Item, ItemFormModal, deleteItem } from "../";
|
||||
import { Ellipsis, Garbage, Pencil } from "../../../icons";
|
||||
import { GenericMenu } from "../../../components/GenericMenu";
|
||||
|
||||
interface ItemCardProps {
|
||||
boxId: string;
|
||||
initialItem: Item;
|
||||
onDeleteItem: (id: string) => void;
|
||||
onMoveItem: (item: Item, boxId: string) => void;
|
||||
}
|
||||
|
||||
export const ItemCard: React.FC<ItemCardProps> = ({
|
||||
boxId,
|
||||
initialItem,
|
||||
onDeleteItem,
|
||||
onMoveItem
|
||||
}) => {
|
||||
const [isItemFormModalOpen, setIsItemFormModalOpen] = useState(false);
|
||||
const [item, setItem] = useState(initialItem);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [formModalMode, setFormModalMode] = useState("edit");
|
||||
|
||||
|
||||
const toggleMenu = () => {
|
||||
setIsMenuOpen((prevState) => !prevState);
|
||||
};
|
||||
const handleCloseItemFormModal = () => {
|
||||
setIsItemFormModalOpen(false);
|
||||
};
|
||||
const handleClickEditItem = async () => {
|
||||
setFormModalMode("edit");
|
||||
setIsItemFormModalOpen(true);
|
||||
};
|
||||
const handleClickViewItem = async () => {
|
||||
setFormModalMode("readonly");
|
||||
setIsItemFormModalOpen(true);
|
||||
|
||||
}
|
||||
const handleUpdateItem = (updatedItem: Item) => {
|
||||
setItem(updatedItem);
|
||||
};
|
||||
|
||||
const handleDeleteItem = async () => {
|
||||
if (item._id != null) {
|
||||
const response = await deleteItem(boxId, item._id);
|
||||
if (response.success) {
|
||||
onDeleteItem(item._id);
|
||||
} else {
|
||||
console.error("No data returned in the response");
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleClickMoveItem = async () => {
|
||||
setIsMenuOpen(false);
|
||||
onMoveItem(item, boxId);
|
||||
}
|
||||
const menuItems = [
|
||||
{ label: "Edit", icon: Pencil, onClick: handleClickEditItem },
|
||||
{ label: "Delete", icon: Garbage, onClick: handleDeleteItem },
|
||||
{ label: "Move", icon: Pencil, onClick: handleClickMoveItem }
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<ItemFormModal
|
||||
mode={formModalMode}
|
||||
isOpen={isItemFormModalOpen}
|
||||
onSubmitItem={handleUpdateItem}
|
||||
onClose={handleCloseItemFormModal}
|
||||
boxId={boxId}
|
||||
initialData={item}
|
||||
/>
|
||||
<li>
|
||||
<div className="item-container">
|
||||
<div className='item' onClick={handleClickViewItem}>
|
||||
<h3 className="item-name">{item.name}</h3>
|
||||
</div>
|
||||
<GenericMenu
|
||||
isOpen={isMenuOpen}
|
||||
menuItems={menuItems}
|
||||
onToggle={toggleMenu}
|
||||
Icon={Ellipsis}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useState } from "react";
|
||||
import { Modal } from '../../../components/Modal';
|
||||
import { Item, createItem, updateItem } from "..";
|
||||
|
||||
interface ItemFormModalProps {
|
||||
mode: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmitItem: (item: Item) => void;
|
||||
boxId: string;
|
||||
initialData?: Partial<Item>;
|
||||
}
|
||||
export const ItemFormModal: React.FC<ItemFormModalProps> = ({
|
||||
mode,
|
||||
initialData,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmitItem,
|
||||
boxId,
|
||||
}) => {
|
||||
const [itemName, setItemName] = useState(initialData?.name ?? "");
|
||||
const [itemDescription, setItemDescription] = useState(
|
||||
initialData?.description ?? "",
|
||||
);
|
||||
const [itemQuantity, setItemQuantity] = useState(initialData?.quantity ?? 0);
|
||||
|
||||
const titles: Record<string, string> = {
|
||||
readonly: 'View',
|
||||
edit: 'Edit',
|
||||
create: 'Create',
|
||||
};
|
||||
const itemTitle = titles[mode] ?? 'Item';
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const itemData: Item = {
|
||||
name: itemName as string,
|
||||
description: itemDescription as string,
|
||||
quantity: itemQuantity as number,
|
||||
};
|
||||
const isEdit = mode === 'edit';
|
||||
|
||||
try {
|
||||
const response = isEdit
|
||||
? await updateItem(boxId!, initialData?._id!, itemData)
|
||||
: await createItem(itemData, boxId);
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
console.error(
|
||||
"Item operation failed:",
|
||||
response.error ?? "Unknown error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmitItem(response.data);
|
||||
} catch (err) {
|
||||
console.error(`Error during ${isEdit ? "update" : "create"}:`, err);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}>
|
||||
<form onSubmit={handleSubmit} id="form">
|
||||
<input value="" id="id" name="id" readOnly={true} hidden={true} />
|
||||
<h1>{itemTitle} Item</h1>
|
||||
<fieldset>
|
||||
<label className="input-label" htmlFor="item-name">
|
||||
name
|
||||
</label>
|
||||
<input
|
||||
readOnly={mode === "readonly"}
|
||||
className="input"
|
||||
value={itemName}
|
||||
onChange={(e) => setItemName(e.currentTarget.value)}
|
||||
id="item-name"
|
||||
name="name"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<label className="input-label" htmlFor="item-description">
|
||||
description
|
||||
</label>
|
||||
<input
|
||||
readOnly={mode === "readonly"}
|
||||
className="input"
|
||||
value={itemDescription}
|
||||
onChange={(e) => setItemDescription(e.currentTarget.value)}
|
||||
id="item-description"
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<label className="input-label" htmlFor="item-quantity">
|
||||
quantity
|
||||
</label>
|
||||
<input
|
||||
readOnly={mode === "readonly"}
|
||||
className="input"
|
||||
value={itemQuantity}
|
||||
onChange={(e) =>
|
||||
setItemQuantity(parseInt(e.currentTarget.value, 10))
|
||||
}
|
||||
id="item-quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<div className='actions'>
|
||||
{mode !== "readonly" && <button className="button" type="submit">
|
||||
Submit
|
||||
</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,80 @@
|
||||
import { api, ApiResponse } from "../../../api";
|
||||
import { Item } from "..";
|
||||
|
||||
const createItem = async (
|
||||
itemData: Item,
|
||||
boxId: string,
|
||||
): Promise<ApiResponse<Item>> => {
|
||||
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,
|
||||
error: "Could not create item",
|
||||
};
|
||||
}
|
||||
};
|
||||
const updateItem = async (
|
||||
boxId: string,
|
||||
itemId: string,
|
||||
itemData: Item,
|
||||
): Promise<ApiResponse<Item>> => {
|
||||
try {
|
||||
const response = await api.put<Item>(
|
||||
`/boxes/${boxId}/items/${itemId}`,
|
||||
itemData,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating item:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Could not update item",
|
||||
};
|
||||
}
|
||||
};
|
||||
const deleteItem = async (
|
||||
boxId: string,
|
||||
itemId: string,
|
||||
): Promise<ApiResponse<null>> => {
|
||||
try {
|
||||
const response = await api.delete<null>(`boxes/${boxId}/items/${itemId}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error deleting item:", error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Could not delete item",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const moveItem = async (itemId: string, boxId: string): Promise<ApiResponse<null>> => {
|
||||
try {
|
||||
const response = await api.patch<null>(`items/${itemId}`, { boxId });
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error moving item:", error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Could not move item",
|
||||
};
|
||||
}
|
||||
}
|
||||
export { createItem, updateItem, deleteItem, moveItem };
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export const Ellipsis = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<Icon {...props}>
|
||||
<path d="M3 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm6.5 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zM16 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"></path>
|
||||
</Icon>
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export const Garbage = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<Icon {...props}>
|
||||
<path d="M5.25 3v-.917C5.25.933 6.183 0 7.333 0h1.334c1.15 0 2.083.933 2.083 2.083V3h4.75v1.5h-.972l-1.257 9.544A2.25 2.25 0 0 1 11.041 16H4.96a2.25 2.25 0 0 1-2.23-1.956L1.472 4.5H.5V3h4.75zm1.5-.917V3h2.5v-.917a.583.583 0 0 0-.583-.583H7.333a.583.583 0 0 0-.583.583zM2.986 4.5l1.23 9.348a.75.75 0 0 0 .744.652h6.08a.75.75 0 0 0 .744-.652L13.015 4.5H2.985z"></path>
|
||||
</Icon>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
type IconProps = React.SVGProps<SVGSVGElement> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Icon = ({
|
||||
className = "icon",
|
||||
height = 16,
|
||||
width = 16,
|
||||
children,
|
||||
...props
|
||||
}: IconProps) => (
|
||||
<svg
|
||||
className={className}
|
||||
height={height}
|
||||
width={width}
|
||||
viewBox="0 0 16 16"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export const Pencil = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<Icon {...props}>
|
||||
<path d="M11.838.714a2.438 2.438 0 0 1 3.448 3.448l-9.841 9.841c-.358.358-.79.633-1.267.806l-3.173 1.146a.75.75 0 0 1-.96-.96l1.146-3.173c.173-.476.448-.909.806-1.267l9.84-9.84zm2.387 1.06a.938.938 0 0 0-1.327 0l-9.84 9.842a1.953 1.953 0 0 0-.456.716L2 14.002l1.669-.604a1.95 1.95 0 0 0 .716-.455l9.841-9.841a.938.938 0 0 0 0-1.327z"></path>
|
||||
</Icon>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export const Plus = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<Icon {...props}>
|
||||
<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8z"></path>
|
||||
<path d="M11.75 8a.75.75 0 0 1-.75.75H8.75V11a.75.75 0 0 1-1.5 0V8.75H5a.75.75 0 0 1 0-1.5h2.25V5a.75.75 0 0 1 1.5 0v2.25H11a.75.75 0 0 1 .75.75z"></path>
|
||||
</Icon>
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export const Search = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<Icon {...props}>
|
||||
<path d="M10.533 1.27893C5.35215 1.27893 1.12598 5.41887 1.12598 10.5579C1.12598 15.697 5.35215 19.8369 10.533 19.8369C12.767 19.8369 14.8235 19.0671 16.4402 17.7794L20.7929 22.132C21.1834 22.5226 21.8166 22.5226 22.2071 22.132C22.5976 21.7415 22.5976 21.1083 22.2071 20.7178L17.8634 16.3741C19.1616 14.7849 19.94 12.7634 19.94 10.5579C19.94 5.41887 15.7138 1.27893 10.533 1.27893ZM3.12598 10.5579C3.12598 6.55226 6.42768 3.27893 10.533 3.27893C14.6383 3.27893 17.94 6.55226 17.94 10.5579C17.94 14.5636 14.6383 17.8369 10.533 17.8369C6.42768 17.8369 3.12598 14.5636 3.12598 10.5579Z"></path>
|
||||
</Icon>
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
export { Plus } from "./Plus";
|
||||
export { Ellipsis } from "./Ellipsis";
|
||||
export { Garbage } from "./Garbage";
|
||||
export { Pencil } from "./Pencil";
|
||||
export { Search } from "./Search";
|
||||
@@ -1,3 +1,8 @@
|
||||
@import 'tailwindcss';
|
||||
* {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
@@ -11,3 +16,132 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-base: #121212;
|
||||
--color-highlight: #1f1f1f;
|
||||
--color-highlight-2: #282828;
|
||||
--color-subdued: #b3b3b3;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
@apply flex-col p-4;
|
||||
}
|
||||
|
||||
.scrollbar-minimal::-webkit-scrollbar {
|
||||
@apply h-1 w-1;
|
||||
}
|
||||
|
||||
.scrollbar-minimal::-webkit-scrollbar-thumb {
|
||||
@apply bg-highlight rounded-full;
|
||||
}
|
||||
|
||||
.scrollbar-minimal::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-black;
|
||||
}
|
||||
|
||||
.utility-container {
|
||||
@apply flex py-4 w-full justify-center items-center;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
@apply h-min cursor-pointer rounded-full p-2 hover:bg-highlight;
|
||||
}
|
||||
.icon {
|
||||
@apply fill-subdued group-hover:fill-white;
|
||||
}
|
||||
|
||||
.box-card-list-container {
|
||||
@apply flex;
|
||||
}
|
||||
.box-card-list {
|
||||
@apply m-0 w-full flex flex-wrap p-0 items-start;
|
||||
}
|
||||
.box-card-container {
|
||||
@apply max-w-md w-full;
|
||||
}
|
||||
.box-card {
|
||||
@apply grid grid-rows-[auto_1fr_auto] m-2 min-h-64 max-h-128 min-w-[320px] max-w-full rounded-md bg-base p-4 text-subdued;
|
||||
}
|
||||
.overlay {
|
||||
@apply absolute inset-0 bg-black opacity-50 z-10 border-dashed border-2 border-subdued hover:border-green-300 hover:cursor-pointer;
|
||||
}
|
||||
.box-card-header {
|
||||
@apply flex justify-between;
|
||||
}
|
||||
.box-card-title {
|
||||
@apply mb-2 text-xl font-bold text-white;
|
||||
}
|
||||
.box-card-subtitle {
|
||||
@apply mb-4 text-sm font-semibold text-subdued;
|
||||
}
|
||||
.menu {
|
||||
@apply z-40 flex items-center relative;
|
||||
}
|
||||
.menu-list-container {
|
||||
@apply absolute right-0 z-10 mt-2 w-36 rounded bg-highlight p-1 shadow-lg;
|
||||
}
|
||||
.menu-list {
|
||||
@apply text-sm;
|
||||
}
|
||||
.menu-button {
|
||||
@apply flex w-full cursor-pointer items-center gap-4 rounded-xs px-4 py-2 text-subdued hover:bg-highlight-2 hover:fill-white hover:text-white;
|
||||
}
|
||||
.menu-button:hover * {
|
||||
@apply fill-white;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
@apply block w-full cursor-pointer text-left;
|
||||
}
|
||||
|
||||
.item-card-list-container {
|
||||
@apply max-h-64 overflow-y-auto;
|
||||
}
|
||||
.item-card-list {
|
||||
}
|
||||
.item-container {
|
||||
@apply flex justify-between align-middle rounded-sm px-2 hover:bg-highlight;
|
||||
}
|
||||
.item {
|
||||
@apply flex flex-auto hover:cursor-pointer p-2;
|
||||
}
|
||||
.item-name {
|
||||
@apply cursor-default;
|
||||
}
|
||||
.modal-container {
|
||||
@apply fixed inset-0 z-50 flex items-center justify-center bg-black/80;
|
||||
}
|
||||
.modal {
|
||||
@apply w-full max-w-lg rounded-xl bg-highlight p-6 m-6;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
@apply block text-sm font-medium;
|
||||
}
|
||||
.input {
|
||||
@apply mt-1 w-full rounded-sm border border-gray-300 p-2 focus:ring-2 focus:ring-blue-500;
|
||||
}
|
||||
.button {
|
||||
@apply rounded-sm bg-base px-4 py-2 text-white hover:bg-highlight-2 hover:cursor-pointer focus:ring-2 focus:ring-blue-500 focus:outline-none;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
@apply relative w-full max-w-lg focus-within:fill-white;
|
||||
}
|
||||
|
||||
.input-search {
|
||||
@apply border-2 border-subdued bg-highlight group-hover:bg-highlight-2 w-full rounded-full py-2 pr-4 pl-10 focus:outline-none group-focus-within:border-white;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
@apply absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 transform fill-subdued group-focus-within:fill-white;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@apply mt-4 flex gap-1;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -1,277 +0,0 @@
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||
|
||||
:root {
|
||||
--black_light: #9aa0a6;
|
||||
--black_dark: #212121;
|
||||
--white_light:#fafafa;
|
||||
--white_darker: #e0e0e0;
|
||||
}
|
||||
html, body, #root {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--white_light);
|
||||
color: var(--black_dark)
|
||||
}
|
||||
.body-container {
|
||||
margin: 0 2em;
|
||||
overflow:auto;
|
||||
}
|
||||
form > fieldset > label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
form > fieldset {
|
||||
border: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight:normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.filter-container {
|
||||
margin: 1rem 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 344px;
|
||||
height: 44px;
|
||||
background-color: white;
|
||||
font-size: 1rem;
|
||||
border-radius: 2rem;
|
||||
border: 1px solid #dfe1e5;
|
||||
}
|
||||
.filter-container > span.icon {
|
||||
font-size: 1rem;
|
||||
margin: 0 0.75rem 0 1rem;
|
||||
color: var(--black_light);
|
||||
}
|
||||
.filter-container > span.icon.clear {
|
||||
margin-left: auto;
|
||||
}
|
||||
.filter-container > span.icon.clear:hover {
|
||||
cursor: pointer;
|
||||
color: var(--black_dark);
|
||||
}
|
||||
.filter {
|
||||
width: 100%;
|
||||
font-weight: lighter;
|
||||
border: none;
|
||||
height: 100%;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.filter:focus {
|
||||
outline: none;
|
||||
}
|
||||
.filter::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.box-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.box-menu-container {
|
||||
margin-right: 1em;
|
||||
margin-left: auto;
|
||||
}
|
||||
.box-title {
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
.box-subtitle {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
opacity: 0.6;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
ul.list-box {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
margin: 0;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
li.box {
|
||||
position: relative;
|
||||
height: min-content;
|
||||
width: auto;
|
||||
padding: 16px 0;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--white_darker);
|
||||
box-shadow:
|
||||
0px 0px 0px 0px rgba(0, 0, 0, 0.2),
|
||||
0px 0px 0px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 0px 0px 0px rgba(0,0,0,.12);
|
||||
list-style: none;
|
||||
|
||||
}
|
||||
li.box.target:hover {
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 3px 6px rgba(0,0,0,0.16),
|
||||
0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
.item-menu-more-container {
|
||||
z-index: 1;
|
||||
}
|
||||
.item-menu-more-list {
|
||||
padding: 0;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
margin: 0 10px 0 0;
|
||||
top: 2rem;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--white_darker);
|
||||
box-shadow:
|
||||
0px 0px 0px 0px rgba(0, 0, 0, 0.2),
|
||||
0px 0px 0px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 0px 0px 0px rgba(0,0,0,.12);
|
||||
}
|
||||
.box-menu-action {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-menu-action:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.item-menu-action > .icon {
|
||||
margin: 1rem 1rem;
|
||||
}
|
||||
.list-box-item {
|
||||
padding: 0;
|
||||
padding: 1rem 0 3rem 0;
|
||||
max-height: 8rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.list-box-item.empty {
|
||||
padding: 0;
|
||||
}
|
||||
.scroll-container::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.scroll-container::-webkit-scrollbar-track {
|
||||
background: #ddd;
|
||||
}
|
||||
.scroll-container::-webkit-scrollbar-thumb {
|
||||
background: #666;
|
||||
}
|
||||
li.item {
|
||||
font-size: 0.75rem;
|
||||
display: grid;
|
||||
grid-template-columns: 0 1fr auto auto ;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0.5rem 0.75rem 0.5rem 1.25rem;
|
||||
list-style: none;
|
||||
}
|
||||
.head-divider {
|
||||
margin: 16px;
|
||||
width: auto;
|
||||
height: 1px;
|
||||
background-color: var(--white_darker);
|
||||
}
|
||||
.menu-more-container {
|
||||
z-index: 1;
|
||||
}
|
||||
.menu-bottom {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.add {
|
||||
background-color: var(--white_light);
|
||||
padding: 0.75rem;
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
border-radius: 50%;
|
||||
|
||||
}
|
||||
.add:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--white_darker);
|
||||
}
|
||||
.menu-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.1rem;
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.menu-btn.box {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.menu-btn.item {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.menu-btn:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.menu-more-list {
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--white_darker);
|
||||
box-shadow:
|
||||
0px 0px 0px 0px rgba(0, 0, 0, 0.2),
|
||||
0px 0px 0px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 0px 0px 0px rgba(0,0,0,.12);
|
||||
}
|
||||
.menu-more-list.box {
|
||||
width: 120px;
|
||||
margin: 0 10px 0 0;
|
||||
top: 4rem;
|
||||
right: 0;
|
||||
}
|
||||
.menu-more-list.item {
|
||||
width: 4.8rem;
|
||||
margin: 0 10px 0 0;
|
||||
top: 2rem;
|
||||
right: 0;
|
||||
}
|
||||
.menu-more-list.item > .menu-action > .icon {
|
||||
margin: 0.2rem 0.2rem;
|
||||
}
|
||||
.menu-action {
|
||||
z-index: inherit;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
.menu-action:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--white_darker);
|
||||
}
|
||||
.menu-action > .icon {
|
||||
margin: 0.6rem 0.6rem;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
button.button {
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
font-size: .875rem;
|
||||
text-transform: uppercase;
|
||||
padding: 0.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 64px;
|
||||
background-color: var(--white_light);
|
||||
border: 1px solid var(--black_light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button.button:hover {
|
||||
background-color: var(--white_darker);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
label.input {
|
||||
display: block;
|
||||
margin: 0 0 0.4rem 0;
|
||||
}
|
||||
input.input {
|
||||
border: 1px solid grey;
|
||||
border-radius: 0.2rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin: 0 0 0.8rem 0;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
.modal-container {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.32)
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.modal-action {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: end;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
.btn {
|
||||
background-color: white;
|
||||
}
|
||||
.btn-close:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--white_darker);
|
||||
}
|
||||
|
||||
.color-selector-container {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
width: fit-content;
|
||||
gap:3px;
|
||||
}
|
||||
.color-selector-container > .color {
|
||||
border-radius: 50%;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
margin-right: 0.2rem;
|
||||
border-style:solid;
|
||||
border-color: black;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.color-selector-container > .color.selected {
|
||||
border-style: solid;
|
||||
border-color: var(--black_light);
|
||||
}
|
||||
.color-selector-container > .color:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.color-selector-container > .color > input[type=radio] {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
label.textarea {
|
||||
display: block;
|
||||
margin: 0 0 0.4rem 0;
|
||||
}
|
||||
textarea.textarea {
|
||||
resize: none;
|
||||
width: 90%;
|
||||
height: 6em;
|
||||
border: 1px solid grey;
|
||||
border-radius: 0.2rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin: 0 0 0.8rem 0;
|
||||
}
|
||||
@@ -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,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+173
-90
@@ -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 = process.env.DB_HOST || 'server';
|
||||
const DB_PORT = process.env.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,97 +46,174 @@ 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);
|
||||
});
|
||||
//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);
|
||||
});
|
||||
//edit item
|
||||
app.put('/box/:boxId/item/:itemId', async (request, response) => {
|
||||
const boxID = request.params.boxId;
|
||||
const itemID = request.params.itemId;
|
||||
await Box.updateOne({ '_id': boxID, 'item._id': itemID }, {
|
||||
$set: {
|
||||
'item.$.name': request.body.name,
|
||||
'item.$.description': request.body.description,
|
||||
'item.$.quantity': request.body.quantity
|
||||
}
|
||||
})
|
||||
const updatedBox = await getBox(boxID);
|
||||
response.send(updatedBox);
|
||||
});
|
||||
//delete item
|
||||
app.delete('/box/:boxId/item/:itemId', async (request, response) => {
|
||||
const boxID = request.params.boxId;
|
||||
const itemID = request.params.itemId;
|
||||
await Box.update({ _id: boxID }, { $pull: { item: { _id: itemID } } });
|
||||
const box = await getBox(boxID);
|
||||
response.send(box);
|
||||
});
|
||||
//move item
|
||||
app.post('/box/item/:itemId/:boxIdTarget', async (request, response) => {
|
||||
const itemID = request.params.itemId;
|
||||
const targetBoxID = request.params.boxIdTarget;
|
||||
//the way I am doing this cannot be the best way...fix me later
|
||||
|
||||
app.post('/boxes', async (request, response) => {
|
||||
try {
|
||||
//find source box
|
||||
const box = await Box.find({ "item._id": itemID });
|
||||
//get item in source box
|
||||
const item = await Box.find({ "item._id": itemID }, { item: { $elemMatch: { _id: itemID } } });
|
||||
//remove item from source box
|
||||
await Box.updateOne({ _id: box[0]._id }, { $pull: { item: { _id: itemID } } });
|
||||
//add item to target box
|
||||
await Box.updateOne({ _id: targetBoxID }, { $push: { item: item[0].item[0] } });
|
||||
const numBoxes = await Box.countDocuments();
|
||||
const newBox = await Box.create({
|
||||
...request.body,
|
||||
sort: numBoxes,
|
||||
});
|
||||
|
||||
const boxes = await getAllBoxes();
|
||||
response.send(boxes);
|
||||
response.status(201).json(newBox);
|
||||
}
|
||||
catch (error) {
|
||||
response.json(error);
|
||||
console.error('Error creating box:', error);
|
||||
response.status(500).json({ error: 'Failed to create box' });
|
||||
}
|
||||
})
|
||||
/*------------/CONTROLLER------------*/
|
||||
});
|
||||
|
||||
// update box
|
||||
app.put('/boxes/:boxId', async (request, response) => {
|
||||
try {
|
||||
const { boxId } = request.params;
|
||||
const updatedBox = await Box.findByIdAndUpdate(
|
||||
boxId,
|
||||
{ ...request.body },
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!updatedBox) {
|
||||
return response.status(404).json({ error: 'Box not found' });
|
||||
}
|
||||
|
||||
response.status(200).json(updatedBox);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error updating box:', error);
|
||||
response.status(500).json({ error: 'Failed to update box' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// delete box
|
||||
app.delete('/boxes/:boxId', async (request, response) => {
|
||||
try {
|
||||
const { boxId } = request.params;
|
||||
const result = await Box.findByIdAndDelete(boxId);
|
||||
|
||||
if (!result) {
|
||||
return response.status(404).json({ success: false, message: 'Box not found' });
|
||||
}
|
||||
|
||||
response.status(200).json({ success: true, message: 'Box deleted', data: result });
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error deleting box:', error);
|
||||
response.status(500).json({ success: false, error: 'Failed to delete box' });
|
||||
}
|
||||
});
|
||||
|
||||
// create item
|
||||
app.post('/boxes/:boxId/items', async (request, response) => {
|
||||
try {
|
||||
const item = new Item(request.body);
|
||||
const savedItem = await item.save();
|
||||
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;
|
||||
}
|
||||
|
||||
});
|
||||
// edit item
|
||||
app.put('/boxes/:boxId/items/:itemId', async (request, response) => {
|
||||
const { boxId, itemId } = request.params;
|
||||
const { name, description, quantity } = request.body;
|
||||
try {
|
||||
const updatedBox = await Box.findOneAndUpdate(
|
||||
{ _id: boxId, 'items._id': itemId },
|
||||
{
|
||||
$set: {
|
||||
'items.$.name': name,
|
||||
'items.$.description': description,
|
||||
'items.$.quantity': quantity,
|
||||
},
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
const updatedItem = updatedBox.items.find(item => item._id.toString() === itemId);
|
||||
|
||||
if (!updatedItem) {
|
||||
return response.status(404).json({ error: 'Box or item not found' });
|
||||
}
|
||||
|
||||
response.status(200).json(updatedItem);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error updating item:', error);
|
||||
response.status(500).json({ error: 'Failed to update item' });
|
||||
}
|
||||
});
|
||||
|
||||
// delete item from box
|
||||
app.delete('/boxes/:boxId/items/:itemId', async (request, response) => {
|
||||
const { boxId, itemId } = request.params;
|
||||
|
||||
try {
|
||||
const updatedBox = await Box.findByIdAndUpdate(
|
||||
boxId,
|
||||
{
|
||||
$pull: { items: { _id: itemId } }
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!updatedBox) {
|
||||
return response.status(404).json({ error: 'Box not found' });
|
||||
}
|
||||
|
||||
response.json(updatedBox);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error deleting item:', error);
|
||||
response.status(500).json({ error: 'Failed to delete item' });
|
||||
}
|
||||
});
|
||||
|
||||
//move item
|
||||
app.patch('/items/:itemId', async (req, res) => {
|
||||
const { itemId } = req.params;
|
||||
const { boxId: targetBoxId } = req.body;
|
||||
|
||||
if (!targetBoxId) return res.status(400).json({ error: 'Target boxId is required' });
|
||||
|
||||
const item = await Item.findById(itemId);
|
||||
if (!item) return res.status(404).json({ error: 'Item not found' });
|
||||
console.log(item);
|
||||
|
||||
const sourceBox = await Box.findOne({ "items._id": item._id });
|
||||
const targetBox = await Box.findById(targetBoxId);
|
||||
if (!targetBox) return res.status(404).json({ error: 'Target box not found' });
|
||||
if (!sourceBox) return res.status(404).json({ error: 'Source box not found' });
|
||||
|
||||
|
||||
await Box.updateOne(
|
||||
{ _id: sourceBox._id },
|
||||
{ $pull: { items: item } }
|
||||
);
|
||||
|
||||
await Box.updateOne(
|
||||
{ _id: targetBoxId },
|
||||
{ $push: { items: item } }
|
||||
);
|
||||
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
/*--------/CONTROLLER------------*/
|
||||
|
||||
const getAllBoxes = async (queryParameters) => {
|
||||
queryParameters = queryParameters ?? {};
|
||||
@@ -145,4 +224,8 @@ const getAllBoxes = async (queryParameters) => {
|
||||
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