diff --git a/.dockerignore b/.dockerignore index 1bc6f17..429340c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -18,6 +18,8 @@ app .git .gitignore .github +.agents +.codex # Docker Dockerfile @@ -27,7 +29,6 @@ docker-compose*.yml # Env / secrets .env .env.* -!.env.production build.sh # Editor / OS diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5d52801 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +APP_PORT=8080 +VITE_API_URL=https://api.persain-game.com/api/v1 +PERSIAN_GAME_IMAGE=persian-game:local diff --git a/.gitignore b/.gitignore index a547bf3..b50664c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ node_modules dist dist-ssr *.local +.env +.env.* +!.env.example # Editor directories and files .vscode/* diff --git a/Dockerfile b/Dockerfile index a0a4cf9..50d5dc5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,31 @@ -# syntax=docker/dockerfile:1 +# syntax=docker/dockerfile:1.7 -# ---------- Build stage ---------- -FROM node:20-alpine AS build +FROM node:22-alpine AS build WORKDIR /app -# Install dependencies (cached unless package files change) COPY package.json package-lock.json ./ -RUN npm ci +RUN --mount=type=cache,target=/root/.npm \ + npm ci --no-audit --no-fund -# Build the app COPY . . -# API base URL is baked in at build time (Vite inlines VITE_* vars). -# Override with: docker build --build-arg VITE_API_URL=https://api.example.com/api/v1 . -ARG VITE_API_URL -ENV VITE_API_URL=$VITE_API_URL +# Vite embeds VITE_* variables in the static bundle during this stage. +ARG VITE_API_URL=https://api.persain-game.com/api/v1 +ENV VITE_API_URL=${VITE_API_URL} RUN npm run build -# ---------- Runtime stage ---------- -FROM nginx:alpine AS runtime +FROM nginxinc/nginx-unprivileged:1.28-alpine AS runtime -# SPA-aware nginx config -COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --chown=nginx:nginx nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build --chown=nginx:nginx /app/dist /usr/share/nginx/html -# Static build output -COPY --from=build /app/dist /usr/share/nginx/html +USER nginx -EXPOSE 80 +EXPOSE 8080 -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -q -O /dev/null http://127.0.0.1/ || exit 1 +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1 CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 0d6babe..7d612a6 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,60 @@ -# React + TypeScript + Vite +# Persian Game frontend -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +رابط کاربری React + TypeScript این پروژه با Vite ساخته و در محیط production توسط Nginx سرو می‌شود. -Currently, two official plugins are available: +## اجرای Docker -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +پیش‌نیازها: Docker Engine و Docker Compose v2. -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} +```bash +cp .env.example .env +docker compose up --detach --build ``` -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +سپس برنامه روی `http://localhost:8080` در دسترس است. برای مشاهده وضعیت و لاگ‌ها: + +```bash +docker compose ps +docker compose logs --follow web +``` + +برای توقف برنامه: + +```bash +docker compose down +``` + +اسکریپت زیر نیز Compose را اعتبارسنجی، image را build و سرویس را اجرا می‌کند: + +```bash +./build.sh +``` + +## تنظیمات + +مقادیر قابل تنظیم در `.env`: + +- `APP_PORT`: پورت میزبان؛ مقدار پیش‌فرض `8080` است. +- `VITE_API_URL`: آدرس کامل API؛ مقدار پیش‌فرض `https://api.persain-game.com/api/v1` است. +- `PERSIAN_GAME_IMAGE`: نام و tag ایمیج ساخته‌شده. + +متغیر `VITE_API_URL` هنگام build داخل فایل‌های استاتیک قرار می‌گیرد. پس از تغییر آن باید image را دوباره build کنید: + +```bash +docker compose build --no-cache web +docker compose up --detach web +``` + +## توسعه محلی بدون Docker + +```bash +npm ci +npm run dev +``` + +بررسی production build و lint: + +```bash +npm run build +npm run lint +``` diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 084873b..b0560a3 --- a/build.sh +++ b/build.sh @@ -1,13 +1,10 @@ -#!/bin/bash +#!/usr/bin/env sh -REPO_DIR="/var/www/html/persian-game/" +set -eu -TOKEN="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" +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +cd "$SCRIPT_DIR" -cd $REPO_DIR - -echo $TOKEN | git pull https://$TOKEN@github.com/joshfe/persian-game.git - -npm i -npm run build -ls -al \ No newline at end of file +docker compose config --quiet +docker compose up --detach --build --remove-orphans +docker compose ps diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..e47c6c4 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,19 @@ +services: + web: + image: ${PERSIAN_GAME_IMAGE:-persian-game:local} + build: + context: . + target: runtime + args: + VITE_API_URL: ${VITE_API_URL:-https://api.persain-game.com/api/v1} + ports: + - "${APP_PORT:-8080}:8080" + restart: unless-stopped + read_only: true + tmpfs: + - /tmp:size=16m,mode=1777 + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + stop_grace_period: 10s diff --git a/nginx.conf b/nginx.conf index 29b7833..869f3a2 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,10 +1,16 @@ server { - listen 80; + listen 8080; server_name _; + server_tokens off; root /usr/share/nginx/html; index index.html; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + # ---- gzip ---- gzip on; gzip_vary on; @@ -26,33 +32,32 @@ server { font/woff2 application/vnd.ms-fontobject; - # ---- Hashed static assets: cache hard ---- - # ^~ so this wins over the regex block below for Vite's fingerprinted files + location = /healthz { + access_log off; + default_type text/plain; + return 200 "ok\n"; + } + + # Vite fingerprints files under /assets, so they can be cached for a year. location ^~ /assets/ { expires 1y; - add_header Cache-Control "public, immutable"; try_files $uri =404; } - # ---- Other static files by extension ---- location ~* \.(?:js|css|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|mp4|webm)$ { expires 30d; - add_header Cache-Control "public"; try_files $uri =404; } - # ---- SPA fallback: unknown routes -> index.html (React Router) ---- location / { try_files $uri $uri/ /index.html; } - # index.html must never be cached, so new deploys are picked up + # Always revalidate the entrypoint so deployments are picked up immediately. location = /index.html { - add_header Cache-Control "no-cache, no-store, must-revalidate"; expires -1; } - # Don't log noise / don't cache the favicon aggressively is fine as-is location = /favicon.ico { log_not_found off; access_log off; diff --git a/src/hooks/useAxios.ts b/src/hooks/useAxios.ts index 64a04fa..6916656 100644 --- a/src/hooks/useAxios.ts +++ b/src/hooks/useAxios.ts @@ -9,7 +9,7 @@ const useAxios = () => { const instance = axios.create({ baseURL: - import.meta.env.VITE_API_URL ?? "https://api.persain-game.com/api/v1", + import.meta.env.VITE_API_URL || "https://api.persain-game.com/api/v1", maxBodyLength: Infinity, ...(sessionStorage.getItem("shop_id") && { headers: {