чюпеп
This commit is contained in:
18
db.json
Normal file
18
db.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"deliveries": [
|
||||
{
|
||||
"id": "578a",
|
||||
"trackingNumber": "IVN012021",
|
||||
"destination": "гоголя10",
|
||||
"status": "Доставлено",
|
||||
"customer": "Парт петрович wwww"
|
||||
},
|
||||
{
|
||||
"id": "0e0f",
|
||||
"trackingNumber": "IVN123456",
|
||||
"destination": "Москва",
|
||||
"status": "В пути",
|
||||
"customer": "1Иванов Иван"
|
||||
}
|
||||
]
|
||||
}
|
||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -9,7 +9,8 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
@@ -3169,6 +3170,53 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
|
||||
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
|
||||
"integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -3319,6 +3367,12 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"server": "json-server --watch db.json --port 3001"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
|
||||
140
src/App.css
140
src/App.css
@@ -133,4 +133,142 @@
|
||||
|
||||
.form-actions button[type="button"]:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-link, .back-link {
|
||||
padding: 8px 16px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.stats-link:hover, .back-link:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.deliveries-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.no-deliveries {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* Стили для статистики */
|
||||
.stats-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-card p {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.delivery-item {
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.delivery-number {
|
||||
color: #646cff;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3em;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.delivery-details {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.delivery-details h4 {
|
||||
margin: 8px 0;
|
||||
font-size: 1.1em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.delivery-customer h4 {
|
||||
margin: 8px 0;
|
||||
font-size: 1.1em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-weight: normal;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.delivery-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.delivery-actions button {
|
||||
padding: 8px 15px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background-color: #646cff;
|
||||
color: white;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.delivery-actions button:last-child {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.delivery-actions button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
41
src/App.jsx
41
src/App.jsx
@@ -1,39 +1,22 @@
|
||||
import { useState } from 'react'
|
||||
import Header from './components/Header'
|
||||
import DeliveryList from './components/DeliveryList'
|
||||
import './App.css'
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import Header from './components/Header';
|
||||
import DeliveryList from './components/DeliveryList';
|
||||
import DeliveryStats from './components/DeliveryStats';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [deliveries, setDeliveries] = useState([
|
||||
{ id: 1, trackingNumber: 'IVN123456', destination: 'Москва', status: 'В пути', customer: 'Иванов Иван' },
|
||||
{ id: 2, trackingNumber: 'IVN654321', destination: 'Санкт-Петербург', status: 'Доставлено', customer: 'Петров Петр' }
|
||||
])
|
||||
|
||||
const addDelivery = (newDelivery) => {
|
||||
setDeliveries([...deliveries, { ...newDelivery, id: Date.now() }])
|
||||
}
|
||||
|
||||
const updateDelivery = (updatedDelivery) => {
|
||||
setDeliveries(deliveries.map(d => d.id === updatedDelivery.id ? updatedDelivery : d))
|
||||
}
|
||||
|
||||
const deleteDelivery = (id) => {
|
||||
setDeliveries(deliveries.filter(d => d.id !== id))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Header />
|
||||
<div className="container">
|
||||
<DeliveryList
|
||||
deliveries={deliveries}
|
||||
onAdd={addDelivery}
|
||||
onUpdate={updateDelivery}
|
||||
onDelete={deleteDelivery}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path="/" element={<DeliveryList />} />
|
||||
<Route path="/deliveries" element={<DeliveryList />} />
|
||||
<Route path="/stats" element={<DeliveryStats />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
@@ -1,20 +1,25 @@
|
||||
const DeliveryItem = ({ delivery, onEdit, onDelete }) => {
|
||||
return (
|
||||
<div className="delivery-item">
|
||||
<div className="delivery-info">
|
||||
<h3>Трек-номер: {delivery.trackingNumber}</h3>
|
||||
<p>Направление: {delivery.destination}</p>
|
||||
<p>Статус: <span className={`status-${delivery.status.toLowerCase().replace(' ', '-')}`}>
|
||||
{delivery.status}
|
||||
</span></p>
|
||||
<p>Клиент: {delivery.customer}</p>
|
||||
<h3 className="delivery-number">Трек-номер: {delivery.trackingNumber}</h3>
|
||||
|
||||
<div className="delivery-details">
|
||||
<h4>Направление: {delivery.destination}</h4>
|
||||
<p className={`status status-${delivery.status.toLowerCase().replace(' ', '-')}`}>
|
||||
Статус: {delivery.status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="delivery-customer">
|
||||
<h4>Клиент: {delivery.customer}</h4>
|
||||
</div>
|
||||
|
||||
<div className="delivery-actions">
|
||||
<button onClick={() => onEdit(delivery)}>Редактировать</button>
|
||||
<button onClick={() => onDelete(delivery.id)}>Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryItem
|
||||
export default DeliveryItem;
|
||||
@@ -1,26 +1,35 @@
|
||||
import { useState } from 'react'
|
||||
import DeliveryItem from './DeliveryItem'
|
||||
import DeliveryForm from './DeliveryForm'
|
||||
import { useState } from 'react';
|
||||
import { useDeliveries } from '../hooks/useDeliveries';
|
||||
import DeliveryItem from './DeliveryItem';
|
||||
import DeliveryForm from './DeliveryForm';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const DeliveryList = ({ deliveries, onAdd, onUpdate, onDelete }) => {
|
||||
const [editingDelivery, setEditingDelivery] = useState(null)
|
||||
const DeliveryList = () => {
|
||||
const { deliveries, loading, error, addDelivery, updateDelivery, deleteDelivery } = useDeliveries();
|
||||
const [editingDelivery, setEditingDelivery] = useState(null);
|
||||
|
||||
const handleEdit = (delivery) => {
|
||||
setEditingDelivery(delivery)
|
||||
}
|
||||
|
||||
const handleSubmit = (formData) => {
|
||||
if (formData.id) {
|
||||
onUpdate(formData)
|
||||
} else {
|
||||
onAdd(formData)
|
||||
const handleSubmit = async (formData) => {
|
||||
try {
|
||||
if (formData.id) {
|
||||
await updateDelivery(formData);
|
||||
} else {
|
||||
await addDelivery(formData);
|
||||
}
|
||||
setEditingDelivery(null);
|
||||
} catch (err) {
|
||||
console.error('Error saving delivery:', err);
|
||||
}
|
||||
setEditingDelivery(null)
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="loading">Загрузка данных...</div>;
|
||||
if (error) return <div className="error">Ошибка: {error}</div>;
|
||||
|
||||
return (
|
||||
<div className="delivery-list">
|
||||
<h2>Список доставок</h2>
|
||||
<div className="list-header">
|
||||
<h2>Список доставок</h2>
|
||||
<Link to="/stats" className="stats-link">Статистика</Link>
|
||||
</div>
|
||||
|
||||
<DeliveryForm
|
||||
key={editingDelivery ? editingDelivery.id : 'new'}
|
||||
@@ -30,17 +39,21 @@ const DeliveryList = ({ deliveries, onAdd, onUpdate, onDelete }) => {
|
||||
/>
|
||||
|
||||
<div className="deliveries">
|
||||
{deliveries.map(delivery => (
|
||||
<DeliveryItem
|
||||
key={delivery.id}
|
||||
delivery={delivery}
|
||||
onEdit={handleEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
{deliveries.length === 0 ? (
|
||||
<p className="no-deliveries">Нет доступных доставок</p>
|
||||
) : (
|
||||
deliveries.map(delivery => (
|
||||
<DeliveryItem
|
||||
key={delivery.id}
|
||||
delivery={delivery}
|
||||
onEdit={setEditingDelivery}
|
||||
onDelete={deleteDelivery}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryList
|
||||
export default DeliveryList;
|
||||
38
src/components/DeliveryStats.jsx
Normal file
38
src/components/DeliveryStats.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useDeliveries } from '../hooks/useDeliveries';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const DeliveryStats = () => {
|
||||
const { getStats } = useDeliveries();
|
||||
const stats = getStats();
|
||||
|
||||
return (
|
||||
<div className="stats-container">
|
||||
<h2>Статистика доставок</h2>
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<h3>Всего</h3>
|
||||
<p>{stats.total}</p>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<h3>Принято</h3>
|
||||
<p>{stats['Принято'] || 0}</p>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<h3>В пути</h3>
|
||||
<p>{stats['В пути'] || 0}</p>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<h3>Доставлено</h3>
|
||||
<p>{stats['Доставлено'] || 0}</p>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<h3>Отменено</h3>
|
||||
<p>{stats['Отменено'] || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link to="/" className="back-link">← Назад к списку</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryStats;
|
||||
87
src/hooks/useDeliveries.js
Normal file
87
src/hooks/useDeliveries.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const API_URL = 'http://localhost:3001/deliveries';
|
||||
|
||||
export const useDeliveries = () => {
|
||||
const [deliveries, setDeliveries] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchDeliveries = async () => {
|
||||
try {
|
||||
const response = await fetch(API_URL);
|
||||
if (!response.ok) throw new Error('Server error');
|
||||
const data = await response.json();
|
||||
setDeliveries(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDeliveries();
|
||||
}, []);
|
||||
|
||||
const addDelivery = async (delivery) => {
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(delivery)
|
||||
});
|
||||
const newDelivery = await response.json();
|
||||
setDeliveries(prev => [...prev, newDelivery]);
|
||||
return newDelivery;
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const updateDelivery = async (delivery) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/${delivery.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(delivery)
|
||||
});
|
||||
const updated = await response.json();
|
||||
setDeliveries(prev => prev.map(d => d.id === delivery.id ? updated : d));
|
||||
return updated;
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDelivery = async (id) => {
|
||||
try {
|
||||
await fetch(`${API_URL}/${id}`, { method: 'DELETE' });
|
||||
setDeliveries(prev => prev.filter(d => d.id !== id));
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const getStats = () => {
|
||||
return deliveries.reduce((acc, curr) => {
|
||||
acc.total++;
|
||||
acc[curr.status] = (acc[curr.status] || 0) + 1;
|
||||
return acc;
|
||||
}, { total: 0 });
|
||||
};
|
||||
|
||||
return {
|
||||
deliveries,
|
||||
loading,
|
||||
error,
|
||||
addDelivery,
|
||||
updateDelivery,
|
||||
deleteDelivery,
|
||||
getStats,
|
||||
refetch: fetchDeliveries
|
||||
};
|
||||
};
|
||||
15
src/main.jsx
15
src/main.jsx
@@ -1,10 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user