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

News & Insights (news.eswf.dev) — Кроки імплементації

Огляд

News — це блог/новинний сайт про IT в логістиці, DOP-системи та автоматизацію бізнесу. Технології: React 19 + TypeScript + Vite 7 + Tailwind CSS v4 + React Router 7. Дизайн: темна тема, консистентна з порталом (eswf.dev).

Поки backend не реалізовано — використовуються mock-дані. API-клієнт підготовлений для інтеграції з Django REST (/api/v1/news/).


Крок 1. Scaffold проекту

cd frontend/news
npx create-vite@latest . -- --template react-ts
npm install

Vite створює базову структуру React + TypeScript проекту з конфігами tsconfig.json, tsconfig.app.json, tsconfig.node.json, vite.config.ts.

Залежності

# Runtime
npm install react-router-dom axios @tabler/icons-react

# Dev (Tailwind v4 + typography plugin)
npm install -D tailwindcss @tailwindcss/vite @tailwindcss/typography autoprefixer postcss
Пакет Призначення
react-router-dom Клієнтська маршрутизація (SPA)
axios HTTP-клієнт для API (готовий до підключення backend)
@tabler/icons-react Іконки (як у portal)
tailwindcss v4 Утилітарний CSS фреймворк
@tailwindcss/vite Інтеграція Tailwind v4 через Vite plugin (замість PostCSS)
@tailwindcss/typography Стилізація prose-контенту (тексти статей)

Крок 2. Конфігурація

vite.config.ts — Vite конфіг

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'

export default defineConfig({
  plugins: [react(), tailwindcss()],  // Tailwind v4 як Vite plugin
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),  // Path alias: @/ → ./src/
    },
  },
  server: {
    port: 5174,  // Окремий порт (portal=3000, erp=5173, news=5174)
    proxy: {
      '/api': {
        target: 'http://localhost:8000',  // Django backend
        changeOrigin: true,
      },
    },
  },
})

Ключові рішення: - Tailwind v4 через Vite plugin — швидше за PostCSS підхід, нативна інтеграція - Path alias @/ — дозволяє писати import X from '@/components/X' замість відносних шляхів - Proxy — в dev-режимі /api/* запити проксуються на Django (порт 8000)

tsconfig.app.json — TypeScript path aliases

Додано baseUrl та paths для підтримки @/ alias:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

src/index.css — Tailwind v4 + шрифти

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
@import "tailwindcss";
@plugin "@tailwindcss/typography";

@theme {
  --font-sans: 'Inter', system-ui, sans-serif;
}

body {
  font-family: 'Inter', system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

Tailwind v4 відмінності від v3: - @import "tailwindcss" замість @tailwind base/components/utilities - @plugin замість plugins: [] в конфіг-файлі - @theme {} замість tailwind.config.ts → theme.extend - Конфіг-файл tailwind.config.ts не потрібен — все в CSS

index.html — точка входу

<html lang="uk">
  <body class="bg-gray-950 text-white">
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Темна тема задається прямо на <body>: bg-gray-950 text-white.


Крок 3. Типи та mock-дані

src/types/index.ts — TypeScript інтерфейси

export interface Category {
  id: number;
  name: string;       // "Технології"
  slug: string;       // "tech"
  description: string;
  color: string;      // "blue" | "green" | "purple" | "amber" — для CategoryBadge
}

export interface Tag {
  id: number;
  name: string;  // "AI"
  slug: string;  // "ai"
}

export interface Article {
  id: number;
  title: string;
  slug: string;        // URL-friendly ідентифікатор
  excerpt: string;     // Короткий опис для картки
  content: string;     // Повний текст (markdown-подібний)
  coverImage: string;  // URL зображення (поки порожній)
  category: Category;  // Зв'язок з категорією
  tags: Tag[];         // Масив тегів
  author: string;
  publishedAt: string; // ISO date
  readTime: number;    // Час читання в хвилинах
}

Чому slug а не id для URL: SEO-friendly URL (/article/ai-logistics-2026 замість /article/1).

src/data/mock.ts — тестові дані

4 категорії, 8 тегів, 6 статей на тематику IT + логістика:

# Стаття Категорія Теги
1 Як AI змінює логістику у 2026 Логістика AI, Fleet
2 DOP для малого бізнесу: з чого почати DOP Python, Django
3 GPS-моніторинг: ROI для автопарку Логістика GPS, Fleet
4 Електронні ТТН в Україні Логістика eTTN, Fleet
5 React vs Angular: вибір для enterprise Технології React, TypeScript
6 Оптимізація маршрутів: алгоритми та практика Аналітика AI, Fleet

Контент статей — реалістичний текст з markdown-заголовками (##, ###), списками (-), жирним текстом (**...**).


Крок 4. API-клієнт

src/api/client.ts

const api = axios.create({
  baseURL: '/api/v1/news',
});

const USE_MOCK = true;  // Перемикач: true = mock-дані, false = Django API

export async function getArticles(): Promise<Article[]> { ... }
export async function getArticle(slug: string): Promise<Article | undefined> { ... }
export async function getCategories(): Promise<Category[]> { ... }
export async function getArticlesByCategory(categorySlug: string): Promise<Article[]> { ... }

Архітектура: кожна функція перевіряє USE_MOCK флаг. Коли backend буде готовий — достатньо змінити USE_MOCK = false, і всі запити підуть на Django REST API:

  • GET /api/v1/news/articles/ — список статей
  • GET /api/v1/news/articles/{slug}/ — одна стаття
  • GET /api/v1/news/categories/ — категорії
  • GET /api/v1/news/articles/?category={slug} — фільтр за категорією

Крок 5. Компоненти

src/components/Layout.tsx — каркас сторінки

Обгортка для всіх сторінок з Header, Footer та <Outlet /> для React Router.

Header: - Логотип ESWF.NEWS (стиль як portal: синій ESWF + зелений NEWS) - Навігація: Головна + посилання на категорії (приховані на мобільних) - Кнопка "← eswf.dev" для повернення на портал - sticky top-0 + backdrop-blur-md — залишається зверху при скролі

Footer: - Копірайт + афоризм (як у portal)

src/components/ArticleCard.tsx — картка статті

Дизайн-система з portal: - rounded-2xl border border-white/10 bg-white/5 backdrop-blur-sm — скляний ефект - hover:-translate-y-1 hover:border-blue-400/30 — підйом при hover - Gradient line зверху (opacity 0 → 1 при hover) - Cover placeholder з великим "ESWF" watermark - Category badge в лівому нижньому куті cover - Мета: автор (IconUser), час читання (IconClock), дата

src/components/CategoryBadge.tsx — badge категорії

Кольоровий pill з маппінгом color → Tailwind класи: - bluebg-blue-500/15 text-blue-400 border-blue-500/30 - greenbg-emerald-500/15 text-emerald-400 border-emerald-500/30 - purplebg-purple-500/15 text-purple-400 border-purple-500/30 - amberbg-amber-500/15 text-amber-400 border-amber-500/30

Клікабельний — переходить на сторінку категорії.

src/components/TagBadge.tsx — badge тегу

Простий label: bg-white/5 border-white/10, текст #AI, #React тощо.

src/components/SearchBar.tsx — пошук

Input з іконкою пошуку (IconSearch). Controlled компонент (value + onChange). Стилі: border-white/10 bg-white/5, при фокусі focus:border-blue-500/50 focus:bg-white/10.


Крок 6. Сторінки

src/pages/Home.tsx — головна (стрічка статей)

  1. Hero секція — заголовок "News & Insights" з іконкою + підзаголовок
  2. SearchBar — пошук по заголовку, excerpt та тегах (useMemo для фільтрації)
  3. Grid статейgrid-cols-1 md:grid-cols-2 lg:grid-cols-3
  4. Empty state — "Статей не знайдено" коли пошук не дає результатів

src/pages/Article.tsx — сторінка статті

  1. "← Усі статті" — посилання назад
  2. Category badge + заголовок + excerpt
  3. Мета — автор, дата (повна, українською), час читання
  4. Контент — простий markdown-рендерер (## → h2, ### → h3, - → li, ... → bold)
  5. Теги — список TagBadge внизу
  6. Схожі статті — до 2 статей тієї ж категорії

Markdown-рендерер — спрощений, парсить рядок за рядком. Для production варто замінити на react-markdown або remark.

src/pages/Category.tsx — сторінка категорії

  1. "← Усі статті" — посилання назад
  2. Назва категорії + опис + кількість статей
  3. Grid статей — відфільтровані за category.slug
  4. Empty state — якщо в категорії немає статей

Крок 7. Маршрутизація (App.tsx)

<BrowserRouter>
  <Routes>
    <Route element={<Layout />}>           {/* Обгортка з Header/Footer */}
      <Route index element={<Home />} />                    {/* / */}
      <Route path="article/:slug" element={<Article />} />  {/* /article/ai-logistics-2026 */}
      <Route path="category/:slug" element={<Category />} /> {/* /category/tech */}
    </Route>
  </Routes>
</BrowserRouter>

Nested routing: Layout рендерить <Outlet />, в який React Router підставляє активну сторінку. Header і Footer залишаються спільними для всіх маршрутів.


Крок 8. Збірка та перевірка

cd frontend/news
npm run build   # tsc -b && vite build — компіляція TS + production bundle
npm run dev     # http://localhost:5174 — dev server з HMR

Результат build: - dist/index.html — 0.52 KB - dist/assets/index-*.css — 37.77 KB (Tailwind CSS) - dist/assets/index-*.js — 256.19 KB (React + Router + Icons + код)


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

frontend/news/
├── index.html                     # HTML точка входу (dark theme на body)
├── package.json                   # Залежності та скрипти
├── vite.config.ts                 # Vite + Tailwind plugin + proxy + aliases
├── tsconfig.json                  # Посилання на app/node конфіги
├── tsconfig.app.json              # TS конфіг з path aliases (@/)
├── tsconfig.node.json             # TS конфіг для Vite/Node
├── eslint.config.js               # ESLint (від Vite scaffold)
├── src/
│   ├── main.tsx                   # Entry point: ReactDOM.createRoot
│   ├── App.tsx                    # BrowserRouter + Routes
│   ├── index.css                  # Tailwind v4 + Inter font + typography
│   │
│   ├── types/
│   │   └── index.ts               # Article, Category, Tag інтерфейси
│   │
│   ├── data/
│   │   └── mock.ts                # 6 статей, 4 категорії, 8 тегів
│   │
│   ├── api/
│   │   └── client.ts              # Axios + mock fallback (USE_MOCK flag)
│   │
│   ├── components/
│   │   ├── Layout.tsx             # Header (ESWF.NEWS) + Footer + Outlet
│   │   ├── ArticleCard.tsx        # Картка статті (hover animations)
│   │   ├── CategoryBadge.tsx      # Кольоровий pill категорії
│   │   ├── TagBadge.tsx           # Badge тегу (#AI, #React)
│   │   └── SearchBar.tsx          # Input пошуку з іконкою
│   │
│   └── pages/
│       ├── Home.tsx               # Стрічка статей + пошук
│       ├── Article.tsx            # Повна стаття + related
│       └── Category.tsx           # Статті за категорією
└── dist/                          # Production build output

Дизайн-система (консистентність з Portal)

Елемент Значення
Фон bg-gray-950
Текст основний text-white
Текст вторинний text-gray-400
Текст третинний text-gray-500 / text-gray-600
Акцент primary text-blue-400 / text-blue-700
Акцент secondary text-green-800
Бордери border-white/10
Hover бордер border-blue-400/30
Фон карток bg-white/5
Hover фон bg-white/10
Заокруглення rounded-2xl (картки), rounded-xl (input)
Backdrop backdrop-blur-sm (картки), backdrop-blur-md (header)
Шрифт Inter (Google Fonts)
Іконки @tabler/icons-react

Крок 9. Динамічні URL між сайтами (Environment Variables)

Проблема

Проект складається з декількох окремих додатків, кожен на своєму порті:

Сайт Dev (localhost) Production
Portal http://localhost:3000 https://eswf.dev
News http://localhost:5174 https://news.eswf.dev
Shop http://localhost:5175 https://shop.eswf.dev
DOP http://localhost:5173 https://erp.eswf.dev

Якщо захардкодити production URL (https://news.eswf.dev), посилання не працюватимуть в dev-середовищі. Потрібен механізм, щоб URL автоматично змінювались залежно від оточення.

Рішення: .env.development / .env.production

Обидва фреймворки (Next.js та Vite) автоматично завантажують відповідний .env файл: - npm run dev → читає .env.development - npm run build → читає .env.production

Важливо: префікси змінних відрізняються: - Next.js (portal): NEXT_PUBLIC_ — робить змінну доступною в клієнтському коді - Vite (news): VITE_ — аналогічно, змінна потрапляє в import.meta.env

Portal (Next.js) — посилання на під-сайти

frontend/portal/.env.development:

NEXT_PUBLIC_NEWS_URL=http://localhost:5174
NEXT_PUBLIC_SHOP_URL=http://localhost:5175
NEXT_PUBLIC_DOP_URL=http://localhost:5173

frontend/portal/.env.production:

NEXT_PUBLIC_NEWS_URL=https://news.eswf.dev
NEXT_PUBLIC_SHOP_URL=https://shop.eswf.dev
NEXT_PUBLIC_DOP_URL=https://erp.eswf.dev

components/CardLinks.tsx — використання:

const cards = [
  {
    title: "News & Insights",
    href: process.env.NEXT_PUBLIC_NEWS_URL || "https://news.eswf.dev",
    // ...
  },
  {
    title: "Solution Catalog",
    href: process.env.NEXT_PUBLIC_SHOP_URL || "https://shop.eswf.dev",
    // ...
  },
  {
    title: "DOP Demo Lab",
    href: process.env.NEXT_PUBLIC_DOP_URL || "https://erp.eswf.dev",
    // ...
  },
];

process.env.NEXT_PUBLIC_* — Next.js інлайнить значення на етапі збірки. Fallback || "https://..." гарантує роботу навіть якщо .env файл відсутній.

News (Vite) — посилання назад на портал

frontend/news/.env.development:

VITE_PORTAL_URL=http://localhost:3000

frontend/news/.env.production:

VITE_PORTAL_URL=https://eswf.dev

src/components/Layout.tsx — використання:

const PORTAL_URL = import.meta.env.VITE_PORTAL_URL || 'https://eswf.dev';

// В JSX:
<a href={PORTAL_URL}>
  <IconArrowLeft size={14} />
  eswf.dev
</a>

import.meta.env.VITE_* — Vite інлайнить значення під час збірки. Змінні без префіксу VITE_ ігноруються з міркувань безпеки (щоб випадково не зливати серверні секрети в клієнтський код).

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

frontend/portal/
├── .env.development          # Dev URL: localhost:5174, localhost:5175, localhost:5173
├── .env.production           # Prod URL: news.eswf.dev, shop.eswf.dev, erp.eswf.dev
└── components/
    └── CardLinks.tsx          # process.env.NEXT_PUBLIC_NEWS_URL

frontend/news/
├── .env.development          # Dev URL: localhost:3000 (portal)
├── .env.production           # Prod URL: eswf.dev (portal)
└── src/components/
    └── Layout.tsx             # import.meta.env.VITE_PORTAL_URL

Перевірка

  1. Запустити portal: cd frontend/portal && npm run devhttp://localhost:3000
  2. Запустити news: cd frontend/news && npm run devhttp://localhost:5174
  3. На portal клікнути "News & Insights" → відкриється localhost:5174
  4. На news клікнути "← eswf.dev" → повернення на localhost:3000

Додавання нових сайтів

При додаванні нового під-сайту (наприклад, shop): 1. Додати NEXT_PUBLIC_SHOP_URL в обидва .env файли portal 2. Додати VITE_PORTAL_URL в .env файли нового сайту 3. За потреби додати cross-links між іншими під-сайтами


Наступні кроки

  1. Backend — створити Django app news/ з моделями Article, Category, Tag
  2. API — реалізувати ендпоінти /api/v1/news/articles/ та /api/v1/news/categories/
  3. Переключити USE_MOCK = false в api/client.ts
  4. Зображення — додати cover images для статей
  5. Пагінація — для великої кількості статей
  6. SEO — розглянути міграцію на Next.js або додати SSR