nicholas/storage-manager#1: add generic menu; configure more styles
This commit is contained in:
+14
-14
@@ -1,33 +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'
|
||||
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'] },
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
files: ["**/*.{js,jsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaVersion: "latest",
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
"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',
|
||||
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
+14
-16
@@ -1,18 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<!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>
|
||||
|
||||
<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>
|
||||
<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
+97
@@ -24,6 +24,7 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@@ -3173,6 +3174,102 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.6.11",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz",
|
||||
"integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "*",
|
||||
"@prettier/plugin-pug": "*",
|
||||
"@shopify/prettier-plugin-liquid": "*",
|
||||
"@trivago/prettier-plugin-sort-imports": "*",
|
||||
"@zackad/prettier-plugin-twig": "*",
|
||||
"prettier": "^3.0",
|
||||
"prettier-plugin-astro": "*",
|
||||
"prettier-plugin-css-order": "*",
|
||||
"prettier-plugin-import-sort": "*",
|
||||
"prettier-plugin-jsdoc": "*",
|
||||
"prettier-plugin-marko": "*",
|
||||
"prettier-plugin-multiline-arrays": "*",
|
||||
"prettier-plugin-organize-attributes": "*",
|
||||
"prettier-plugin-organize-imports": "*",
|
||||
"prettier-plugin-sort-imports": "*",
|
||||
"prettier-plugin-style-order": "*",
|
||||
"prettier-plugin-svelte": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@ianvs/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@shopify/prettier-plugin-liquid": {
|
||||
"optional": true
|
||||
},
|
||||
"@trivago/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@zackad/prettier-plugin-twig": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-astro": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-css-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-import-sort": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-jsdoc": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-marko": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-multiline-arrays": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-attributes": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-style-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -1,23 +1,23 @@
|
||||
import axios from 'axios';
|
||||
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',
|
||||
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',
|
||||
},
|
||||
baseURL: baseApiUrl,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
console.error('API error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error("API error:", error);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { api } from './api';
|
||||
export * from './apiResponse.types';
|
||||
export { api } from "./api";
|
||||
export * from "./apiResponse.types";
|
||||
|
||||
+93
-91
@@ -1,102 +1,104 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getAllBoxes, Box, BoxCard, BoxFormModal } from './features/box';
|
||||
import { useEffect, useState } from "react";
|
||||
import { getAllBoxes, Box, BoxCard, BoxFormModal } from "./features/box";
|
||||
import { Plus, Search } from "./icons";
|
||||
|
||||
const App = () => {
|
||||
const [boxes, setBoxes] = useState<Box[]>([]);
|
||||
const handleCreateBox = (newBox: Box) => {
|
||||
setBoxes((boxes) => [...boxes, newBox]);
|
||||
};
|
||||
const handleDeleteBox = (id: string) => {
|
||||
setBoxes((boxes) => boxes.filter(box => box._id !== id));
|
||||
}
|
||||
const [boxes, setBoxes] = useState<Box[]>([]);
|
||||
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);
|
||||
};
|
||||
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);
|
||||
};
|
||||
const filteredBoxes = boxes.filter(box => {
|
||||
const ss = searchString.toLowerCase();
|
||||
// box filter
|
||||
const [searchString, setSearch] = useState("");
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(event.target.value);
|
||||
};
|
||||
const filteredBoxes = boxes.filter((box) => {
|
||||
const ss = searchString.toLowerCase();
|
||||
|
||||
const matchingItems = box.items?.filter(item =>
|
||||
item.name.toLowerCase().includes(ss) ||
|
||||
item.description.toLowerCase().includes(ss)
|
||||
);
|
||||
|
||||
return (
|
||||
box.name.toLowerCase().includes(ss) ||
|
||||
box.description.toLowerCase().includes(ss) ||
|
||||
matchingItems && matchingItems.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
}, []);
|
||||
const matchingItems = box.items?.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(ss) ||
|
||||
item.description.toLowerCase().includes(ss),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BoxFormModal
|
||||
mode="create"
|
||||
isOpen={isBoxFormModalOpen}
|
||||
onSubmitBox={handleCreateBox}
|
||||
onClose={handleBoxFormModalClose}
|
||||
/>
|
||||
|
||||
<div className="">
|
||||
<label htmlFor="box-filter" className="text-sm font-medium text-gray-700">Search</label>
|
||||
<input
|
||||
onChange={handleSearchChange}
|
||||
spellCheck="false"
|
||||
type="search"
|
||||
id="box-filter"
|
||||
name="filter"
|
||||
className=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<button
|
||||
onClick={handleBoxFormModalOpen}
|
||||
className=""
|
||||
>
|
||||
New Box
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='box-card-list-container'>
|
||||
<ul className='box-card-list'>
|
||||
{filteredBoxes.map((item) => (
|
||||
<BoxCard
|
||||
key={item._id}
|
||||
initialBox={item}
|
||||
onDelete={handleDeleteBox}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
box.name.toLowerCase().includes(ss) ||
|
||||
box.description.toLowerCase().includes(ss) ||
|
||||
(matchingItems && matchingItems.length > 0)
|
||||
);
|
||||
});
|
||||
|
||||
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="utility-container">
|
||||
<button onClick={handleBoxFormModalOpen} className="group button-icon">
|
||||
{<Plus height="32" width="32" />}
|
||||
</button>
|
||||
<div className="flex justify-center p-2 align-middle">
|
||||
<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">
|
||||
<ul className="box-card-list">
|
||||
{filteredBoxes.map((item) => (
|
||||
<BoxCard
|
||||
key={item._id}
|
||||
initialBox={item}
|
||||
onDelete={handleDeleteBox}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
+21
-12
@@ -1,17 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState } from "react";
|
||||
|
||||
const ColorSelect = ({ colorData, handler, boxColor }) => {
|
||||
const [style, setStyle] = useState(colorData.defaultStyle);
|
||||
const [style, setStyle] = useState(colorData.defaultStyle);
|
||||
|
||||
const handleHover = (isHover) => {
|
||||
setStyle(isHover ? colorData.lightStyle : 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>
|
||||
)
|
||||
}
|
||||
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;
|
||||
export default ColorSelect;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Item } from '../item/item.types';
|
||||
import { Item } from "../item/item.types";
|
||||
|
||||
export interface Box {
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
items?: Item[];
|
||||
}
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
items?: Item[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Box, BoxFormModal, deleteBox } from '../';
|
||||
import { Item, ItemCard, ItemFormModal } from '../../item';
|
||||
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 "../../generic-menu/GenericMenu";
|
||||
|
||||
interface BoxCardProps {
|
||||
initialBox: Box;
|
||||
@@ -8,7 +10,6 @@ interface BoxCardProps {
|
||||
}
|
||||
|
||||
export const BoxCard: React.FC<BoxCardProps> = ({ initialBox, onDelete }) => {
|
||||
|
||||
const [items, setItems] = useState<Item[]>(initialBox.items!);
|
||||
const [box, setBox] = useState<Box>(initialBox);
|
||||
|
||||
@@ -16,103 +17,115 @@ export const BoxCard: React.FC<BoxCardProps> = ({ initialBox, onDelete }) => {
|
||||
const [isItemFormModalOpen, setItemFormModalOpen] = useState(false);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
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)
|
||||
) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleMenu = () => {
|
||||
setIsMenuOpen(prevState => !prevState);
|
||||
setIsMenuOpen((prevState) => !prevState);
|
||||
};
|
||||
|
||||
const handleOpenItemFormModal = () => {
|
||||
setItemFormModalOpen(true);
|
||||
}
|
||||
};
|
||||
const handleCloseItemFormModal = () => {
|
||||
setItemFormModalOpen(false);
|
||||
};
|
||||
|
||||
const handleOpenBoxFormModal = () => {
|
||||
setBoxFormModalOpen(true);
|
||||
}
|
||||
};
|
||||
const handleCloseBoxFormModal = () => {
|
||||
setBoxFormModalOpen(false);
|
||||
};
|
||||
|
||||
|
||||
const handleCreateItem = (newItem: Item) => {
|
||||
setItems((items) => [...items, newItem]);
|
||||
};
|
||||
|
||||
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) {
|
||||
onDelete(box._id);
|
||||
} else {
|
||||
console.error('No data returned in the response');
|
||||
console.error("No data returned in the response");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleDeleteItem = (id: string) => {
|
||||
setItems((items) => items.filter(item => item._id !== id));
|
||||
}
|
||||
|
||||
setItems((items) => items.filter((item) => item._id !== id));
|
||||
};
|
||||
|
||||
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!} />
|
||||
|
||||
<li className='box-card'>
|
||||
<div>
|
||||
<div>
|
||||
<h2 className='box-card-title'>{box.name}</h2>
|
||||
<h3 className='box-card-description'>{box.description}</h3>
|
||||
</div>
|
||||
<button ref={menuButtonRef} onClick={toggleMenu}>menu</button>
|
||||
{isMenuOpen && (
|
||||
<div ref={menuRef}>
|
||||
<ul>
|
||||
<li><button onClick={handleClickEditBox}>edit</button></li>
|
||||
<li><button onClick={handleDeleteBox}>delete</button></li>
|
||||
</ul>
|
||||
<BoxFormModal
|
||||
mode="edit"
|
||||
initialData={box}
|
||||
isOpen={isBoxFormModalOpen}
|
||||
onSubmitBox={handleUpdateBox}
|
||||
onClose={handleCloseBoxFormModal}
|
||||
/>
|
||||
<ItemFormModal
|
||||
mode="create"
|
||||
isOpen={isItemFormModalOpen}
|
||||
onSubmitItem={handleCreateItem}
|
||||
onClose={handleCloseItemFormModal}
|
||||
boxId={box._id!}
|
||||
/>
|
||||
<li 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>
|
||||
)}
|
||||
</div>
|
||||
<div className='item-card-list-container'>
|
||||
<ul className='item-card-list'>
|
||||
{items?.map((item) => (
|
||||
<ItemCard key={item._id} boxId={box._id!} initialItem={item} onDelete={handleDeleteItem} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={handleOpenItemFormModal}>add</button>
|
||||
</div>
|
||||
</li>
|
||||
<GenericMenu
|
||||
onToggle={toggleMenu}
|
||||
menuItems={menuItems}
|
||||
isOpen={isMenuOpen}
|
||||
Icon={Ellipsis}
|
||||
iconProps={{ height: 24, width: 24 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="item-card-list-container">
|
||||
<ul className="item-card-list">
|
||||
{items?.map((item) => (
|
||||
<ItemCard
|
||||
key={item._id}
|
||||
boxId={box._id!}
|
||||
initialItem={item}
|
||||
onDelete={handleDeleteItem}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="group button-icon"
|
||||
onClick={handleOpenItemFormModal}
|
||||
>
|
||||
<Plus height="24" width="24" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,61 +1,96 @@
|
||||
import { useState } from 'react';
|
||||
import Modal from '../../../modal';
|
||||
import { Box, createBox, updateBox } from '..';
|
||||
import { Item } from '../../item';
|
||||
import { useState } from "react";
|
||||
import Modal from "../../../modal";
|
||||
import { Box, createBox, updateBox } from "..";
|
||||
import { Item } from "../../item";
|
||||
|
||||
interface BoxFormModalProps {
|
||||
mode: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmitBox: (box: Box) => void;
|
||||
initialData?: Partial<Box>;
|
||||
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();
|
||||
}
|
||||
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,
|
||||
};
|
||||
|
||||
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 htmlFor="box-name">name</label>
|
||||
<input value={boxName} onChange={(e) => setBoxName(e.target.value)} id='box-name' name='name' type='text' />
|
||||
<label htmlFor="box-description">description</label>
|
||||
<input value={boxDescription} onChange={(e) => setBoxDescription(e.target.value)} id='box-description' name='description' type='text' />
|
||||
<div>
|
||||
<button type="submit">Submit</button>
|
||||
<button type="button" onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
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>
|
||||
<button className="button" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
<button className="button" type="button" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './box.types';
|
||||
export * from "./box.types";
|
||||
|
||||
export * from './components/BoxCard';
|
||||
export * from './components/BoxFormModal';
|
||||
export * from './services/BoxService';
|
||||
export * from "./components/BoxCard";
|
||||
export * from "./components/BoxFormModal";
|
||||
export * from "./services/BoxService";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { api, ApiResponse } from '../../../api';
|
||||
import { Box } from '..';
|
||||
import { api, ApiResponse } from "../../../api";
|
||||
import { Box } from "..";
|
||||
|
||||
const getAllBoxes = async (): Promise<ApiResponse<Box[]>> => {
|
||||
try {
|
||||
const response = await api.get<Box[]>('/boxes');
|
||||
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';
|
||||
error instanceof Error ? error.message : "Could not fetch boxes";
|
||||
return { success: false, data: [], error: message };
|
||||
}
|
||||
};
|
||||
@@ -20,45 +20,45 @@ const createBox = async (boxData: Box): Promise<ApiResponse<Box>> => {
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating box:', error);
|
||||
console.error("Error creating box:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Could not create box',
|
||||
error: "Could not create box",
|
||||
};
|
||||
}
|
||||
}
|
||||
const updateBox = async (boxId: string, boxData: Box): Promise<ApiResponse<Box>> => {
|
||||
};
|
||||
const updateBox = async (
|
||||
boxId: string,
|
||||
boxData: Box,
|
||||
): Promise<ApiResponse<Box>> => {
|
||||
try {
|
||||
console.log(boxData);
|
||||
const response = await api.put<Box>(`/boxes/${boxId}`, boxData);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating box:', error);
|
||||
console.error("Error updating box:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Could not update box',
|
||||
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);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error deleting box:", error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Could not delete box'
|
||||
}
|
||||
error: "Could not delete box",
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export { getAllBoxes, createBox, updateBox, deleteBox }
|
||||
export { getAllBoxes, createBox, updateBox, deleteBox };
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { Ellipsis, Garbage } from "../../icons";
|
||||
|
||||
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="relative">
|
||||
<button
|
||||
className="group button-icon"
|
||||
ref={menuButtonRef}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Icon {...iconProps} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div ref={menuRef} className="menu-container">
|
||||
<ul className={`menu`}>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +1,70 @@
|
||||
import { useState } from 'react';
|
||||
import { Item, ItemFormModal, deleteItem } from '../';
|
||||
import { useState } from "react";
|
||||
import { Item, ItemFormModal, deleteItem } from "../";
|
||||
import { Ellipsis, Garbage, Pencil } from "../../../icons";
|
||||
import { GenericMenu } from "../../generic-menu/GenericMenu";
|
||||
|
||||
interface ItemCardProps {
|
||||
boxId: string;
|
||||
initialItem: Item;
|
||||
onDelete: (id: string) => void;
|
||||
boxId: string;
|
||||
initialItem: Item;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ItemCard: React.FC<ItemCardProps> = ({ boxId, initialItem, onDelete }) => {
|
||||
const [isItemFormModalOpen, setIsItemFormModalOpen] = useState(false);
|
||||
const [item, setItem] = useState(initialItem);
|
||||
export const ItemCard: React.FC<ItemCardProps> = ({
|
||||
boxId,
|
||||
initialItem,
|
||||
onDelete,
|
||||
}) => {
|
||||
const [isItemFormModalOpen, setIsItemFormModalOpen] = useState(false);
|
||||
const [item, setItem] = useState(initialItem);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const toggleMenu = () => {
|
||||
setIsMenuOpen((prevState) => !prevState);
|
||||
};
|
||||
const handleCloseItemFormModal = () => {
|
||||
setIsItemFormModalOpen(false);
|
||||
};
|
||||
const handleClickEditItem = async () => {
|
||||
setIsItemFormModalOpen(true);
|
||||
};
|
||||
const handleUpdateItem = (updatedItem: Item) => {
|
||||
setItem(updatedItem);
|
||||
};
|
||||
|
||||
|
||||
const handleCloseItemFormModal = () => {
|
||||
setIsItemFormModalOpen(false);
|
||||
const handleDeleteItem = async () => {
|
||||
if (item._id != null) {
|
||||
const response = await deleteItem(boxId, item._id);
|
||||
if (response.success) {
|
||||
onDelete(item._id);
|
||||
} else {
|
||||
console.error("No data returned in the response");
|
||||
}
|
||||
}
|
||||
const handleClickEditItem = async () => {
|
||||
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) {
|
||||
onDelete(item._id);
|
||||
} else {
|
||||
console.error('No data returned in the response');
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li>
|
||||
<ItemFormModal mode="edit" isOpen={isItemFormModalOpen} onSubmitItem={handleUpdateItem} onClose={handleCloseItemFormModal} boxId={boxId} initialData={item} />
|
||||
<span>{item.name}</span>
|
||||
<button onClick={handleClickEditItem} >edit</button>
|
||||
<button onClick={handleDeleteItem} >remove</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
const menuItems = [
|
||||
{ label: "Edit", icon: Pencil, onClick: handleClickEditItem },
|
||||
{ label: "Delete", icon: Garbage, onClick: handleDeleteItem },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<ItemFormModal
|
||||
mode="edit"
|
||||
isOpen={isItemFormModalOpen}
|
||||
onSubmitItem={handleUpdateItem}
|
||||
onClose={handleCloseItemFormModal}
|
||||
boxId={boxId}
|
||||
initialData={item}
|
||||
/>
|
||||
<li>
|
||||
<div className="item-container">
|
||||
<h3 className="item-name">{item.name}</h3>
|
||||
<GenericMenu
|
||||
isOpen={isMenuOpen}
|
||||
menuItems={menuItems}
|
||||
onToggle={toggleMenu}
|
||||
Icon={Ellipsis}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,72 +1,116 @@
|
||||
import { useState } from 'react';
|
||||
import Modal from '../../../modal';
|
||||
import { Item, createItem, updateItem } from '..';
|
||||
import { createBox } from '../../box';
|
||||
import { useState } from "react";
|
||||
import Modal from "../../../modal";
|
||||
import { Item, createItem, updateItem } from "..";
|
||||
import { createBox } from "../../box";
|
||||
|
||||
interface ItemFormModalProps {
|
||||
mode: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmitItem: (item: Item) => void;
|
||||
boxId: string;
|
||||
initialData?: Partial<Item>;
|
||||
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);
|
||||
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 handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const isEdit = mode === 'edit';
|
||||
|
||||
const itemData: Item = {
|
||||
name: itemName as string,
|
||||
description: itemDescription as string,
|
||||
quantity: itemQuantity as number,
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
const isEdit = mode === "edit";
|
||||
|
||||
const itemData: Item = {
|
||||
name: itemName as string,
|
||||
description: itemDescription as string,
|
||||
quantity: itemQuantity as number,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}>
|
||||
<form onSubmit={handleSubmit} id='form'>
|
||||
<input value="" id='id' name='id' readOnly={true} hidden={true} />
|
||||
<h1>Edit Item</h1>
|
||||
<fieldset>
|
||||
try {
|
||||
const response = isEdit
|
||||
? await updateItem(boxId!, initialData?._id!, itemData)
|
||||
: await createItem(itemData, boxId);
|
||||
|
||||
<label htmlFor="item-name">name</label>
|
||||
<input value={itemName} onChange={(e) => setItemName(e.currentTarget.value)} id='item-name' name='name' type='text' />
|
||||
if (!response.success || !response.data) {
|
||||
console.error(
|
||||
"Item operation failed:",
|
||||
response.error ?? "Unknown error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
<label htmlFor="item-description">description</label>
|
||||
<input value={itemDescription} onChange={(e) => setItemDescription(e.currentTarget.value)}id='item-description' name='description' type='text' />
|
||||
onSubmitItem(response.data);
|
||||
} catch (err) {
|
||||
console.error(`Error during ${isEdit ? "update" : "create"}:`, err);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
<label htmlFor="item-quantity">quantity</label>
|
||||
<input value={itemQuantity} onChange={(e) => setItemQuantity(parseInt(e.currentTarget.value, 10))}id='item-quantity' name='quantity' type='number' />
|
||||
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-label" htmlFor="item-name">
|
||||
name
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={itemName}
|
||||
onChange={(e) => setItemName(e.currentTarget.value)}
|
||||
id="item-name"
|
||||
name="name"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<button type="submit">Submit</button>
|
||||
<button type="button" onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<label className="input-label" htmlFor="item-description">
|
||||
description
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={itemDescription}
|
||||
onChange={(e) => setItemDescription(e.currentTarget.value)}
|
||||
id="item-description"
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
<label className="input-label" htmlFor="item-quantity">
|
||||
quantity
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={itemQuantity}
|
||||
onChange={(e) =>
|
||||
setItemQuantity(parseInt(e.currentTarget.value, 10))
|
||||
}
|
||||
id="item-quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<button className="button" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
<button className="button" type="button" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './item.types';
|
||||
export * from "./item.types";
|
||||
|
||||
export * from './components/ItemCard';
|
||||
export * from './components/ItemFormModal';
|
||||
export * from './services/ItemService';
|
||||
export * from "./components/ItemCard";
|
||||
export * from "./components/ItemFormModal";
|
||||
export * from "./services/ItemService";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface Item {
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
}
|
||||
_id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { api, ApiResponse } from '../../../api';
|
||||
import { Item } from '..';
|
||||
import { api, ApiResponse } from "../../../api";
|
||||
import { Item } from "..";
|
||||
|
||||
const createItem = async (itemData: Item, boxId: string,): Promise<ApiResponse<Item>> => {
|
||||
const createItem = async (
|
||||
itemData: Item,
|
||||
boxId: string,
|
||||
): Promise<ApiResponse<Item>> => {
|
||||
try {
|
||||
const response = await api.post<Item>(`/boxes/${boxId}/items`, itemData);
|
||||
return {
|
||||
@@ -9,43 +12,51 @@ const createItem = async (itemData: Item, boxId: string,): Promise<ApiResponse<I
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating item:', error);
|
||||
console.error("Error creating item:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Could not create item',
|
||||
error: "Could not create item",
|
||||
};
|
||||
}
|
||||
}
|
||||
const updateItem = async (boxId: string, itemId: string, itemData: Item): Promise<ApiResponse<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);
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error("Error updating item:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Could not update item',
|
||||
error: "Could not update item",
|
||||
};
|
||||
}
|
||||
}
|
||||
const deleteItem = async (boxId: string, itemId: string): Promise<ApiResponse<null>> => {
|
||||
};
|
||||
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);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error deleting item:", error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Could not delete item'
|
||||
}
|
||||
error: "Could not delete item",
|
||||
};
|
||||
}
|
||||
}
|
||||
export { createItem, updateItem, deleteItem }
|
||||
};
|
||||
export { createItem, updateItem, deleteItem };
|
||||
|
||||
@@ -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";
|
||||
+83
-9
@@ -1,4 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
* {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -14,22 +17,93 @@ code {
|
||||
monospace;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-base: #121212;
|
||||
--color-highlight: #1f1f1f;
|
||||
--color-highlight-2: #282828;
|
||||
--color-subdued: #b3b3b3;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-black;
|
||||
}
|
||||
|
||||
.utility-container {
|
||||
@apply flex py-4 w-full justify-center;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
@apply cursor-pointer rounded-full p-2 hover:bg-highlight;
|
||||
}
|
||||
.icon {
|
||||
@apply fill-subdued group-hover:fill-white;
|
||||
}
|
||||
|
||||
.box-card-list-container {
|
||||
@apply flex
|
||||
@apply flex p-4;
|
||||
}
|
||||
.box-card-list {
|
||||
@apply flex flex-wrap space-x-4 p-0 m-0;
|
||||
@apply m-0 flex flex-wrap space-x-4 p-0;
|
||||
}
|
||||
.box-card {
|
||||
@apply bg-white p-6 m-2 min-w-sm max-h-128 rounded-lg shadow-md hover:shadow-lg transition-all;
|
||||
@apply m-2 max-h-128 min-w-sm rounded-md bg-base p-6 text-subdued;
|
||||
}
|
||||
.box-card-header {
|
||||
@apply flex justify-between;
|
||||
}
|
||||
|
||||
|
||||
.box-card-title {
|
||||
@apply text-xl font-semibold text-gray-800 mb-2;
|
||||
@apply mb-2 text-xl font-bold text-white;
|
||||
}
|
||||
.box-card-description {
|
||||
@apply text-sm text-gray-600 mb-4;
|
||||
.box-card-subtitle {
|
||||
@apply mb-4 text-sm font-semibold text-subdued;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
@apply absolute right-0 z-10 mt-2 w-36 rounded bg-highlight p-1 shadow-[0_16px_24px_rgba(0,0,0,0.3),_0_6px_8px_rgba(0,0,0,0.2)];
|
||||
}
|
||||
.menu {
|
||||
@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-container {
|
||||
@apply flex justify-between p-2 hover:bg-highlight;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@apply fixed inset-0 z-20 flex items-center justify-center bg-black/80;
|
||||
}
|
||||
.modal {
|
||||
@apply w-full max-w-lg rounded-xl bg-highlight p-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-blue-600 px-4 py-2 text-white hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:outline-none;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
@apply relative w-full max-w-sm focus-within:border-white focus-within:fill-white;
|
||||
}
|
||||
|
||||
.input-search {
|
||||
@apply box-border w-full rounded-full border-1 border-subdued py-2 pr-4 pl-10 group-focus-within:border-1 group-focus-within:border-white focus:outline-none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './app';
|
||||
import './index.css';
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./app";
|
||||
import "./index.css";
|
||||
|
||||
const rootElement = document.getElementById('root') as HTMLElement;
|
||||
const rootElement = document.getElementById("root") as HTMLElement;
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -9,11 +9,9 @@ const Modal: React.FC<ModalProps> = ({ isOpen, children }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
<div className="modal-container">
|
||||
<div className="modal">
|
||||
<div className="modal-children">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+10
-10
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+4
-4
@@ -1,11 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
VITE_API_HOST: string;
|
||||
VITE_API_PORT: string;
|
||||
VITE_API_PROTOCOL: string
|
||||
VITE_API_HOST: string;
|
||||
VITE_API_PORT: string;
|
||||
VITE_API_PROTOCOL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
"jsx": "react-jsx",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
}
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
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(),
|
||||
],
|
||||
})
|
||||
plugins: [react(), tailwindcss()],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user