Перейти до змісту

ESWF Shop — Frontend Implementation

Дата: 2026-02-17 Сайт: shop.eswf.dev Dev порт: 5175 Статус: ✅ реалізовано


Навіщо

Shop — четвертий сайт мульти-сайтової архітектури ESWF. Це Solution Catalog — маркетплейс модулів для DOP-системи. Дозволяє:

  • переглядати каталог DOP-модулів (Fleet, Essentials, Logistic та ін.)
  • фільтрувати за статусом і типом ціноутворення
  • додавати модулі в кошик
  • оформлювати замовлення (під авторизованим користувачем)
  • переглядати історію замовлень

Продукти у магазині — це платні/безкоштовні DOP-модулі. Покупка → активація ліцензії на тенант.


Tech Stack

Технологія Версія Роль
Next.js 15.1 Framework (SSR, App Router)
React 19.0 UI
TypeScript 5.7 Типізація
Tailwind CSS 3.4 Стилі (v3, як у Portal)
Zustand 5.0 State management (кошик + авторизація)
Axios 1.13 HTTP клієнт
@tabler/icons-react 3.36 Іконки (узгоджено з DOP)

Чому Next.js (не Vite)? Узгодженість з Portal (теж Next.js). Але на відміну від Portal (static export), Shop використовує SSR — продукти можна рендерити на сервері, що дає SEO.

Чому Zustand? Кошик потрібно зберігати між сторінками та між сесіями (localStorage). Zustand з persist middleware — той самий підхід що і в DOP (eswf-tabs, eswf-auth, eswf-ui).

Чому без React Query? Shop — простий каталог без складного кешування. Базового useState + useEffect + axios достатньо. React Query додали б у DOP-стилі, якщо каталог виросте.


Структура файлів

frontend/shop/
├── app/                          # Next.js App Router
│   ├── layout.tsx                # Корневий layout: metadata, globals.css, bg-gray-950
│   ├── page.tsx                  # Головна: Hero + Features + Available Products
│   ├── not-found.tsx             # 404 сторінка
│   ├── globals.css               # Tailwind + Inter font + анімації
│   ├── catalog/
│   │   └── page.tsx              # Каталог: фільтри + grid всіх продуктів
│   ├── products/
│   │   └── [slug]/
│   │       └── page.tsx          # Деталь продукту + sticky buy panel
│   ├── cart/
│   │   └── page.tsx              # Кошик + order summary
│   ├── checkout/
│   │   └── page.tsx              # Оформлення замовлення (вимагає auth)
│   ├── orders/
│   │   └── page.tsx              # Мої замовлення (вимагає auth)
│   └── login/
│       └── page.tsx              # Форма входу (DOP credentials)
├── components/
│   ├── Header.tsx                # Sticky header: logo, nav, cart badge, auth
│   ├── Footer.tsx                # Footer: links, GitHub
│   ├── ProductCard.tsx           # Картка продукту в grid
│   ├── ProductIcon.tsx           # Tabler-іконка за кодом модуля (fleet→truck, etc.)
│   ├── StatusBadge.tsx           # available / coming_soon / discontinued
│   ├── PriceBadge.tsx            # free / paid / subscription + ціна
│   ├── FilterBar.tsx             # Search input + status select + pricing select
│   └── CartButton.tsx            # Іконка кошика з badge лічильника
├── lib/
│   ├── types.ts                  # TypeScript інтерфейси: Product, CartItem, ShopOrder, User...
│   └── api.ts                    # Axios клієнт + функції: getProducts, getProduct, createOrder...
├── store/
│   ├── cartStore.ts              # Zustand: items, addItem, removeItem, clearCart, getTotal
│   └── authStore.ts              # Zustand: user, accessToken, refreshToken, isAuthenticated
├── .env.development              # BACKEND_URL=localhost:8000, DOP/News/Portal URLs
├── .env.production               # api.eswf.dev, erp.eswf.dev, etc.
├── next.config.ts                # rewrites /api/* → backend, images domains
├── tailwind.config.ts
├── tsconfig.json
├── postcss.config.mjs
└── package.json                  # "dev": "next dev -p 5175"

Сторінки

/ — Home

Файл: app/page.tsx (Server Component — SSR)

  • Hero — заголовок, підзаголовок, CTA-кнопки "Browse Catalog" і "Try DOP Demo"
  • Core Modules — 4 статичні картки: Essentials, Fleet, Logistic, Analytics
  • Available Modules — сервер-рендерить перші 6 доступних продуктів з API (getProducts({ status: "available" }))
  • Empty state — якщо бекенд недоступний під час білда, відображає заглушку

Фон: градієнт blue-950/30 → gray-950 → green-950/20 + radial-gradient.


/catalog — Каталог

Файл: app/catalog/page.tsx (Client Component — "use client")

  • FilterBar — пошук (debounce 300ms), фільтр за status, фільтр за pricing
  • Запит до GET /api/v1/shop/products/?status=...&pricing=...&search=...
  • Grid 1 → 2 → 3 колонки з анімацією по черзі (animationDelay: i * 40ms)
  • Loading spinner, empty state, error + retry

/products/[slug] — Деталь продукту

Файл: app/products/[slug]/page.tsx (Client Component)

  • Лівий стовпець (2/3): назва, badges, short_description, повний description (+ ua), деталі (code, status, pricing, version, updated_at)
  • Правий стовпець (1/3, sticky): ціна, кнопка "Add to Cart" / "Added" / "Coming Soon" / "Discontinued"
  • При натисканні "Add to Cart" → useCartStore.addItem() → кнопка стає "Added to Cart" + з'являється "View Cart"

/cart — Кошик

Файл: app/cart/page.tsx (Client Component)

  • Список CartItem з іконкою, назвою, ціною, кнопкою видалення
  • Order Summary: перелік + total
  • Кнопка "Proceed to Checkout" (якщо isAuthenticated) або "Sign In to Checkout"
  • "Clear all" видаляє весь кошик

/checkout — Оформлення

Файл: app/checkout/page.tsx (Client Component)

  • Guard: якщо не авторизований → екран "Sign In Required"
  • Guard: якщо кошик порожній → редирект
  • Показує order summary + дані акаунту
  • POST /api/v1/shop/orders/ з { items: [{ product, price }] }
  • При успіху → clearCart() → екран "Order Placed!" з іконкою ✓
  • При помилці → inline error message

/orders — Мої замовлення

Файл: app/orders/page.tsx (Client Component)

  • Guard: якщо не авторизований → екран "Sign In Required"
  • GET /api/v1/shop/orders/ (відфільтровано по user на бекенді)
  • Кожне замовлення: id, статус-badge (pending/paid/cancelled), дата, total, список items
  • Empty state з посиланням на каталог

/login — Авторизація

Файл: app/login/page.tsx (Client Component)

  • Форма: username + password (з toggle показу)
  • POST /api/v1/auth/login/{ access, refresh }
  • Потім GET /api/v1/auth/me/ → user object
  • Зберігає в authStore (persisted: eswf-shop-auth)
  • Після входу — редирект на ?redirect=... або /
  • Якщо вже авторизований — одразу редирект

State Management

cartStore (Zustand + persist → eswf-shop-cart)

items: CartItem[]           // { product: Product, quantity: number }[]
addItem(product)            // додає, якщо ще немає (дублі не допускаються)
removeItem(productId)       // видаляє за id
clearCart()                 // очищує
getTotal()                  // сума всіх items як string "123.00"
getCount()                  // кількість позицій
hasItem(productId)          // чи є продукт у кошику

authStore (Zustand + persist → eswf-shop-auth)

user: User | null
accessToken: string | null
refreshToken: string | null
isAuthenticated: boolean
login(user, access, refresh)
logout()

API Client

Файл: lib/api.ts

Базовий URL: /api/v1 (проксується Next.js rewrites → localhost:8000/api/v1)

Interceptor: зчитує accessToken з localStorage (eswf-shop-authstate.accessToken) і додає Authorization: Bearer <token>.

Функція Endpoint Auth
getProducts(filters?) GET /shop/products/ Ні (public)
getProduct(slug) GET /shop/products/{slug}/ Ні (public)
createOrder(items) POST /shop/orders/ Так
getOrders() GET /shop/orders/ Так
login(username, password) POST /auth/login/ Ні
getCurrentUser() GET /auth/me/ Так

Компоненти

ProductCard

Показує: - ProductIcon — Tabler icon підібрана за product.code (fleet→IconTruck, essentials→IconFileInvoice, etc.) - Назва + версія - StatusBadge + PriceBadge - Кнопка залежно від стану: - available + не в кошику → "Add to Cart" або "Get Free" - available + у кошику → "In Cart" (disabled, зелений) - coming_soon → "Coming Soon" (yellow, disabled) - discontinued → "Discontinued" (gray, disabled)

FilterBar

  • Search: debounce 300ms в parent через useCallback + useEffect + setTimeout
  • Status select: "" / available / coming_soon / discontinued
  • Pricing select: "" / free / paid / subscription
  • Clear button: з'являється тільки коли є активні фільтри
  • Logo (SVG, той самий що в Portal) + "ESWF Shop"
  • Nav links: Home, Catalog, My Orders (підсвічується active route)
  • CartButton — іконка з синім badge (лічильник items)
  • Якщо авторизований: ім'я + logout
  • Якщо не авторизований: "Sign In" кнопка

Next.js Config

// next.config.ts
rewrites: [{ source: "/api/:path*", destination: "http://localhost:8000/api/:path*" }]
images.remotePatterns: [localhost:8000/media/**, https://api.eswf.dev/media/**]

Без output: "export" — SSR/SSG, щоб: 1. Головна сторінка рендерить продукти на сервері (SEO) 2. API rewrites працюють (недоступно в static export) 3. Dynamic routes /products/[slug] без generateStaticParams


Стилізація

Повністю узгоджено з Portal: - bg-gray-950 — основний фон (темніше ніж Portal's bg-gray-900) - border-white/10, bg-white/5 — скляні картки - text-blue-400 / bg-blue-600 — акценти - hover:-translate-y-1 + hover:shadow-[0_16px_48px_rgba(74,158,255,0.12)] — hover ефекти - Анімації: fadeIn, slideFromTop, slideFromLeft (ті самі keyframes що в Portal) - Шрифт: Inter (Google Fonts) - Іконки: @tabler/icons-react (stroke=1.5)


Env Variables

.env.development

NEXT_PUBLIC_DOP_URL=http://localhost:5173
NEXT_PUBLIC_NEWS_URL=http://localhost:5174
NEXT_PUBLIC_PORTAL_URL=http://localhost:3000
BACKEND_URL=http://localhost:8000

.env.production

NEXT_PUBLIC_DOP_URL=https://erp.eswf.dev
NEXT_PUBLIC_NEWS_URL=https://news.eswf.dev
NEXT_PUBLIC_PORTAL_URL=https://eswf.dev
BACKEND_URL=https://api.eswf.dev

BACKEND_URL — серверна (для rewrites), без NEXT_PUBLIC_ префіксу. NEXT_PUBLIC_* — доступні на клієнті.


Launcher

До launcher.js додано:

{
  name: "Shop",
  cwd: path.join(ROOT, "frontend", "shop"),
  command: "npm.cmd",
  args: ["run", "dev"],
  url: "http://localhost:5175",
  color: "\x1b[32m", // green
}


Бекенд (вже існував)

Shop використовує наявний Django app backend/shop/:

Модель Опис
Product Модуль/продукт: name, slug, code, description, status, pricing, price
ShopOrder Замовлення: user, tenant, status (pending/paid/cancelled), total
ShopOrderItem Позиція замовлення: order, product, price
License Ліцензія: tenant, product, is_active, expires_at

Відкриті endpoints (AllowAny): GET /api/v1/shop/products/, GET /api/v1/shop/products/{slug}/ Захищені (IsAuthenticated): orders, licenses


Запуск

cd c:\eswf\frontend\shop
npm install
npm run dev
# → http://localhost:5175

або через launcher:

cd c:\eswf
node launcher.js


Що ще можна додати

Функція Пріоритет
Refresh token логіка (auto-refresh при 401) Середній
Пагінація каталогу (next/previous) Середній
Категорії продуктів (потрібна модель на бекенді) Низький
Сторінка "My Licenses" Низький
Stripe/LiqPay інтеграція для оплати Низький
Рейтинги і відгуки Низький
i18n (en/ua) через next-i18next Низький
ISR (Incremental Static Regeneration) для каталогу Низький