feat: add page items

This commit is contained in:
2026-06-24 13:13:09 +03:00
parent ddad7b577b
commit 7ca8967bc3
5 changed files with 165 additions and 3 deletions
+2
View File
@@ -8,6 +8,7 @@ import NotFoundPage from "./pages/NotFoundPage";
import Contact from "./pages/Contact"; import Contact from "./pages/Contact";
import Cart from "./pages/Cart"; import Cart from "./pages/Cart";
import Profile from "./pages/Profile"; import Profile from "./pages/Profile";
import ItemPage from "./pages/ItemPage";
const PrivateRoute = ({ children }) => { const PrivateRoute = ({ children }) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@@ -37,6 +38,7 @@ export default function App() {
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route path="items/:id" element={<ItemPage />} />
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
+6 -1
View File
@@ -1,7 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import Arrow from "../components/Arrow"; import Arrow from "../components/Arrow";
import { useNavigate } from "react-router-dom";
export default function CardItem({ item, onAddToCart }) { export default function CardItem({ item, onAddToCart }) {
const navigate = useNavigate();
const isPiece = item.unit_type === "piece"; const isPiece = item.unit_type === "piece";
const step = isPiece ? 1 : 0.1; const step = isPiece ? 1 : 0.1;
const minValue = isPiece ? 1 : 0.1; const minValue = isPiece ? 1 : 0.1;
@@ -20,7 +22,10 @@ export default function CardItem({ item, onAddToCart }) {
return ( return (
<div className="flex flex-col text-white w-full"> <div className="flex flex-col text-white w-full">
<div className="w-full rounded-md overflow-hidden bg-slate-800/60 flex h-[400px] md:h-[600px] flex-col items-center justify-center border border-slate-700/50 select-none"> <div
className="w-full rounded-md overflow-hidden bg-slate-800/60 flex h-[400px] md:h-[600px] flex-col items-center justify-center border border-slate-700/50 select-none cursor-pointer"
onClick={() => navigate(`/items/${item.id}`)}
>
<img <img
src={item.avatar_url} src={item.avatar_url}
alt={item.name} alt={item.name}
+5 -1
View File
@@ -1,7 +1,10 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import PageLayout from "../components/PageLayout"; import PageLayout from "../components/PageLayout";
import { getCart, removeFromCart, createOrder } from "../api/index"; import { getCart, removeFromCart, createOrder } from "../api/index";
import { useNavigate } from "react-router-dom";
export default function Cart() { export default function Cart() {
const navigate = useNavigate();
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -63,7 +66,8 @@ export default function Cart() {
<img <img
src={`/static/items/${ci.item.avatar_url}`} src={`/static/items/${ci.item.avatar_url}`}
alt={ci.item.name} alt={ci.item.name}
className="w-20 md:w-40 h-20 md:h-40 object-cover rounded-md shrink-0" className="w-20 md:w-40 h-20 md:h-40 object-cover rounded-md shrink-0 cursor-pointer"
onClick={() => navigate(`/items/${ci.item.id}`)}
/> />
<div className="grow"> <div className="grow">
<p className="font-medium text-base md:text-lg"> <p className="font-medium text-base md:text-lg">
+150
View File
@@ -0,0 +1,150 @@
import PageLayout from "../components/PageLayout";
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { getItem, addToCart } from "../api/index";
import Arrow from "../components/Arrow";
export default function ItemPage() {
const { id } = useParams();
const [item, setItem] = useState(null);
const [loading, setLoading] = useState(true);
const [quantity, setQuantity] = useState(1);
useEffect(() => {
getItem(id)
.then((res) => setItem(res.data))
.catch(() => { })
.finally(() => setLoading(false));
}, [id]);
const isPiece = item?.unit_type === "piece";
const step = isPiece ? 1 : 0.1;
const minValue = isPiece ? 1 : 0.1;
useEffect(() => {
if (item) setQuantity(isPiece ? 1 : 0.1);
}, [item, isPiece]);
const handleIncrement = () => {
setQuantity((prev) => parseFloat((prev + step).toFixed(3)));
};
const handleDecrement = () => {
setQuantity((prev) =>
prev > minValue ? parseFloat((prev - step).toFixed(3)) : minValue,
);
};
const handleAddToCart = async () => {
try {
await addToCart({ item_id: item.id, quantity });
alert(`${item.name} добавлен в корзину`);
} catch {
alert("Не удалось добавить товар в корзину");
}
};
if (loading) {
return (
<PageLayout>
<p className="text-white p-6 md:p-16">Загрузка...</p>
</PageLayout>
);
}
if (!item) {
return (
<PageLayout>
<p className="text-white p-6 md:p-16">Товар не найден</p>
</PageLayout>
);
}
return (
<PageLayout>
<main className="px-6 md:px-16 grow w-full mb-10 text-white">
<div className="flex flex-col md:flex-row gap-8 md:gap-12 mt-6">
{/* ЛЕВО — фото */}
<div className="md:w-1/2">
<img
src={item.avatar_url}
alt={item.name}
className="w-full h-[300px] md:h-[500px] object-cover rounded-lg"
/>
</div>
{/* ПРАВО — всё инфо в одной колонке */}
<div className="md:w-1/2 flex flex-col">
<h1 className="text-2xl md:text-4xl font-medium uppercase mb-3">
{item.name}
</h1>
<p className="text-2xl md:text-3xl font-medium mb-6">
{item.price}
<span className="text-base text-white/50"> / {item.unit}</span>
</p>
<div className="flex flex-col gap-2 text-white/70 text-sm md:text-base mb-8">
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">Категория</span>
<span>{item.category}</span>
</div>
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">Город</span>
<span>{item.city}</span>
</div>
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">Тип</span>
<span>{isPiece ? "Поштучно" : "На вес"}</span>
</div>
<div className="flex justify-between border-b border-white/10 pb-2">
<span className="text-white/50">В наличии</span>
<span
className={item.stock > 0 ? "text-green-400" : "text-red-400"}
>
{item.stock > 0 ? `${item.stock} ${item.unit}` : "нет"}
</span>
</div>
</div>
<div className="flex items-center gap-6 mb-6">
<span className="text-white/50 text-sm">Количество:</span>
<button
onClick={handleDecrement}
className="w-10 h-10 flex items-center justify-center bg-white/10 hover:bg-white/20 rounded font-medium text-xl cursor-pointer select-none transition-colors"
>
-
</button>
<span className="mx-2 font-mono text-sm min-w-20 text-center">
{quantity} {item.unit}
</span>
<button
onClick={handleIncrement}
className="w-10 h-10 flex items-center justify-center bg-white/10 hover:bg-white/20 rounded font-medium text-xl cursor-pointer select-none transition-colors"
>
+
</button>
</div>
<p className="text-lg mb-6">
Стоимость:{" "}
<span className="font-medium">
{parseFloat((quantity * item.price).toFixed(2))}
</span>
</p>
<button
onClick={handleAddToCart}
disabled={item.stock <= 0}
className="group flex items-center gap-4 text-base font-bold text-white cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
>
<span>
{item.stock > 0 ? "Добавить в корзину" : "Нет в наличии"}
</span>
<Arrow />
</button>
</div>
</div>
</main>
</PageLayout>
);
}
+2 -1
View File
@@ -97,7 +97,8 @@ export default function Profile() {
<img <img
src={`/static/items/${oi.item?.avatar_url}`} src={`/static/items/${oi.item?.avatar_url}`}
alt={oi.item?.name} alt={oi.item?.name}
className="w-10 h-10 object-cover rounded shrink-0" className="w-10 h-10 md:w-30 md:h-30 object-cover rounded shrink-0 cursor-pointer"
onClick={() => navigate(`/items/${oi.item.id}`)}
/> />
<span className="grow">{oi.item?.name}</span> <span className="grow">{oi.item?.name}</span>
<span className="whitespace-nowrap shrink-0"> <span className="whitespace-nowrap shrink-0">