11 KiB
11 KiB
Архитектура Calculator
Дерево файлов
calculator/
├── service.yaml # Конфиг для ci-templates (тип, домен, ресурсы)
├── .drone.yml # CI/CD пайплайн (копия base.drone.yml)
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── public/
│ └── favicon.svg
└── src/
├── main.tsx # Точка входа, рендер <App />
├── App.tsx # Роутинг: /login → /calculator
├── App.css # Глобальные стили (reset, body, центрирование)
│
├── context/
│ └── AuthContext.tsx # React Context + Provider для псевдоавторизации
│
├── hooks/
│ ├── useCalculator.ts # Стейт-машина калькулятора (вся логика)
│ └── useKeyboard.ts # Слушатель клавиатуры → маппинг на действия
│
├── utils/
│ └── calculate.ts # Чистые функции вычислений
│
├── pages/
│ ├── LoginPage.tsx # Страница входа
│ ├── LoginPage.module.css
│ ├── CalculatorPage.tsx # Страница калькулятора (шапка + калькулятор)
│ └── CalculatorPage.module.css
│
└── components/
├── Calculator/
│ ├── Calculator.tsx # Обёртка: Display + Keypad
│ └── Calculator.module.css
├── Display/
│ ├── Display.tsx # Выражение + результат
│ └── Display.module.css
├── Keypad/
│ ├── Keypad.tsx # Сетка кнопок
│ └── Keypad.module.css
└── Button/
├── Button.tsx # Кнопка калькулятора
└── Button.module.css
Схема компонентов
<App>
<AuthProvider> ← context/AuthContext.tsx
<Routes>
/login → <LoginPage /> ← Форма логина
/ → <ProtectedRoute> ← Редирект на /login если нет юзера
<CalculatorPage />
├── Header (логин + выход)
└── <Calculator />
├── <Display /> ← expression + result
└── <Keypad /> ← сетка <Button />
</ProtectedRoute>
</Routes>
</AuthProvider>
</App>
Слои приложения
1. AuthContext — псевдоавторизация
AuthContext {
user: string | null // текущий логин
login(name: string): void // сохранить в localStorage, установить user
logout(): void // очистить localStorage, user = null
}
- При инициализации читает
localStorage.getItem('calculator_user') login()— записывает в localStorage, обновляет стейтlogout()— удаляет из localStorage, обновляет стейтProtectedRoute— обёртка, редиректит на/loginеслиuser === null
2. useCalculator — стейт-машина
Управляет всем состоянием калькулятора. Чистая логика, без побочных эффектов.
State {
display: string // текущее отображаемое число ("0")
expression: string // строка выражения ("12 + 3")
operator: string | null // текущий оператор
prevValue: number | null // предыдущее значение
waitingForOperand: boolean
lastResult: boolean // только что получили результат
}
Actions (dispatch):
inputDigit(digit: string)
inputDecimal()
inputOperator(op: string)
calculate()
clear()
backspace()
toggleSign()
percent()
Логика переходов:
[Ввод цифры]
waitingForOperand=true → display = digit, waiting = false
waitingForOperand=false → display = display + digit (макс 16 символов)
[Оператор (+, -, *, /)]
prevValue=null → сохранить display как prevValue, запомнить оператор
prevValue!=null → вычислить промежуточный результат, показать его
[Равно]
Вычислить prevValue {operator} display, показать результат
[C] → полный сброс к начальному состоянию
[Backspace] → удалить последний символ из display (минимум "0")
[+/-] → сменить знак display
[%] → display = display / 100
3. useKeyboard — слушатель клавиатуры
useKeyboard(actions: CalculatorActions): Map<string, string>
- Вешает
keydownlistener наwindow - Маппинг клавиш → действий:
| key | action |
|---|---|
0-9 |
inputDigit(key) |
. |
inputDecimal() |
+ - * / |
inputOperator(key) |
Enter, = |
calculate() |
Escape |
clear() |
Backspace |
backspace() |
% |
percent() |
- Возвращает
activeKey(текущая нажатая клавиша) для подсветки кнопки preventDefault()для/(чтобы не открывался поиск в браузере)
4. calculate.ts — вычисления
function calculate(a: number, operator: string, b: number): number
- Чистая функция, без стейта
- Обработка деления на ноль → возвращает
Infinity(дисплей покажет "Error") - Округление для борьбы с погрешностью float (0.1 + 0.2)
5. Компоненты
Display — два блока:
expression— мелкий шрифт, выражение сверхуresult— крупный шрифт, текущее число/результат снизу- Auto-fit:
font-sizeуменьшается при длине > 10 символов
Keypad — CSS Grid 4x5:
| C | +/- | % | ÷ |
| 7 | 8 | 9 | × |
| 4 | 5 | 6 | − |
| 1 | 2 | 3 | + |
| 0 (span 2) | . | = |
Button — три варианта стиля:
default— цифры (тёмно-серый фон)function— C, +/-, % (светло-серый фон)operator— +, -, ×, ÷, = (акцентный цвет)- Пропс
active— подсветка при нажатии с клавиатуры
Адаптивность
/* Калькулятор */
max-width: 360px; /* ограничение на больших экранах */
width: calc(100vw - 32px); /* на маленьких — почти вся ширина */
margin: 0 auto;
/* Центрирование на экране */
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* Кнопки */
min-height: 56px; /* тач-friendly */
font-size: clamp(16px, 4vw, 20px);
/* Дисплей результата */
font-size: clamp(28px, 8vw, 40px);
Поток данных
[Keyboard Event] [Button Click]
│ │
▼ ▼
useKeyboard ──── action ────→ useCalculator (useReducer)
│ │
│ activeKey │ state
▼ ▼
<Keypad> ◄──── props ──── <Calculator>
│ │
▼ ▼
<Button active?> <Display expression, result>
Клавиатура и клики вызывают одни и те же actions хука useCalculator. Хук useKeyboard дополнительно пробрасывает activeKey для визуальной подсветки.
Деплой и CI/CD
Инфраструктура
Домен: test-calculator.vigdorov.ru
Namespace: test-calculator
Тип: web-frontend (SPA → nginx)
Registry: registry.vigdorov.ru/library/test-calculator
Ingress: Traefik + wildcard-cert
Пайплайн (Drone CI)
git push (master)
│
▼
[prepare] Клонирует ci-templates → .ci/
Парсит service.yaml
Копирует react.Dockerfile + spa.conf
Генерирует .ci/env
│
▼
[build] Kaniko собирает образ:
- Stage 1: node:20-alpine → npm ci → npm run build
- Stage 2: nginx:alpine + dist + spa.conf
Push в Harbor: registry.vigdorov.ru/library/test-calculator:<sha>
│
▼
[deploy] Helm upgrade --install в namespace test-calculator
Chart: ci-templates/helm-charts/frontend/
Ingress: test-calculator.vigdorov.ru → Service:80
│
▼
[notify] Telegram уведомление
service.yaml
service:
name: test-calculator
type: web-frontend
deploy:
namespace: test-calculator
domain: test-calculator.vigdorov.ru
Минимальная конфигурация. ci-templates применит дефолты:
frontend.framework: reactfrontend.resources.cpu: 100mfrontend.resources.memory: 128Mifrontend.replicas: 1- Dockerfile:
react.Dockerfile(multi-stage: node → nginx) - Nginx:
spa.conf(gzip, кэш статики, SPA fallback, /health)
Helm-чарт (frontend/)
Deployment
└── Pod (nginx:alpine)
├── /usr/share/nginx/html ← собранный SPA
└── /etc/nginx/nginx.conf ← spa.conf
Service (ClusterIP:80)
└── → Pod:80
Ingress (Traefik)
└── test-calculator.vigdorov.ru → Service:80
TLS: wildcard-cert (автоматически отражён в namespace)
Схема сети
Браузер
│
│ HTTPS
▼
DNS (cishost.ru) → VDS Nginx (reg.ru) → Keenetic Router
│
▼
K3s Traefik
│
┌───────────────┘
▼
Ingress: test-calculator.vigdorov.ru
│
▼
Service: test-calculator (ClusterIP:80)
│
▼
Pod: nginx (SPA)