feat: migrate to Next.js for Mini App and enhance project structure
- Replaced the previous webapp with a new Mini App built using Next.js, improving performance and maintainability. - Updated the `.gitignore` to exclude Next.js build artifacts and node modules. - Revised documentation in `AGENTS.md`, `README.md`, and `architecture.md` to reflect the new Mini App structure and technology stack. - Enhanced Dockerfile to support the new build process for the Next.js application. - Updated CI workflow to build and test the Next.js application. - Added new configuration options for the Mini App, including `MINI_APP_SHORT_NAME` for improved deep linking. - Refactored frontend testing setup to accommodate the new structure and testing framework. - Removed legacy webapp files and dependencies to streamline the project.
This commit is contained in:
@@ -1,93 +1,53 @@
|
||||
---
|
||||
description: Rules for working with the Telegram Mini App frontend (webapp/)
|
||||
description: Rules for working with the Telegram Mini App frontend (webapp-next/)
|
||||
globs:
|
||||
- webapp/**
|
||||
- webapp-next/**
|
||||
---
|
||||
|
||||
# Frontend — Telegram Mini App
|
||||
# Frontend — Telegram Mini App (Next.js)
|
||||
|
||||
## Module structure
|
||||
The Mini App lives in `webapp-next/`. It is built as a static export and served by FastAPI at `/app`.
|
||||
|
||||
All source lives in `webapp/js/`. Each module has a single responsibility:
|
||||
## Stack
|
||||
|
||||
| Module | Responsibility |
|
||||
|--------|---------------|
|
||||
| `main.js` | Entry point: theme init, auth gate, `loadMonth`, navigation, swipe gestures, sticky scroll |
|
||||
| `dom.js` | Lazy DOM getters (`getCalendarEl()`, `getDutyListEl()`, etc.) and shared mutable `state` |
|
||||
| `i18n.js` | `MESSAGES` dictionary (en/ru), `getLang()`, `t()`, `monthName()`, `weekdayLabels()` |
|
||||
| `auth.js` | Telegram `initData` extraction (`getInitData`), `isLocalhost`, hash/query fallback |
|
||||
| `theme.js` | Theme detection (`getTheme`), CSS variable injection (`applyThemeParamsToCss`), `initTheme` |
|
||||
| `api.js` | `apiGet`, `fetchDuties`, `fetchCalendarEvents`; timeout, auth header, `Accept-Language` |
|
||||
| `calendar.js` | `dutiesByDate`, `calendarEventsByDate`, `renderCalendar` (6-week grid with indicators) |
|
||||
| `dutyList.js` | `dutyTimelineCardHtml`, `dutyItemHtml`, `renderDutyList` (monthly duty timeline cards) |
|
||||
| `dayDetail.js` | Day detail panel — popover (desktop) or bottom sheet (mobile), `buildDayDetailContent` |
|
||||
| `hints.js` | Tooltip positioning, duty marker hint content, info-button tooltips |
|
||||
| `dateUtils.js` | Date helpers: `localDateString`, `dutyOverlapsLocalDay/Range`, `getMonday`, `formatHHMM`, etc. |
|
||||
| `utils.js` | `escapeHtml` utility |
|
||||
| `constants.js` | `FETCH_TIMEOUT_MS`, `RETRY_DELAY_MS`, `RETRY_AFTER_ACCESS_DENIED_MS` |
|
||||
- **Next.js** (App Router, `output: 'export'`, `basePath: '/app'`)
|
||||
- **TypeScript**
|
||||
- **Tailwind CSS** — theme extended with custom tokens (surface, muted, accent, duty, today, etc.)
|
||||
- **shadcn/ui** — Button, Card, Sheet, Popover, Tooltip, Skeleton, Badge
|
||||
- **Zustand** — app store (month, lang, duties, calendar events, loading, view state)
|
||||
- **@telegram-apps/sdk-react** — SDKProvider, useThemeParams, useLaunchParams, useMiniApp, useBackButton
|
||||
|
||||
## State management
|
||||
## Structure
|
||||
|
||||
A single mutable `state` object is exported from `dom.js`:
|
||||
| Area | Location |
|
||||
|------|----------|
|
||||
| App entry, layout | `src/app/layout.tsx`, `src/app/page.tsx` |
|
||||
| Providers | `src/components/providers/TelegramProvider.tsx` |
|
||||
| Calendar | `src/components/calendar/` — CalendarHeader, CalendarGrid, CalendarDay, DayIndicators |
|
||||
| Duty list | `src/components/duty/` — DutyList, DutyTimelineCard, DutyItem |
|
||||
| Day detail | `src/components/day-detail/` — DayDetail (Sheet/Popover), DayDetailContent |
|
||||
| Current duty view | `src/components/current-duty/CurrentDutyView.tsx` |
|
||||
| Contact links | `src/components/contact/ContactLinks.tsx` |
|
||||
| State views | `src/components/states/` — LoadingState, ErrorState, AccessDenied |
|
||||
| Hooks | `src/hooks/` — use-telegram-theme, use-telegram-auth, use-month-data, use-swipe, use-media-query, use-sticky-scroll, use-auto-refresh |
|
||||
| Lib | `src/lib/` — api, calendar-data, date-utils, phone-format, constants, utils |
|
||||
| i18n | `src/i18n/` — messages.ts, use-translation.ts |
|
||||
| Store | `src/store/app-store.ts` |
|
||||
| Types | `src/types/index.ts` |
|
||||
|
||||
```js
|
||||
export const state = {
|
||||
current: new Date(), // currently displayed month
|
||||
lastDutiesForList: [], // duties array for the duty list
|
||||
todayRefreshInterval: null, // interval handle
|
||||
lang: "en" // 'ru' | 'en'
|
||||
};
|
||||
```
|
||||
## Conventions
|
||||
|
||||
- **No store / pub-sub / reactivity.** `main.js` mutates `state`, then calls
|
||||
render functions (`renderCalendar`, `renderDutyList`) imperatively.
|
||||
- Other modules read `state` but should not mutate it directly.
|
||||
- **Client components:** Use `'use client'` where hooks or browser APIs are used (layout loads config script; page and most UI are client).
|
||||
- **Theme:** CSS variables in `globals.css`; `useTelegramTheme` maps Telegram theme params to `--tg-theme-*` and sets `data-theme` on `<html>`.
|
||||
- **Auth:** `useTelegramAuth` provides initData for API; access gated for non-Telegram except localhost.
|
||||
- **i18n:** `useTranslation()` from store lang; `window.__DT_LANG` set by `/app/config.js` (backend).
|
||||
- **API:** `fetchDuties`, `fetchCalendarEvents` in `src/lib/api.ts`; pass initData, lang, AbortSignal; handle ACCESS_DENIED.
|
||||
|
||||
## HTML rendering
|
||||
## Testing
|
||||
|
||||
- Rendering functions build HTML strings (concatenation + `escapeHtml`) or use
|
||||
`createElement` + `setAttribute`.
|
||||
- Always escape user-controlled text with `escapeHtml()` before inserting via `innerHTML`.
|
||||
- `setAttribute()` handles attribute quoting automatically — do not manually escape
|
||||
quotes for data attributes.
|
||||
- **Runner:** Vitest in `webapp-next/`; environment: jsdom; React Testing Library.
|
||||
- **Config:** `webapp-next/vitest.config.ts`; setup in `src/test/setup.ts`.
|
||||
- **Run:** `cd webapp-next && npm test` (or `npm run test`). Build: `npm run build`.
|
||||
- **Coverage:** Unit tests for lib (api, date-utils, calendar-data, i18n, etc.) and component tests for calendar, duty list, day detail, current duty, states.
|
||||
|
||||
## i18n
|
||||
|
||||
```js
|
||||
t(lang, key, params?) // → translated string
|
||||
```
|
||||
|
||||
- `lang` is `'ru'` or `'en'`, stored in `state.lang`.
|
||||
- `getLang()` reads **backend config** only: `window.__DT_LANG` (set by `/app/config.js` from `DEFAULT_LANGUAGE`). If missing or invalid, falls back to `"en"`. No Telegram or navigator language detection.
|
||||
- Params use named placeholders: `t(lang, "duty.until", { time: "14:00" })`.
|
||||
- Fallback chain: `MESSAGES[lang][key]` → `MESSAGES.en[key]` → raw key string.
|
||||
- All user-visible text must go through `t()` — never hardcode Russian strings in JS.
|
||||
|
||||
## Telegram WebApp SDK
|
||||
|
||||
- Loaded via `<script>` in `index.html` from `https://telegram.org/js/telegram-web-app.js`.
|
||||
- `main.js` calls `Telegram.WebApp.ready()` and `expand()` on startup.
|
||||
- Auth: `initData` from SDK → hash param `tgWebAppData` → query string (fallback chain in `auth.js`).
|
||||
- The `X-Telegram-Init-Data` header carries initData to the backend API.
|
||||
- `requireTelegramOrLocalhost` gates access; localhost is allowed for dev.
|
||||
- Theme changes subscribed via `Telegram.WebApp.onEvent("theme_changed", applyTheme)`.
|
||||
|
||||
## Theme system
|
||||
|
||||
- CSS variables in `:root`: `--bg`, `--surface`, `--text`, `--muted`, `--accent`,
|
||||
`--duty`, `--today`, `--unavailable`, `--vacation`, `--error`, etc.
|
||||
- `[data-theme="light"]` and `[data-theme="dark"]` override base variables.
|
||||
- `theme.js` resolves scheme via:
|
||||
1. `Telegram.WebApp.colorScheme`
|
||||
2. CSS `--tg-color-scheme`
|
||||
3. `prefers-color-scheme` media query
|
||||
- `applyThemeParamsToCss()` maps Telegram `themeParams` to `--tg-theme-*` CSS variables.
|
||||
- Prefer `var(--token)` over hardcoded colors; use `color-mix()` for alpha variants.
|
||||
|
||||
## Testing (frontend)
|
||||
|
||||
- **Runner:** Vitest (`webapp/vitest.config.js`), environment: `happy-dom`.
|
||||
- **Test files:** `webapp/js/**/*.test.js`.
|
||||
- **DOM setup:** Element refs are resolved lazily (getters). Set `document.body.innerHTML`
|
||||
with required DOM structure in `beforeAll`/`beforeEach`; import order of modules using `dom.js` is flexible.
|
||||
- **Run:** `npm run test` (in `webapp/`).
|
||||
Consider these rules when changing the Mini App or adding frontend features.
|
||||
|
||||
@@ -20,7 +20,7 @@ with a Telegram Mini App (webapp) for calendar visualization.
|
||||
┌──────────────┐ HTTP ┌──────────▼───────────┐
|
||||
│ Telegram │◄────────────►│ FastAPI (api/) │
|
||||
│ Mini App │ initData │ + static webapp │
|
||||
│ (webapp/) │ auth └──────────┬───────────┘
|
||||
│ (webapp-next/) │ auth └──────────┬───────────┘
|
||||
└──────────────┘ │
|
||||
┌──────────▼───────────┐
|
||||
│ SQLite + SQLAlchemy │
|
||||
@@ -31,7 +31,7 @@ with a Telegram Mini App (webapp) for calendar visualization.
|
||||
- **Bot:** python-telegram-bot v22, async polling mode.
|
||||
- **API:** FastAPI served by uvicorn in a daemon thread alongside the bot.
|
||||
- **Database:** SQLite via SQLAlchemy 2.x ORM; Alembic for migrations.
|
||||
- **Frontend:** Vanilla JS Telegram Mini App at `/app`, no framework.
|
||||
- **Frontend:** Next.js (TypeScript, Tailwind, shadcn/ui) static export at `/app`; source in `webapp-next/`.
|
||||
|
||||
## Key packages
|
||||
|
||||
@@ -46,7 +46,7 @@ with a Telegram Mini App (webapp) for calendar visualization.
|
||||
| `duty_teller/utils/` | Date helpers, user utilities, HTTP client |
|
||||
| `duty_teller/cache.py` | TTL caches with pattern-based invalidation |
|
||||
| `duty_teller/config.py` | Environment-based configuration |
|
||||
| `webapp/` | Telegram Mini App (HTML + JS + CSS) |
|
||||
| `webapp-next/` | Telegram Mini App (Next.js, Tailwind, shadcn/ui; build → `out/`) |
|
||||
|
||||
## API endpoints
|
||||
|
||||
@@ -103,7 +103,7 @@ Triggered on `v*` tags:
|
||||
## Languages
|
||||
|
||||
- **Backend:** Python 3.12+
|
||||
- **Frontend:** Vanilla JavaScript (ES modules, no bundler)
|
||||
- **Frontend:** Next.js (TypeScript), Tailwind CSS, shadcn/ui; Vitest for tests
|
||||
- **i18n:** Russian (default) and English
|
||||
|
||||
## Version control
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description: Rules for writing and running tests
|
||||
globs:
|
||||
- tests/**
|
||||
- webapp/js/**/*.test.js
|
||||
- webapp-next/src/**/*.test.{ts,tsx}
|
||||
---
|
||||
|
||||
# Testing
|
||||
@@ -51,48 +51,28 @@ def test_get_or_create_user_creates_new(test_db_url):
|
||||
assert user.telegram_user_id == 123
|
||||
```
|
||||
|
||||
## JavaScript tests (Vitest)
|
||||
## Frontend tests (Vitest + React Testing Library)
|
||||
|
||||
### Configuration
|
||||
|
||||
- Config: `webapp/vitest.config.js`.
|
||||
- Environment: `happy-dom` (lightweight DOM implementation).
|
||||
- Test files: `webapp/js/**/*.test.js`.
|
||||
- Run: `npm run test` (from `webapp/`).
|
||||
- Config: `webapp-next/vitest.config.ts`.
|
||||
- Environment: jsdom; React Testing Library for components.
|
||||
- Test files: `webapp-next/src/**/*.test.{ts,tsx}` (co-located or in test files).
|
||||
- Setup: `webapp-next/src/test/setup.ts`.
|
||||
- Run: `cd webapp-next && npm test` (or `npm run test`).
|
||||
|
||||
### DOM setup
|
||||
### Writing frontend tests
|
||||
|
||||
Modules that import from `dom.js` expect DOM elements to exist at import time.
|
||||
Use `beforeAll` to set up the required HTML structure before the first import:
|
||||
|
||||
```js
|
||||
import { beforeAll, describe, it, expect } from "vitest";
|
||||
|
||||
beforeAll(() => {
|
||||
document.body.innerHTML = `
|
||||
<div id="calendar"></div>
|
||||
<h2 id="monthTitle"></h2>
|
||||
<button id="prevBtn"></button>
|
||||
<button id="nextBtn"></button>
|
||||
<div id="dutyList"></div>
|
||||
<div id="dayDetail"></div>
|
||||
<div id="accessDenied" class="hidden"></div>
|
||||
<div id="errorBanner" class="hidden"></div>
|
||||
`;
|
||||
});
|
||||
```
|
||||
|
||||
### Writing JS tests
|
||||
|
||||
- File naming: `webapp/js/<module>.test.js` (co-located with the source module).
|
||||
- Test pure functions first (`dateUtils`, `i18n`, `utils`); mock DOM for render tests.
|
||||
- Use `describe` blocks to group by function, `it` blocks for individual cases.
|
||||
- Pure lib modules: unit test with Vitest (`describe` / `it` / `expect`).
|
||||
- React components: use `@testing-library/react` (render, screen, userEvent); wrap with required providers (e.g. TelegramProvider, store) via `src/test/test-utils.tsx` where needed.
|
||||
- Mock Telegram SDK and API fetch where necessary.
|
||||
- File naming: `<module>.test.ts` or `<Component>.test.tsx`.
|
||||
|
||||
### Example structure
|
||||
|
||||
```js
|
||||
```ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { localDateString } from "./dateUtils.js";
|
||||
import { localDateString } from "./date-utils";
|
||||
|
||||
describe("localDateString", () => {
|
||||
it("formats date as YYYY-MM-DD", () => {
|
||||
|
||||
Reference in New Issue
Block a user