Files
vontor-cz/.github/copilot-instructions.md
2026-01-06 16:42:29 +01:00

8.8 KiB

Copilot Instructions for Vontor CZ

Overview

This monorepo contains a Django backend and a Vite/React frontend, orchestrated via Docker Compose. The project is designed for a Czech e-marketplace, with custom payment integrations and real-time features.

Architecture

  • backend/: Django project (vontor_cz), custom account app, and third-party payment integrations (thirdparty/).
    • Uses Django REST Framework, Channels (ASGI), Celery, and S3/static/media via django-storages.
    • Custom user model: account.CustomUser.
    • API docs: DRF Spectacular (/api/schema/).
  • frontend/: Vite + React + TypeScript app.
    • Organized by src/api/, components/, features/, layouts/, pages/, routes/.
    • Uses React Router layouts and nested routes (see src/layouts/, src/routes/).
    • Uses Tailwind CSS for styling (configured via src/index.css with @import "tailwindcss";). Prefer utility classes over custom CSS.

Developer Workflows

  • Backend
    • Local dev: python manage.py runserver (or use Docker Compose)
    • Migrations: python manage.py makemigrations && python manage.py migrate
    • Celery: celery -A vontor_cz worker -l info
    • Channels: Daphne/ASGI (see Docker Compose command)
    • Env config: .env files in backend/ (see .gitignore for secrets)
  • Frontend
    • Install: npm install
    • Dev server: npm run dev
    • Build: npm run build
    • Preview: npm run preview
    • Static assets: src/assets/ (import in JS/CSS), public/ (referenced in HTML)

Conventions & Patterns

  • Backend
    • Use environment variables for secrets and config (see settings.py).
    • Static/media files: S3 in production, local in dev (see settings.py).
    • API versioning and docs: DRF Spectacular config in settings.py.
    • Custom permissions, filters, and serializers in each app.
    • Serializer Best Practices:
      • Prevent Duplicate Schemas: When the same ChoiceField or complex field appears in multiple serializers, define it once as a reusable field class and use it everywhere instead of repeated definitions.
      • Example: Create OrderStatusField(serializers.ChoiceField) with choices=Order.OrderStatus.choices and reuse it in all serializers that need order status.
      • This ensures consistent OpenAPI schema generation and reduces maintenance overhead.
  • Frontend
    • Use React Router layouts for shared UI (see src/layouts/, LAYOUTS.md).
    • API calls and JWT handling in src/api/.
    • Route definitions and guards in src/routes/ (ROUTES.md).
    • Use TypeScript strict mode (see tsconfig.*.json).
    • Linting: ESLint config in eslint.config.js.
    • Styling: Tailwind CSS is present. Prefer utility classes; keep minimal component-scoped CSS. Global/base styles live in src/index.css. Avoid inline styles and CSS-in-JS unless necessary.

Frontend API Client (required)

All frontend API calls must use the shared client at frontend/src/api/Client.ts.

  • Client.public: no cookies, no Authorization header (for public Django endpoints).
  • Client.auth: sends cookies and includes Bearer token; auto-refreshes on 401 (retries up to 2x).
  • Centralized error handling: subscribe via Client.onError to show toasts/snackbars.
  • Tokens are stored in cookies by Client.setTokens and cleared by Client.clearTokens.

Example usage (TypeScript)

import Client from "@/api/Client";

// Public request (no credentials)
async function listPublicItems() {
  const res = await Client.public.get("/api/public/items/");
  return res.data;
}

// Login (obtain tokens and persist to cookies)
async function login(username: string, password: string) {
  // Default SimpleJWT endpoint (adjust if your backend differs)
  const res = await Client.public.post("/api/token/", { username, password });
  const { access, refresh } = res.data;
  Client.setTokens(access, refresh);
}

// Authenticated requests (auto Bearer + refresh on 401)
async function fetchProfile() {
  const res = await Client.auth.get("/api/users/me/");
  return res.data;
}

function logout() {
  Client.clearTokens();
  window.location.assign("/login");
}

// Global error toasts
import { useEffect } from "react";
function useApiErrors(showToast: (msg: string) => void) {
  useEffect(() => {
    const unsubscribe = Client.onError((e) => {
      const { message, status } = e.detail;
      showToast(status ? `${status}: ${message}` : message);
    });
    return unsubscribe;
  }, [showToast]);
}

Vite env used by the client:

  • VITE_API_BASE_URL (default: http://localhost:8000)
  • VITE_API_REFRESH_URL (default: /api/token/refresh/)
  • VITE_LOGIN_PATH (default: /login)

Notes

  • Public client never sends cookies or Authorization.
  • Ensure Django CORS settings allow your frontend origin. See backend/vontor_cz/settings.py.
  • Use React Router layouts and guards as documented in frontend/src/routes/ROUTES.md and frontend/src/layouts/LAYOUTS.md.

Integration Points

  • Payments: thirdparty/ contains custom integrations for Stripe, GoPay, Trading212.
  • Real-time: Django Channels (ASGI, Redis) for websockets.
  • Task queue: Celery + Redis for async/background jobs.
  • API: REST endpoints, JWT auth, API key support.

OpenAPI Client Generation (Orval)

This project uses Orval to auto-generate TypeScript API clients from the Django OpenAPI schema.

Configuration

  • Orval config: frontend/src/orval.config.ts
  • Schema URL: /api/schema/ (DRF Spectacular endpoint)
  • Fetch script: frontend/scripts/fetch-openapi.js
  • Commands:
    • npm run api:update — fetches schema + generates client
    • Runs: node scripts/fetch-openapi.js && npx orval

Generated Output

  • Location: frontend/src/api/generated/
  • Files: TypeScript interfaces, Axios-based API hooks
  • Uses custom mutators: publicMutator and privateMutator

Custom Mutators

Two Axios clients handle public/private API requests:

Public Client (frontend/src/api/publicClient.ts):

import axios, { type AxiosRequestConfig } from "axios";

const backendUrl = import.meta.env.VITE_BACKEND_URL || "http://localhost:8000";

export const publicApi = axios.create({
  baseURL: backendUrl + "/api/",
  withCredentials: false, // no cookies for public endpoints
});

export const publicMutator = async <T>(config: AxiosRequestConfig): Promise<T> => {
  const response = await publicApi.request<T>(config);
  return response.data;
};

Private Client (frontend/src/api/privateClient.ts):

import axios, { type AxiosRequestConfig } from "axios";

const backendUrl = import.meta.env.VITE_BACKEND_URL || "http://localhost:8000";

export const privateApi = axios.create({
  baseURL: backendUrl + "/api/",
  withCredentials: true, // sends HttpOnly cookies (access/refresh tokens)
});

// Auto-refresh on 401
privateApi.interceptors.response.use(
  (res) => res,
  async (error) => {
    const original = error.config;
    if (error.response?.status === 401 && !original._retry) {
      original._retry = true;
      try {
        await privateApi.post("/auth/refresh/");
        return privateApi(original);
      } catch {
        // optional: logout
      }
    }
    return Promise.reject(error);
  }
);

export const privateMutator = async <T>(config: AxiosRequestConfig): Promise<T> => {
  const response = await privateApi.request<T>(config);
  return response.data;
};

Environment Variables (Vite)

  • IMPORTANT: Use import.meta.env.VITE_* instead of process.env in browser code
  • NEVER import dotenv/config in frontend files (causes "process is not defined" error)
  • Available vars:
    • VITE_BACKEND_URL (default: http://localhost:8000)
    • VITE_API_BASE_URL (if using Client.ts wrapper)
    • VITE_API_REFRESH_URL (default: /api/token/refresh/)
    • VITE_LOGIN_PATH (default: /login)

Usage Example

import { useGetOrders } from "@/api/generated/orders";

function OrdersList() {
  const { data, isLoading, error } = useGetOrders();
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <ul>
      {data?.map(order => <li key={order.id}>{order.status}</li>)}
    </ul>
  );
}

Helpers

  • Choices helper: frontend/src/api/get_choices.ts
    • Function: getChoices(requests, lang)
    • Returns: { "Model.field": [{ value, label }] }

References


When in doubt, check the referenced markdown files and settings.py for project-specific logic and patterns.