# MaleniaVPN — Frontend Agent Guidance ## Project Overview MaleniaVPN (malenia) is a VPN purchase service built around a **pay-for-device** model: one flat rate per device ("seat"), no tiers. The frontend is a **Russian-language** web app living in `frontend/` on top of a FastAPI backend. This file is the canonical guidance for AI assistants working on it. The service is run by a two-person studio. The brand is minimalist and archival (cream/ink/gold, extreme whitespace), but the *voice* is honest, ironic and sometimes snarky. Keep the UI elegant; keep the copy human. ### Screens | Screen | Route | Access | Source of truth | |---|---|---|---| | Landing | `/` | Public | `.ref/landing.html` (existing hosted page) | | Plan calculator | `/plan` | Public (auth needed only to confirm an order) | `.ref/calculator.html` | | Login / Signup | `/login` | Public | `.ref/login.html` | | Account | `/account` | Protected (auth required) | `.ref/account.html` | ## Authoritative Source Directories (read-only) Never edit these; they are reference material. | Path | What it is | | --- | --- | | `.info/openapi.json` | **The** backend API contract. All types below mirror it. | | `.info/ToV.md` | Company values and tone of voice (Russian). | | `.design/DESIGN.md`, `.design/brand.json` | Visual identity: palette, typography, layout posture. | | `.design/fonts/` | Self-hosted `.woff2` + `fonts.css`. **Latin-only subsets — see Typography.** | | `.design/logos/` | `favicon-0.ico` — the favicon. | | `.ref/*.html` | Ready-made page references: full markup, styles and behaviors. Refactor into React, do not copy verbatim. | The `.design/system/` folder (tokens, `kit.html`, artifacts) is brand-system material; at page build time use the **canonical tokens below** rather than the `--brand-*` derived tokens. ## Tech Stack - Vite + React 19 + TypeScript (`"strict": true`) - React Router v6 - Zustand — **auth tokens and UI state only**, never server-fetched data - Native `fetch` API client (no axios) - Vanilla CSS with custom properties (no Tailwind, no CSS-in-JS, no CSS frameworks) - Vitest + React Testing Library for tests - ESLint (typescript/react/hooks) + Prettier (print width 100, single quotes, trailing commas, semicolons) ## Design System ### Canonical CSS tokens (`src/styles/tokens.css`) | Token | Value | Usage | | --- | --- | --- | | `--bg` | `#faf7f2` | Page background (cream) | | `--surface` | `#f5f0e8` | Cards, raised blocks (warm white) | | `--fg` | `#1a1610` | Primary text, primary buttons (ink) | | `--muted` | `#3d3428` | Secondary text (ink-soft) | | `--fg-muted` | `#7a6e5f` | Tertiary text, captions (ink-muted) | | `--accent` | `#c9a84c` | Highlights, active states, emblems (gold) | | `--accent-light` | `#e8ca7a` | Hover glow, gold-light | | `--accent-dim` | `#8b6914` | Emphasis inside quote text | | `--hairline` | `rgba(26, 22, 16, 0.10)` | Faint structural rules, grid gaps | | `--hairline-strong` | `rgba(26, 22, 16, 0.18)` | Input underlines, card borders | | `--border` | `rgba(201, 168, 76, 0.25)` | Landing header/footer rules | | `--font-display` | `'Cormorant Garamond', Georgia, serif` | Headings, numerals | | `--font-body` | `'Tenor Sans', 'Helvetica Neue', Arial, sans-serif` | Body, labels, buttons | | `--radius` | `8px` | All corners | | `--baseline` | `8px` | Spacing unit | ### Layout rules - **8px baseline grid** for spacing and padding; generous padding everywhere. - **Extreme whitespace + centered alignment**. One primary purpose per screen. - **1px hairlines** for structural division (horizontal rules, stat strips divided by grid `gap: 1px` over a hairline background). - Buttons: uppercase, letterspacing `0.16–0.22em`, 10px font-size, `--radius` corners. Primary = `--fg` background / `--bg` text; hover → `--accent`. Secondary = transparent, `1px solid var(--hairline-strong)`. - Inputs: underline style (`border-bottom: 1px solid var(--hairline-strong)`), transparent background, gold animated focus line (`scaleX`), no boxed inputs. ### Signature UI details (all screens) - **Custom cursor**: gold dot (6px) + trailing ring (32px) that grows to 48px over interactive elements. Hide on touch devices (`@media (hover: none), (pointer: coarse)`) — these also need `* { cursor: auto !important; }`. - **Grain overlay**: fixed `body::before` with the fractal-noise SVG data-URI, `opacity: 0.035`, `mix-blend-mode: multiply`. - **Fade-up reveals**: `.reveal { opacity: 0; transform: translateY(14px); }` → `.is-in`; staggered `transition-delay` via IntersectionObserver. - **Logo**: `Malenia` — Cormorant Garamond, uppercase, `letter-spacing: 0.35em`; the `e` is `var(--accent)`. - **Emblem**: the 90×90 geometric SVG (ring + diamond + cardinal marks), gold stroke. - Respect `prefers-reduced-motion`: disable cursor animation, grain is fine (static), use plain opacity transitions without translate. ### Typography - **Display/headings:** Cormorant Garamond (weights 400, 500, 600 + italics). Large sizes with `line-height 1.1–1.25` and slight negative `letter-spacing`. - **Body:** Tenor Sans. - **CRITICAL — Cyrillic fonts:** the self-hosted `.design/fonts/*.woff2` subsets have `unicode-range` for **Latin Extended only** (no `U+0400–04FF`) and will render Russian text with fallback glyphs. The interface is Russian, so load fonts from **Google Fonts** exactly as the `.ref` pages do: `https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Tenor+Sans&display=swap` (with `preconnect` links). Self-hosting is only acceptable after adding Cyrillic subsets. ## Brand Voice & Copy (from `.info/ToV.md`) - All interface copy is **Russian**. No placeholders left in English. - Tone: restrained, archival and minimal on the surface; honest, ironic, occasionally snarky underneath. Plain wording, no corporate clichés, no hype ("revolutionize", "unleash the power", "best-in-class" are banned). - Service attitude: "включил и забыл" (set-and-forget), transparency about incidents, engineering-first, anti-censorship. - Tagline: **«Свобода. Практика. Простота.»** - Landing quote block: rotate randomly through the Russian quotes array from `.ref/landing.html`; render the second line as an italic ``. ## Routing (`src/App.tsx`) | Path | Element | Notes | | --- | --- | --- | | `/` | `Landing` | Existing marketing page | | `/login` | `LoginPage` | Credentials only. `?returnTo=` preserved for post-auth redirect | | `/plan` | `PlanPage` | Calculator + confirm → checkout | | `/account` | `AccountPage` | Protected: redirect to `/login` when unauthenticated | ## Backend API (mirrors `.info/openapi.json` — the only source of truth) Base: backend routes have **no `/api/` prefix**. In dev, `vite.config.ts` proxies `/auth`, `/plans`, `/orders`, `/users`, `/payments` to `http://127.0.0.1:8000`. In prod the reverse proxy serves same-origin. Never hardcode absolute URLs. ### Types (`src/types.ts`) ```ts type Provider = 'credentials' | 'telegram' | 'api'; interface UserInfo { username: string | null; telegram_id: string | null; referal_code: string; } interface LoginResponse { // POST /auth/login access_token: string; refresh_token: string; expires_at: number; // epoch seconds user: UserInfo; } interface Tokens { // POST /auth/refresh access_token: string; refresh_token: string; expires_at: number; } interface AddonData { // inside GET /plans/ id: string; name: string; price: number; free_threshold: number; } interface PricingPlans { // GET /plans/ device_price: number; addons: AddonData[]; } interface OrderDetails { // POST /orders/checkout body devices: number; addons: string[]; // addon ids duration_days: number; } interface CheckoutResponse { // POST /orders/checkout → 201 order_id: string; total_amount: number; bonus_paid: number; amount_to_pay: number; payment_link: string | null; } interface Device { // GET /users/subscription/hwid os: string | null; model: string | null; client: string | null; hwid: string; } ``` ### Endpoint table | Method | Path | Auth | Purpose | |---|---|---|---| | POST | `/auth/signup` | – | Register, body `{provider, username?, password?, telegram_id?, referal_code?}` | | POST | `/auth/login` | – | Login, body `{provider:'credentials', username, password}` | | POST | `/auth/refresh?refresh_token=&iss=` | – | New token pair; `iss` is the provider | | GET | `/plans/` | – | Pricing: `device_price` + addons | | POST | `/orders/checkout` | yes | Create order → `payment_link` | | GET | `/users/me` | yes | `UserInfo` | | GET | `/users/subscription` | yes | Subscription state (schema is `{}` — shape to be confirmed at implementation) | | GET | `/users/subscription/hwid` | yes | `Device[]` | | DELETE | `/users/subscription/hwid?hwid=` | yes | Unlink a device | | POST | `/payments/pally/result` | – | Payment callback — backend-internal, frontend never calls it | ### Auth flow - **Credentials only.** Sign up / sign in are username + password. Telegram auth is explicitly **out of scope** for now (linking ships later). The API *accepts* `provider: telegram`, but the frontend must only use `credentials`. - Tokens stored via a `useLocalStorage`-style hook under keys `malenia.access`, `malenia.refresh`, `malenia.expires_at` (epoch seconds). - The API client injects `Authorization: Bearer ` on every request. - On **401**: try `POST /auth/refresh?refresh_token=…&iss=credentials` once; on success retry the original request, on failure clear tokens and redirect to `/login`. Guard against refresh loops. - **Signup** may include `referal_code` — expose an optional field on the signup form and pass it through. ### API client conventions - Generic `request()` helper; throws `Error` with the backend `detail` for non-2xx. - 201 responses return parsed JSON; no 204s in this API. - Errors surface as **toast notifications** (inline errors only for form validation). ## Pages — implementation notes (from `.ref/`) ### Landing (`/`) — `.ref/landing.html` Keep the overall structure and design. **Apply the existing page's corrections:** - **Header** gains navigation links (`