feat: add admin page
This commit is contained in:
@@ -9,6 +9,7 @@ import Contact from "./pages/Contact";
|
||||
import Cart from "./pages/Cart";
|
||||
import Profile from "./pages/Profile";
|
||||
import ItemPage from "./pages/ItemPage";
|
||||
import AdminPage from "./pages/AdminPage";
|
||||
|
||||
const PrivateRoute = ({ children }) => {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -19,6 +20,21 @@ const PrivateRoute = ({ children }) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
const AdminRoute = ({ children }) => {
|
||||
const token = localStorage.getItem("token");
|
||||
const user = JSON.parse(localStorage.getItem("user") || "{}");
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
if (user.role !== "Админ") {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
@@ -38,6 +54,14 @@ export default function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<AdminPage />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="items/:id" element={<ItemPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { getAllItems, deleteItem } from "../api/index";
|
||||
|
||||
export default function AdminItems() {
|
||||
const [items, setItems] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadItems = () => {
|
||||
getAllItems()
|
||||
.then((res) => setItems(res.data))
|
||||
.catch(() => { })
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadItems();
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (id, name) => {
|
||||
if (!confirm(`Удалить товар "${name}"?`)) return;
|
||||
try {
|
||||
await deleteItem(id);
|
||||
setItems((prev) => prev.filter((it) => it.id !== id));
|
||||
} catch {
|
||||
alert("Не удалось удалить товар");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <p className="text-white/60">Загрузка...</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Кнопка добавления (форму подключим следующим шагом) */}
|
||||
<button className="mb-6 bg-gold text-black px-6 py-2 rounded-md font-medium uppercase text-sm">
|
||||
+ Добавить товар
|
||||
</button>
|
||||
|
||||
{/* Таблица товаров */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-white/50 uppercase text-xs border-b border-white/10">
|
||||
<tr>
|
||||
<th className="py-3 pr-4">Фото</th>
|
||||
<th className="py-3 pr-4">Название</th>
|
||||
<th className="py-3 pr-4">Категория</th>
|
||||
<th className="py-3 pr-4">Цена</th>
|
||||
<th className="py-3 pr-4">Остаток</th>
|
||||
<th className="py-3 pr-4">Тип</th>
|
||||
<th className="py-3 pr-4">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="border-b border-white/5 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<td className="py-3 pr-4">
|
||||
<img
|
||||
src={item.avatar_url}
|
||||
alt=""
|
||||
className="w-12 h-12 object-cover rounded"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3 pr-4 font-medium">{item.name}</td>
|
||||
<td className="py-3 pr-4 text-white/70">{item.category}</td>
|
||||
<td className="py-3 pr-4">{item.price} ₽</td>
|
||||
<td className="py-3 pr-4">
|
||||
{item.stock} {item.unit}
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-white/70">
|
||||
{item.unit_type === "piece" ? "шт" : "вес"}
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<div className="flex gap-2">
|
||||
<button className="text-blue-400 hover:text-blue-300 text-sm">
|
||||
Изменить
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(item.id, item.name)}
|
||||
className="text-red-400 hover:text-red-300 text-sm"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useState } from "react";
|
||||
import PageLayout from "../components/PageLayout";
|
||||
import AdminItems from "../components/AdminItems";
|
||||
// import AdminOrders from "../components/AdminOrders"; // добавим позже
|
||||
// import AdminStaff from "../components/AdminStaff"; // добавим позже
|
||||
|
||||
export default function AdminPage() {
|
||||
const [tab, setTab] = useState("items");
|
||||
|
||||
const tabs = [
|
||||
{ key: "items", label: "Товары" },
|
||||
{ key: "orders", label: "Заказы" },
|
||||
{ key: "staff", label: "Сотрудники" },
|
||||
];
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<main className="px-6 md:px-16 grow w-full mb-10 text-white">
|
||||
<h1 className="text-2xl md:text-4xl font-medium uppercase mb-8">
|
||||
Админ-панель
|
||||
</h1>
|
||||
|
||||
{/* Вкладки */}
|
||||
<div className="flex gap-2 mb-8 border-b border-white/10">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={`px-4 py-2 text-sm uppercase transition-colors ${tab === t.key
|
||||
? "text-white border-b-2 border-gold"
|
||||
: "text-white/50 hover:text-white/80"
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Содержимое активной вкладки */}
|
||||
{tab === "items" && <AdminItems />}
|
||||
{tab === "orders" && (
|
||||
<p className="text-white/50">Раздел заказов — в разработке</p>
|
||||
)}
|
||||
{tab === "staff" && (
|
||||
<p className="text-white/50">Раздел сотрудников — в разработке</p>
|
||||
)}
|
||||
</main>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user