feat: new screens and docker setup
This commit is contained in:
parent
c20e04557b
commit
31600e5f1b
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
*.md
|
||||||
|
.git
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Step 1: Build the React app
|
||||||
|
FROM node:18-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Step 2: Serve with NGINX
|
||||||
|
FROM nginx:stable-alpine
|
||||||
|
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
6
docker-compose.yml
Normal file
6
docker-compose.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
12
nginx.conf
Normal file
12
nginx.conf
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 404 /index.html;
|
||||||
|
}
|
||||||
@ -11,9 +11,11 @@
|
|||||||
will-change: filter;
|
will-change: filter;
|
||||||
transition: filter 300ms;
|
transition: filter 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo:hover {
|
.logo:hover {
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo.react:hover {
|
.logo.react:hover {
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
}
|
}
|
||||||
@ -22,6 +24,7 @@
|
|||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import Box from '@mui/material/Box';
|
|||||||
import Products from './private/products/Products';
|
import Products from './private/products/Products';
|
||||||
import Clients from './private/clients/Clients';
|
import Clients from './private/clients/Clients';
|
||||||
import Providers from './private/providers/Providers';
|
import Providers from './private/providers/Providers';
|
||||||
|
import Categories from './private/categories/Categories';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [zone, setZone] = useState('public'); // Could be 'public' | 'restricted' | 'private'
|
const [zone, setZone] = useState('public'); // Could be 'public' | 'restricted' | 'private'
|
||||||
@ -31,7 +32,8 @@ function App() {
|
|||||||
|
|
||||||
{zone === 'public' && currentView === 'Products' && <Products />}
|
{zone === 'public' && currentView === 'Products' && <Products />}
|
||||||
{zone === 'public' && currentView === 'Clients' && <Clients />}
|
{zone === 'public' && currentView === 'Clients' && <Clients />}
|
||||||
{zone === 'public' && currentView === 'Providers' && <Providers />}
|
{zone === 'public' && currentView === 'Providers' && <Providers />}
|
||||||
|
{zone === 'public' && currentView === 'Categories' && <Categories />}
|
||||||
</Box>
|
</Box>
|
||||||
<Footer zone={zone} />
|
<Footer zone={zone} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,9 +1,27 @@
|
|||||||
import { Drawer, List, ListItem, ListItemText, useMediaQuery } from '@mui/material';
|
import { Drawer, List, ListItem, ListItemText, ListItemIcon, Avatar, Typography, Box, useMediaQuery } from '@mui/material';
|
||||||
|
import CategoryIcon from '@mui/icons-material/Category';
|
||||||
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
|
import InventoryIcon from '@mui/icons-material/Inventory';
|
||||||
|
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
|
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
const menuOptions = {
|
const menuOptions = {
|
||||||
public: ['Home', 'Explore', 'Contact'],
|
public: [
|
||||||
restricted: ['Dashboard', 'Projects', 'Support'],
|
{ text: 'Categories', icon: <CategoryIcon /> },
|
||||||
private: ['Products', 'Clients', 'Providers', 'Logout'],
|
{ text: 'Clients', icon: <PeopleIcon /> },
|
||||||
|
{ text: 'Products', icon: <InventoryIcon /> },
|
||||||
|
{ text: 'Providers', icon: <LocalShippingIcon /> },
|
||||||
|
{ text: 'Logout', icon: <ExitToAppIcon /> },
|
||||||
|
],
|
||||||
|
restricted: [],
|
||||||
|
private: [
|
||||||
|
{ text: 'Categories', icon: <CategoryIcon /> },
|
||||||
|
{ text: 'Clients', icon: <PeopleIcon /> },
|
||||||
|
{ text: 'Products', icon: <InventoryIcon /> },
|
||||||
|
{ text: 'Providers', icon: <LocalShippingIcon /> },
|
||||||
|
{ text: 'Logout', icon: <ExitToAppIcon /> },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MenuDrawer({ zone = 'public', open, onClose, onSelect }) {
|
export default function MenuDrawer({ zone = 'public', open, onClose, onSelect }) {
|
||||||
@ -16,15 +34,27 @@ export default function MenuDrawer({ zone = 'public', open, onClose, onSelect })
|
|||||||
sx: {
|
sx: {
|
||||||
backgroundColor: '#40120EFF',
|
backgroundColor: '#40120EFF',
|
||||||
width: isMobile ? '100vw' : 250,
|
width: isMobile ? '100vw' : 250,
|
||||||
|
color: '#DFCCBCFF'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<List sx={{ width: isMobile ? '100vw' : 250, marginTop: 14 }}>
|
<Box textAlign="center" p={3}>
|
||||||
{items.map((text, index) => (
|
<Avatar
|
||||||
|
src="/favicon.png"
|
||||||
|
alt="User"
|
||||||
|
sx={{ width: 64, height: 64, mx: 'auto', mb: 1 }}
|
||||||
|
/>
|
||||||
|
<Typography variant="subtitle1" fontWeight={600}>Fendi Casa</Typography>
|
||||||
|
<Typography variant="body2">Administrator</Typography>
|
||||||
|
</Box>
|
||||||
|
<List sx={{ width: isMobile ? '100vw' : 250, marginTop: 2 }}>
|
||||||
|
{items.map(({ text, icon }, index) => (
|
||||||
<ListItem key={index} onClick={() => {
|
<ListItem key={index} onClick={() => {
|
||||||
onClose(); // Close drawer
|
onClose(); // Close drawer
|
||||||
onSelect?.(text); // Notify parent of selected item
|
onSelect?.(text); // Notify parent of selected item
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
|
<ListItemIcon sx={{ color: '#DFCCBCFF' }}>{icon}</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={text}
|
primary={text}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@ -41,4 +71,4 @@ export default function MenuDrawer({ zone = 'public', open, onClose, onSelect })
|
|||||||
</List>
|
</List>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/private/categories/AddOrEditCategoryForm.jsx
Normal file
64
src/private/categories/AddOrEditCategoryForm.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Box, Button, TextField, Typography, Paper } from '@mui/material';
|
||||||
|
|
||||||
|
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) {
|
||||||
|
const [category, setCategory] = useState({
|
||||||
|
name: '',
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
setCategory(initialData);
|
||||||
|
} else {
|
||||||
|
setCategory({ name: '', description: '' });
|
||||||
|
}
|
||||||
|
}, [initialData]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setCategory((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (onAdd) {
|
||||||
|
onAdd(category);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ px: 2, py: 3 }}>
|
||||||
|
<Paper elevation={0} sx={{ p: 3, bgcolor: '#f9f9f9', borderRadius: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Category Details
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
value={category.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Description"
|
||||||
|
name="description"
|
||||||
|
value={category.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
margin="normal"
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<Box display="flex" justifyContent="flex-end" gap={1} mt={3}>
|
||||||
|
<Button onClick={onCancel} className="button-transparent">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleSubmit} className="button-gold">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
142
src/private/categories/Categories.jsx
Normal file
142
src/private/categories/Categories.jsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import SectionContainer from '../../components/SectionContainer.jsx';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
|
import { Typography, Button, Dialog, DialogTitle, DialogContent, IconButton, Box } from '@mui/material';
|
||||||
|
import AddOrEditCategoryForm from './AddOrEditCategoryForm.jsx';
|
||||||
|
|
||||||
|
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||||
|
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
|
||||||
|
import '../../App.css';
|
||||||
|
|
||||||
|
const columnsBase = [
|
||||||
|
{ field: 'name', headerName: 'Name', flex: 1 },
|
||||||
|
{ field: 'description', headerName: 'Description', flex: 2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Categories({ children, maxWidth = 'lg', sx = {} }) {
|
||||||
|
const [rows, setRows] = useState([
|
||||||
|
{ id: 1, name: 'Fabrics', description: 'Textile materials including silk, cotton, and synthetics.' },
|
||||||
|
{ id: 2, name: 'Leather Goods', description: 'Leather-based components for luxury goods.' },
|
||||||
|
{ id: 3, name: 'Metal Accessories', description: 'Buttons, zippers, and hardware in metal.' },
|
||||||
|
{ id: 4, name: 'Embellishments', description: 'Decorative materials such as beads and sequins.' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [editingCategory, setEditingCategory] = useState(null);
|
||||||
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
const [rowToDelete, setRowToDelete] = useState(null);
|
||||||
|
|
||||||
|
const handleAddOrEditCategory = (category) => {
|
||||||
|
if (editingCategory) {
|
||||||
|
setRows(rows.map((row) => (row.id === editingCategory.id ? { ...editingCategory, ...category } : row)));
|
||||||
|
} else {
|
||||||
|
const id = rows.length + 1;
|
||||||
|
setRows([...rows, { id, ...category }]);
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
setEditingCategory(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (params) => {
|
||||||
|
setEditingCategory(params.row);
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (row) => {
|
||||||
|
setRowToDelete(row);
|
||||||
|
setConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
setRows(rows.filter((row) => row.id !== rowToDelete.id));
|
||||||
|
setRowToDelete(null);
|
||||||
|
setConfirmOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
...columnsBase,
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
headerName: '',
|
||||||
|
width: 130,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="flex-end" height="100%" gap={2}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#DFCCBC',
|
||||||
|
color: '#26201A',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#C2B2A4',
|
||||||
|
},
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleEditClick(params)}
|
||||||
|
>
|
||||||
|
<EditRoundedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#FBE9E7',
|
||||||
|
color: '#C62828',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#EF9A9A',
|
||||||
|
},
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleDeleteClick(params.row)}
|
||||||
|
>
|
||||||
|
<DeleteRoundedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContainer sx={{ width: '100%' }}>
|
||||||
|
<Typography variant="h4" gutterBottom color='#26201AFF'>
|
||||||
|
Categories
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Dialog open={open} onClose={() => { setOpen(false); setEditingCategory(null); }} fullWidth>
|
||||||
|
<DialogTitle>{editingCategory ? 'Edit Category' : 'Add Category'}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<AddOrEditCategoryForm onAdd={handleAddOrEditCategory} initialData={editingCategory} onCancel={() => { setOpen(false); setEditingCategory(null); }} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}>
|
||||||
|
<DialogTitle>Confirm Delete</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography>
|
||||||
|
Are you sure you want to delete <strong>{rowToDelete?.name}</strong>?
|
||||||
|
</Typography>
|
||||||
|
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
|
||||||
|
<Button onClick={() => setConfirmOpen(false)} className='button-transparent'>Cancel</Button>
|
||||||
|
<Button variant="contained" onClick={confirmDelete} className="button-gold">Delete</Button>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Box mt={2}>
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={5}
|
||||||
|
rowsPerPageOptions={[5]}
|
||||||
|
getRowSpacing={() => ({ top: 8, bottom: 8 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box display="flex" justifyContent="flex-end" mt={2}>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(true)} className="button-gold">
|
||||||
|
Add Category
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SectionContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -66,7 +66,7 @@ const theme = createTheme({
|
|||||||
root: {
|
root: {
|
||||||
transition: 'background-color 0.2s ease-in-out',
|
transition: 'background-color 0.2s ease-in-out',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#AA7665FF',
|
backgroundColor: '#AA7665FF',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user