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 ( <>

TEST

{categoryButtons}
{searchOpen ? (
) : null}
); } 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}

{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 ? ( {displayName} ) : (
{getInitials(user)}
)}

{displayName}

{username}
Orders {orders.length}
Favorites {favoritesCount}
Cart items {cartCount}
Профиль
Username {username}
Язык {user?.language_code || "ru"}
Последний вход {authDate}
{supportUsername ? (
Поддержка @{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}` : ""}
))}
)) ) : (
Заказов пока нет.
)}
Promo Codes
{editingPromoId ? ( ) : null}
{dashboard.promoCodes.map((promo) => (
{promo.code}
{promo.discount_type} • {promo.amount} {promo.usage_limit ? ` • ${promo.uses_count}/${promo.usage_limit}` : ` • used ${promo.uses_count}`}
{promo.is_active ? "active" : "inactive"}
))}
Products