const { useEffect, useMemo, useState } = React;
const API_BASE =
window.__SHOP_API_BASE__ ||
localStorage.getItem("shop-api-base") ||
(window.location.protocol === "file:" ? "http://127.0.0.1:8000/api" : "/api");
const SESSION_TOKEN_KEY = "shop-session-token";
const navItems = [
{ id: "home", label: "Home", icon: "icon-home-fill" },
{ id: "favorites", label: "Favorites", icon: "icon-heart-fill" },
{ id: "cart", label: "Cart", icon: "icon-bag-fill" },
{ id: "profile", label: "Profile", icon: "icon-user-fill" },
];
const sortModes = [
{ id: "featured", label: "По популярности" },
{ id: "priceAsc", label: "Цена: ниже" },
{ id: "priceDesc", label: "Цена: выше" },
];
const emptyPromoForm = {
code: "",
discount_type: "percent",
amount: "10",
description: "",
usage_limit: "",
is_active: true,
};
const emptyProductForm = {
title: "",
price: "2990",
category: "ФУТБОЛКИ",
sort_order: "0",
is_active: true,
imagesText: "",
sizesText: "S\nM\nL\nXL",
descriptionText: "",
};
function rub(value) {
return `${new Intl.NumberFormat("ru-RU").format(value)} \u20bd`;
}
function getStoredToken() {
return localStorage.getItem(SESSION_TOKEN_KEY) || "";
}
function setStoredToken(token) {
localStorage.setItem(SESSION_TOKEN_KEY, token);
}
function clearStoredToken() {
localStorage.removeItem(SESSION_TOKEN_KEY);
}
function getTelegramInitData() {
return window.Telegram?.WebApp?.initData || "";
}
function isLocalPreview() {
return (
window.location.protocol === "file:" ||
window.location.hostname === "127.0.0.1" ||
window.location.hostname === "localhost"
);
}
function parseTimestamp(value) {
if (!value) {
return null;
}
if (typeof value === "number") {
return new Date(value * 1000);
}
return new Date(`${value.replace(" ", "T")}Z`);
}
function formatOrderDate(value) {
const date = parseTimestamp(value);
if (!date || Number.isNaN(date.getTime())) {
return "\u2014";
}
return new Intl.DateTimeFormat("ru-RU", {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
}).format(date);
}
function getDisplayName(user) {
if (!user) {
return "Покупатель";
}
return (
user.display_name ||
[user.first_name, user.last_name].filter(Boolean).join(" ").trim() ||
(user.username ? `@${user.username}` : "Покупатель")
);
}
function getInitials(user) {
const name = getDisplayName(user).replace("@", "").trim();
const parts = name.split(/\s+/).filter(Boolean);
const letters = parts.slice(0, 2).map((part) => part[0]?.toUpperCase() || "");
return letters.join("") || "TG";
}
async function copyText(text) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const input = document.createElement("textarea");
input.value = text;
input.setAttribute("readonly", "");
input.style.position = "absolute";
input.style.left = "-9999px";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
}
async function apiRequest(path, options = {}) {
const headers = {
"Content-Type": "application/json",
...(options.headers || {}),
};
if (!options.skipAuth) {
const token = getStoredToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}
}
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers,
});
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
if (response.status === 401 && !options.skipAuth) {
clearStoredToken();
}
const detail = payload.detail;
const error = new Error(
typeof detail === "string" ? detail : payload.message || `API error ${response.status}`
);
error.status = response.status;
error.payload = payload;
error.path = path;
throw error;
}
return response.json();
}
function makeCartKey(productId, size = "") {
return `${productId}::${size || ""}`;
}
function buildCartIndex(items) {
return Object.fromEntries(items.map((item) => [item.key, item]));
}
function normalizeState(payload) {
const favorites = payload.favorites || [];
const cartItems = (payload.cart || []).map((item) => ({
...item,
size: item.size || "",
key: item.key || makeCartKey(item.product_id, item.size || ""),
}));
return {
user: payload.user || null,
favorites,
cartItems,
cartIndex: buildCartIndex(cartItems),
orders: payload.orders || [],
supportUrl: payload.support_url || null,
supportUsername: payload.support_username || null,
};
}
function Icon({ id }) {
return (
);
}
function CatalogHeader({
activeCategory,
searchOpen,
searchQuery,
onSearchToggle,
onSearchChange,
onOpenCategories,
onCycleSort,
onOpenProfile,
categoryButtons,
}) {
return (
<>
>
);
}
function ProductCard({ product, isFavorite, onToggleFavorite, onOpen }) {
return (
{product.images.map((_, index) => (
))}
);
}
function FavoritesEmpty({ onBackToShop }) {
return (
В избранном пока пусто
Сохраняйте понравившиеся вещи, чтобы быстро вернуться к ним позже.
);
}
function CartScreen({
cartItems,
productMap,
couponCode,
promoPreview,
applyingPromo,
onCouponChange,
onApplyPromo,
onClearCart,
onIncrement,
onDecrement,
onCheckout,
}) {
if (!cartItems.length) {
return (
Корзина пока пустая
Добавьте товары в корзину, чтобы оформить заказ.
);
}
const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0);
const subtotalAmount = cartItems.reduce((sum, item) => {
const product = productMap[item.product_id];
return sum + (product ? product.price * item.quantity : 0);
}, 0);
const discountAmount = promoPreview?.coupon_applied ? promoPreview.discount_amount || 0 : 0;
const totalAmount = promoPreview?.coupon_applied
? promoPreview.total_amount || Math.max(0, subtotalAmount - discountAmount)
: subtotalAmount;
const appliedCouponCode = promoPreview?.coupon_applied ? promoPreview.coupon_code : "";
return (
Shopping Cart
{cartItems.map((item) => {
const product = productMap[item.product_id];
if (!product) {
return null;
}
return (
{product.title}
Размер одежды {item.size || "ONE SIZE"}
{item.quantity}
{rub(product.price * item.quantity)}
);
})}
onCouponChange(event.target.value.toUpperCase())}
/>
{promoPreview?.promo_error ? (
{promoPreview.promo_error}
) : null}
{promoPreview?.coupon_applied ? (
Promo {appliedCouponCode} applied, discount {rub(discountAmount)}
) : null}
Items ({totalItems})
{rub(subtotalAmount)}
{discountAmount > 0 ? (
Discount
-{rub(discountAmount)}
) : null}
Total
{rub(totalAmount)}
);
}
function ProfileScreen({
user,
orders,
favoritesCount,
cartCount,
productMap,
supportUrl,
supportUsername,
onOpenAdmin,
}) {
const displayName = getDisplayName(user);
const username = user?.username ? `@${user.username}` : "не указан";
const authDate = user?.last_auth_date ? formatOrderDate(user.last_auth_date) : "\u2014";
return (
{user?.photo_url ? (

) : (
{getInitials(user)}
)}
{displayName}
{username}
Orders
{orders.length}
Favorites
{favoritesCount}
Cart items
{cartCount}
Профиль
Username
{username}
Язык
{user?.language_code || "ru"}
Последний вход
{authDate}
{supportUsername ? (
) : null}
{user?.is_admin ? (
) : null}
История заказов
{orders.length ? (
{orders.map((order) => (
Order #{order.id}
{formatOrderDate(order.created_at)}
{rub(order.total_amount)}
{order.items.map((item, index) => {
const product = productMap[item.product_id];
return (
{product?.title || `Product #${item.product_id}`}
{item.quantity} × {rub(item.unit_price)}
);
})}
{order.coupon_code ? Coupon: {order.coupon_code}
: null}
))}
) : (
У вас пока нет заказов. Когда оформите первую покупку, она появится здесь.
)}
);
}
function AdminScreen({
dashboard,
loading,
promoForm,
productForm,
editingPromoId,
editingProductId,
onPromoFormChange,
onProductFormChange,
onSavePromo,
onEditPromo,
onResetPromo,
onSaveProduct,
onEditProduct,
onToggleProduct,
onResetProduct,
onRefresh,
}) {
if (loading) {
return (
Загружаем данные
Еще немного, и все будет готово.
);
}
return (
Admin Panel
Заказы, промокоды и управление товарами
All Orders
{dashboard.orders.length ? (
dashboard.orders.map((order) => (
Order #{order.id}
{order.customer.display_name}
{order.customer.username ? ` • @${order.customer.username}` : ""}
{rub(order.total_amount)}
{formatOrderDate(order.created_at)}
{order.coupon_code ? (
Promo: {order.coupon_code} • discount {rub(order.discount_amount || 0)}
) : null}
{order.items.map((item, index) => (
{item.product_title || `Product #${item.product_id}`}
{item.quantity} × {rub(item.unit_price)} {item.size ? `• ${item.size}` : ""}
))}
))
) : (
Заказов пока нет.
)}
);
}
function CategorySheet({ categories, activeCategory, onSelect, onClose }) {
return (
<>
Категории
{categories.map((category) => (
))}
>
);
}
function ProductModal({
product,
slideIndex,
selectedSize,
isFavorite,
quantity,
hasSupport,
onClose,
onPrevSlide,
onNextSlide,
onToggleFavorite,
onSelectSize,
onQtyMinus,
onQtyPlus,
onGoToCart,
onShare,
onShowSizeChart,
onContactSeller,
}) {
if (!product) {
return null;
}
const imageSrc = product.images[slideIndex];
const imageFitClass = slideIndex === product.images.length - 1 ? "contain" : "";
return (
<>
{product.images.map((_, index) => (
))}
Размер одежды: {selectedSize}
{product.sizes.map((size) => (
))}
{product.title}
{rub(product.price)}
Product details
{product.description.map((line) => (
{line}
))}
Still have questions?
>
);
}
function Toast({ message }) {
if (!message) {
return null;
}
return {message}
;
}
function App() {
const [viewer, setViewer] = useState(null);
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [activeTab, setActiveTab] = useState("home");
const [activeCategory, setActiveCategory] = useState("ALL");
const [favorites, setFavorites] = useState([]);
const [cartItems, setCartItems] = useState([]);
const [cartIndex, setCartIndex] = useState({});
const [orders, setOrders] = useState([]);
const [searchOpen, setSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [sortMode, setSortMode] = useState("featured");
const [activeProductId, setActiveProductId] = useState(null);
const [selectedSize, setSelectedSize] = useState({});
const [draftQuantities, setDraftQuantities] = useState({});
const [couponCode, setCouponCode] = useState("");
const [promoPreview, setPromoPreview] = useState(null);
const [applyingPromo, setApplyingPromo] = useState(false);
const [productSlides, setProductSlides] = useState({});
const [categorySheetOpen, setCategorySheetOpen] = useState(false);
const [toast, setToast] = useState("");
const [supportUrl, setSupportUrl] = useState(null);
const [supportUsername, setSupportUsername] = useState(null);
const [adminDashboard, setAdminDashboard] = useState({
orders: [],
promoCodes: [],
products: [],
});
const [adminLoading, setAdminLoading] = useState(false);
const [promoForm, setPromoForm] = useState(emptyPromoForm);
const [productForm, setProductForm] = useState(emptyProductForm);
const [editingPromoId, setEditingPromoId] = useState(null);
const [editingProductId, setEditingProductId] = useState(null);
const favoritesSet = useMemo(() => new Set(favorites), [favorites]);
const productMap = useMemo(
() => Object.fromEntries(products.map((product) => [product.id, product])),
[products]
);
const cartCount = useMemo(
() => cartItems.reduce((sum, entry) => sum + entry.quantity, 0),
[cartItems]
);
const categories = useMemo(
() => ["ALL", ...new Set(products.map((product) => product.category))],
[products]
);
const activeProduct = useMemo(
() => products.find((product) => product.id === activeProductId) || null,
[activeProductId, products]
);
function showToast(message) {
setToast(message);
}
function applyState(payload) {
const next = normalizeState(payload);
setViewer((prev) => next.user || prev);
setFavorites(next.favorites);
setCartItems(next.cartItems);
setCartIndex(next.cartIndex);
setOrders(next.orders);
setSupportUrl(next.supportUrl);
setSupportUsername(next.supportUsername);
}
function resetPromoPreview() {
setPromoPreview(null);
}
function handleCouponChange(value) {
setCouponCode(value);
resetPromoPreview();
}
function getCartItem(productId, size = "") {
return cartIndex[makeCartKey(productId, size || "")] || null;
}
function getDefaultSize(productId) {
const product = productMap[productId];
const existingSize = cartItems.find((item) => item.product_id === productId)?.size;
return (
selectedSize[productId] ||
existingSize ||
product?.sizes[1] ||
product?.sizes[0] ||
"M"
);
}
function getDraftQuantity(productId, size) {
const key = makeCartKey(productId, size || "");
return draftQuantities[key] || getCartItem(productId, size)?.quantity || 1;
}
async function loadAdminDashboard() {
setAdminLoading(true);
try {
const payload = await apiRequest("/admin/dashboard");
setAdminDashboard({
orders: payload.orders || [],
promoCodes: payload.promo_codes || [],
products: payload.products || [],
});
} catch (err) {
showToast(err.message || "Не удалось загрузить админку");
} finally {
setAdminLoading(false);
}
}
async function authenticate() {
const existingToken = getStoredToken();
if (existingToken) {
try {
const session = await apiRequest("/me");
setViewer(session.user || null);
setSupportUrl(session.support_url || null);
setSupportUsername(session.support_username || null);
return;
} catch (err) {
clearStoredToken();
}
}
const initData = getTelegramInitData();
if (initData) {
try {
const authPayload = await apiRequest("/auth/telegram", {
method: "POST",
body: JSON.stringify({ init_data: initData }),
skipAuth: true,
});
setStoredToken(authPayload.token);
setViewer(authPayload.user || null);
setSupportUrl(authPayload.support_url || null);
setSupportUsername(authPayload.support_username || null);
return;
} catch (err) {
const message = err?.message || "";
if (!isLocalPreview()) {
throw err;
}
console.warn("Telegram auth failed in local preview, falling back to dev auth.", message);
}
}
if (!isLocalPreview()) {
throw new Error("Откройте миниапп внутри Telegram, чтобы пройти авторизацию");
}
const devPayload = await apiRequest("/auth/dev", {
method: "POST",
body: JSON.stringify({ display_name: "Local Tester" }),
skipAuth: true,
});
setStoredToken(devPayload.token);
setViewer(devPayload.user || null);
setSupportUrl(devPayload.support_url || null);
setSupportUsername(devPayload.support_username || null);
}
async function syncState() {
const payload = await apiRequest("/state");
applyState(payload);
}
useEffect(() => {
let cancelled = false;
async function bootstrap() {
try {
setLoading(true);
await authenticate();
const [productsPayload, statePayload] = await Promise.all([
apiRequest("/products"),
apiRequest("/state"),
]);
if (cancelled) {
return;
}
setProducts(productsPayload.products || []);
applyState(statePayload);
setError("");
} catch (err) {
if (!cancelled) {
setError(err.message || "Не удалось загрузить данные");
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
bootstrap();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
const webApp = window.Telegram?.WebApp;
if (!webApp) {
return;
}
webApp.ready();
webApp.expand();
try {
webApp.setHeaderColor("#eceef3");
webApp.setBackgroundColor("#eceef3");
} catch (err) {
console.warn("Telegram WebApp styling is not available in this client.", err);
}
}, []);
useEffect(() => {
if (!toast) {
return undefined;
}
const id = window.setTimeout(() => setToast(""), 1800);
return () => window.clearTimeout(id);
}, [toast]);
useEffect(() => {
const onKeyDown = (event) => {
if (event.key === "Escape") {
setActiveProductId(null);
setCategorySheetOpen(false);
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
const visibleProducts = useMemo(() => {
let list = [...products];
if (activeTab === "favorites") {
list = list.filter((product) => favoritesSet.has(product.id));
}
if (activeTab === "home" && activeCategory !== "ALL") {
list = list.filter((product) => product.category === activeCategory);
}
if (searchQuery.trim()) {
const query = searchQuery.trim().toLowerCase();
list = list.filter(
(product) =>
product.title.toLowerCase().includes(query) ||
product.category.toLowerCase().includes(query)
);
}
if (sortMode === "priceAsc") {
list.sort((a, b) => a.price - b.price);
}
if (sortMode === "priceDesc") {
list.sort((a, b) => b.price - a.price);
}
return list;
}, [activeTab, activeCategory, favoritesSet, products, searchQuery, sortMode]);
function cycleSort() {
const currentIndex = sortModes.findIndex((mode) => mode.id === sortMode);
const nextMode = sortModes[(currentIndex + 1) % sortModes.length];
setSortMode(nextMode.id);
showToast(nextMode.label);
}
async function toggleFavorite(productId) {
const favorite = !favoritesSet.has(productId);
try {
const payload = await apiRequest("/favorites", {
method: "PUT",
body: JSON.stringify({ product_id: productId, favorite }),
});
applyState(payload);
showToast(favorite ? "Добавили в избранное" : "Убрали из избранного");
} catch (err) {
showToast(err.message || "Не удалось обновить избранное");
}
}
async function saveCartItem(productId, quantity, size) {
try {
const payload = await apiRequest("/cart/items", {
method: "PUT",
body: JSON.stringify({
product_id: productId,
quantity,
size,
}),
});
applyState(payload);
resetPromoPreview();
return true;
} catch (err) {
showToast(err.message || "Не удалось обновить корзину");
return false;
}
}
async function goToCartFromProduct(productId, quantity, size) {
const updated = await saveCartItem(productId, quantity, size || getDefaultSize(productId));
if (!updated) {
return;
}
setActiveProductId(null);
setActiveTab("cart");
}
function openProduct(productId) {
setActiveProductId(productId);
setCategorySheetOpen(false);
}
function closeProduct() {
setActiveProductId(null);
}
function nextSlide(productId, direction) {
const product = productMap[productId];
if (!product) {
return;
}
setProductSlides((prev) => ({
...prev,
[productId]: ((prev[productId] || 0) + direction + product.images.length) % product.images.length,
}));
}
function switchTab(tab) {
setActiveProductId(null);
setCategorySheetOpen(false);
setActiveTab(tab);
if (tab !== "home") {
setActiveCategory("ALL");
setSearchOpen(false);
}
if (tab === "admin" && viewer?.is_admin) {
loadAdminDashboard();
}
}
async function clearCart() {
try {
const payload = await apiRequest("/cart", {
method: "DELETE",
});
applyState(payload);
setCouponCode("");
resetPromoPreview();
showToast("Корзина очищена");
} catch (err) {
showToast(err.message || "Не удалось очистить корзину");
}
}
async function applyPromoCode() {
const normalizedCode = couponCode.trim().toUpperCase();
setCouponCode(normalizedCode);
if (!normalizedCode) {
resetPromoPreview();
showToast("Введите промокод");
return;
}
try {
setApplyingPromo(true);
const payload = await apiRequest("/cart/promo-preview", {
method: "POST",
body: JSON.stringify({
coupon_code: normalizedCode,
}),
});
setPromoPreview(payload);
if (payload.coupon_applied) {
showToast("Промокод применен");
} else if (payload.promo_error) {
showToast(payload.promo_error);
}
} catch (err) {
setPromoPreview({
coupon_code: normalizedCode,
coupon_applied: false,
promo_error: err.message || "Не удалось проверить промокод",
});
showToast(err.message || "Не удалось проверить промокод");
} finally {
setApplyingPromo(false);
}
}
async function checkout() {
const normalizedCoupon = couponCode.trim().toUpperCase();
if (normalizedCoupon && !promoPreview?.coupon_applied) {
showToast("Сначала примените промокод");
return;
}
try {
const payload = await apiRequest("/orders/checkout", {
method: "POST",
body: JSON.stringify({
coupon_code: promoPreview?.coupon_applied ? normalizedCoupon : null,
}),
});
applyState(payload);
setCouponCode("");
resetPromoPreview();
setActiveTab("profile");
showToast("Заказ сохранен в историю");
} catch (err) {
showToast(err.message || "Не удалось оформить заказ");
}
}
async function handleShare() {
if (!activeProduct) {
return;
}
const shareText = `${activeProduct.title} • ${rub(activeProduct.price)}`;
try {
if (navigator.share) {
await navigator.share({
title: activeProduct.title,
text: shareText,
});
} else {
await copyText(shareText);
showToast("Название товара скопировано");
return;
}
} catch (err) {
return;
}
showToast("Поделились товаром");
}
function openSupport() {
if (!supportUrl) {
showToast("Добавьте SHOP_SUPPORT_USERNAME на сервере");
return;
}
const webApp = window.Telegram?.WebApp;
if (webApp?.openTelegramLink) {
webApp.openTelegramLink(supportUrl);
return;
}
window.open(supportUrl, "_blank", "noopener,noreferrer");
}
function updatePromoForm(field, value) {
setPromoForm((prev) => ({ ...prev, [field]: value }));
}
function resetPromoForm() {
setPromoForm(emptyPromoForm);
setEditingPromoId(null);
}
function editPromo(promo) {
setEditingPromoId(promo.id);
setPromoForm({
code: promo.code,
discount_type: promo.discount_type,
amount: String(promo.amount),
description: promo.description || "",
usage_limit: promo.usage_limit ? String(promo.usage_limit) : "",
is_active: promo.is_active,
});
setActiveTab("admin");
}
async function savePromo() {
try {
const payload = {
code: promoForm.code.trim(),
discount_type: promoForm.discount_type,
amount: Number(promoForm.amount),
description: promoForm.description.trim() || null,
usage_limit: promoForm.usage_limit ? Number(promoForm.usage_limit) : null,
is_active: promoForm.is_active,
};
if (!payload.code || !payload.amount) {
showToast("Заполните промокод и размер скидки");
return;
}
const path = editingPromoId ? `/admin/promo-codes/${editingPromoId}` : "/admin/promo-codes";
const method = editingPromoId ? "PUT" : "POST";
const response = await apiRequest(path, {
method,
body: JSON.stringify(payload),
});
setAdminDashboard((prev) => ({ ...prev, promoCodes: response.promo_codes || [] }));
resetPromoForm();
showToast(editingPromoId ? "Промокод обновлен" : "Промокод создан");
} catch (err) {
showToast(err.message || "Не удалось сохранить промокод");
}
}
function updateProductForm(field, value) {
setProductForm((prev) => ({ ...prev, [field]: value }));
}
function resetProductForm() {
setProductForm(emptyProductForm);
setEditingProductId(null);
}
function editProduct(product) {
setEditingProductId(product.id);
setProductForm({
title: product.title,
price: String(product.price),
category: product.category,
sort_order: String(product.sort_order || 0),
is_active: Boolean(product.is_active),
imagesText: (product.images || []).join("\n"),
sizesText: (product.sizes || []).join("\n"),
descriptionText: (product.description || []).join("\n"),
});
setActiveTab("admin");
}
async function saveProduct() {
try {
const payload = {
title: productForm.title.trim(),
price: Number(productForm.price),
category: productForm.category.trim(),
sort_order: Number(productForm.sort_order || 0),
is_active: productForm.is_active,
images: productForm.imagesText.split("\n").map((item) => item.trim()).filter(Boolean),
sizes: productForm.sizesText.split("\n").map((item) => item.trim()).filter(Boolean),
description: productForm.descriptionText.split("\n").map((item) => item.trim()).filter(Boolean),
};
if (!payload.title || !payload.category || !payload.price) {
showToast("Заполните title, price и category");
return;
}
const path = editingProductId ? `/admin/products/${editingProductId}` : "/admin/products";
const method = editingProductId ? "PUT" : "POST";
const response = await apiRequest(path, {
method,
body: JSON.stringify(payload),
});
const nextProducts = response.products || [];
setAdminDashboard((prev) => ({ ...prev, products: nextProducts }));
setProducts(nextProducts.filter((item) => item.is_active));
resetProductForm();
showToast(editingProductId ? "Товар обновлен" : "Товар создан");
} catch (err) {
showToast(err.message || "Не удалось сохранить товар");
}
}
async function toggleProduct(product) {
try {
const response = await apiRequest(`/admin/products/${product.id}`, {
method: "PUT",
body: JSON.stringify({
title: product.title,
price: product.price,
category: product.category,
sort_order: product.sort_order || 0,
is_active: !product.is_active,
images: product.images || [],
sizes: product.sizes || [],
description: product.description || [],
}),
});
const nextProducts = response.products || [];
setAdminDashboard((prev) => ({ ...prev, products: nextProducts }));
setProducts(nextProducts.filter((item) => item.is_active));
showToast(product.is_active ? "Товар скрыт" : "Товар снова видим");
} catch (err) {
showToast(err.message || "Не удалось изменить видимость товара");
}
}
const categoryButtons = categories
.filter((category) => category !== "ALL")
.map((category) => (
));
const currentProductSize = activeProduct ? getDefaultSize(activeProduct.id) : "";
const currentProductQty = activeProduct ? Math.max(1, getDraftQuantity(activeProduct.id, currentProductSize)) : 1;
return (
{activeTab === "home" ? (
setSearchOpen((prev) => !prev)}
onSearchChange={setSearchQuery}
onOpenCategories={() => setCategorySheetOpen(true)}
onCycleSort={cycleSort}
onOpenProfile={() => switchTab("profile")}
categoryButtons={categoryButtons}
/>
) : null}
{loading ? (
Открываем магазин
Еще чуть-чуть, загружаем каталог и ваш профиль.
) : null}
{!loading && error ? (
Не удалось подключиться
{error}
) : null}
{!loading && !error && activeTab === "favorites" && !visibleProducts.length ? (
switchTab("home")} />
) : null}
{!loading && !error && activeTab === "cart" ? (
saveCartItem(item.product_id, item.quantity + 1, item.size)}
onDecrement={(item) => saveCartItem(item.product_id, Math.max(0, item.quantity - 1), item.size)}
onCheckout={checkout}
/>
) : null}
{!loading && !error && activeTab === "profile" ? (
switchTab("admin")}
/>
) : null}
{!loading && !error && activeTab === "admin" && viewer?.is_admin ? (
) : null}
{!loading &&
!error &&
(activeTab === "home" || (activeTab === "favorites" && visibleProducts.length))
? visibleProducts.map((product) => (
))
: null}
{categorySheetOpen ? (
setCategorySheetOpen(false)}
onSelect={(category) => {
setActiveCategory(category);
setActiveTab("home");
setCategorySheetOpen(false);
}}
/>
) : null}
activeProduct && nextSlide(activeProduct.id, -1)}
onNextSlide={() => activeProduct && nextSlide(activeProduct.id, 1)}
onToggleFavorite={toggleFavorite}
onSelectSize={(size) => {
if (!activeProduct) return;
setSelectedSize((prev) => ({
...prev,
[activeProduct.id]: size,
}));
}}
onQtyMinus={() => {
if (!activeProduct) return;
setDraftQuantities((prev) => ({
...prev,
[makeCartKey(activeProduct.id, currentProductSize)]: Math.max(1, currentProductQty - 1),
}));
}}
onQtyPlus={() => {
if (!activeProduct) return;
setDraftQuantities((prev) => ({
...prev,
[makeCartKey(activeProduct.id, currentProductSize)]: currentProductQty + 1,
}));
}}
onGoToCart={async () => {
if (!activeProduct) return;
await goToCartFromProduct(activeProduct.id, currentProductQty, currentProductSize);
}}
onShare={handleShare}
onShowSizeChart={() => {
if (!activeProduct) return;
setProductSlides((prev) => ({
...prev,
[activeProduct.id]: activeProduct.images.length - 1,
}));
}}
onContactSeller={openSupport}
/>
);
}
ReactDOM.createRoot(document.getElementById("root")).render();