diff --git a/.forgejo/workflows/deploy-frontend-beta.yaml b/.forgejo/workflows/deploy-frontend-beta.yaml new file mode 100644 index 00000000..a596d4ba --- /dev/null +++ b/.forgejo/workflows/deploy-frontend-beta.yaml @@ -0,0 +1,39 @@ +name: Deploy Frontend Beta (S3) + +on: + push: + branches: + - beta + +jobs: + deploy-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: https://github.com/actions/checkout@v4 + + - name: Install Dependencies and Build + env: + VITE_API_BASE_URL: https://betaapicsa.tradhox.com/api + run: | + cd frontend + npm ci + npm run build + + - name: Deploy to AWS S3 & Invalidate CloudFront + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ap-south-2 + S3_BUCKET: ${{ secrets.AWS_S3_BUCKET_BETA || 'betacsa' }} + CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID_BETA || 'E8KO4T7M0MRBS' }} + run: | + apk add --no-cache aws-cli + if [ -z "$AWS_ACCESS_KEY_ID" ]; then + unset AWS_ACCESS_KEY_ID + unset AWS_SECRET_ACCESS_KEY + fi + aws s3 sync frontend/dist/ s3://${S3_BUCKET} --delete --region ap-south-2 + if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then + aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DISTRIBUTION_ID} --paths "/*" --region ap-south-2 + fi diff --git a/.forgejo/workflows/deploy-frontend-production.yaml b/.forgejo/workflows/deploy-frontend-production.yaml new file mode 100644 index 00000000..d640d771 --- /dev/null +++ b/.forgejo/workflows/deploy-frontend-production.yaml @@ -0,0 +1,39 @@ +name: Deploy Frontend Production (S3) + +on: + push: + branches: + - main + +jobs: + deploy-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: https://github.com/actions/checkout@v4 + + - name: Install Dependencies and Build + env: + VITE_API_BASE_URL: https://apicsa.tradhox.com/api + run: | + cd frontend + npm ci + npm run build + + - name: Deploy to AWS S3 & Invalidate CloudFront + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ap-south-2 + S3_BUCKET: ${{ secrets.AWS_S3_BUCKET_PRODUCTION || 'csawebsite' }} + CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID_PRODUCTION || 'E2G2C75ZGRMFVG' }} + run: | + apk add --no-cache aws-cli + if [ -z "$AWS_ACCESS_KEY_ID" ]; then + unset AWS_ACCESS_KEY_ID + unset AWS_SECRET_ACCESS_KEY + fi + aws s3 sync frontend/dist/ s3://${S3_BUCKET} --delete --region ap-south-2 + if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then + aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_DISTRIBUTION_ID} --paths "/*" --region ap-south-2 + fi diff --git a/.gitignore b/.gitignore index 2f75e907..c2680718 100644 --- a/.gitignore +++ b/.gitignore @@ -57,11 +57,9 @@ site/ # Node.js / Frontend frontend/node_modules/ -frontend/.svelte-kit/ -frontend/build/ +frontend/dist/ frontend/.env frontend/.env.local -frontend/package-lock.json # Logs logs/ diff --git a/frontend/.dockerignore b/frontend/.dockerignore deleted file mode 100644 index 42f89fe6..00000000 --- a/frontend/.dockerignore +++ /dev/null @@ -1,30 +0,0 @@ -# The compose frontend service builds with `context: ./frontend`, and Docker -# reads .dockerignore from the context root, so the repo-root .dockerignore has -# never applied to this build. Without this file the whole of node_modules and -# .svelte-kit ships to the daemon on every build. - -# Secrets. Load-bearing, not tidiness: the builder stage copies the source in, -# and vite.config.js calls loadEnv(), so a real .env in the context would be -# read at build time. PUBLIC_SENTRY_DSN in particular flips autoUploadSourceMaps -# on, and the build then fails looking for a SENTRY_AUTH_TOKEN that is not there. -# Every PUBLIC_ value this app reads comes from $env/dynamic/public, which is -# resolved at RUNTIME, so nothing here needs an .env to build correctly. -.env -.env.* - -# Install and build outputs. Regenerated inside the image; copying the host's -# would also drop a linux-x64 image on top of whatever the host built. -node_modules -.svelte-kit -build - -# Never needed by a build -.git -.gitignore -.vscode -.idea -.playwright-mcp -coverage -*.log -.DS_Store -Thumbs.db diff --git a/frontend/.env.example b/frontend/.env.example index 51ff1a5c..440c9e51 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,29 +1 @@ -# Google OAuth (client_id only - secret is on backend) -GOOGLE_CLIENT_ID="your-google-client-id-here" -GOOGLE_LOGIN_DOMAIN="http://localhost:5181" - -# Django API URL (PUBLIC_ prefix makes it available in both server and browser) -PUBLIC_DJANGO_API_URL=http://localhost:8000 - -# Sentry (only set in production, omit locally to disable Sentry) -# PUBLIC_SENTRY_DSN="https://your-dsn.ingest.sentry.io/your-project" - -# Environment -NODE_ENV=development - -# Relayed visitor addresses (optional locally; deploy-frontend.sh requires them -# in hosted production). Several pages reach the API from this server; with -# RELAY_SECRET set they also send the visitor's address, signed with it, so -# each visitor gets their own per-IP throttle bucket and recorded address. Must equal RELAY_SECRET on the Django -# API and on bottlecrm.io. Private: never prefix it with PUBLIC_. -# RELAY_SECRET= -# -# Behind nginx in production, adapter-node only knows the visitor if told which -# header nginx writes. Without these, getClientAddress() is nginx itself. -# ADDRESS_HEADER=X-Forwarded-For -# XFF_DEPTH=1 -# -# adapter-node binds 0.0.0.0 by default. Bind it to loopback in production so -# only nginx can reach it: reachable directly, a caller writes its own -# X-Forwarded-For and the relay signs it as the visitor. -# HOST=127.0.0.1 +VITE_API_BASE_URL=https://betaapicsa.tradhox.com/api diff --git a/frontend/.gitignore b/frontend/.gitignore index af1577b8..a547bf36 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,30 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + node_modules +dist +dist-ssr +*.local -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build - -# OS +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea .DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* - -generated/* -src/generated/* -.github/prompts/* - -# Sentry Config File -.env.sentry-build-plugin +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.npmrc b/frontend/.npmrc deleted file mode 100644 index b6f27f13..00000000 --- a/frontend/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 00000000..12550782 --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index 66354938..00000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,14 +0,0 @@ -# Package Managers -package-lock.json -pnpm-lock.yaml -yarn.lock -bun.lock -bun.lockb - -# Local session artifacts -.remember/ - -# Generated API schema. drf-spectacular writes YAML, and the file name says -# .json, so prettier picks the JSON parser and dies on line 1 instead of -# reporting a formatting diff. That aborts `pnpm run lint` before eslint runs. -openapi.json diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index ef3063bc..00000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "useTabs": false, - "tabWidth": 2, - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ] -} diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index bb6717f6..00000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,87 +0,0 @@ -# syntax=docker/dockerfile:1 -# -# One file, two outputs: the `dev` stage docker-compose runs, and the default -# `runtime` stage a release publishes. They were going to be two files, which is -# how the pnpm pin in here already drifted from the one in .github/workflows -# (11.17.0 against 10). A single base stage means there is one version to bump. -# -# RUNTIME CONFIGURATION -# -# Every value this app reads from the environment is declared in src/env.js and -# read per process at startup, not at build time. So one image serves any -# deployment and nothing below bakes a hostname in: -# -# PUBLIC_DJANGO_API_URL required. The Django API origin, no trailing /api. -# PUBLIC_SENTRY_DSN optional. Unset disables Sentry (`enabled: !!dsn`). -# PORT defaults to 3000. HOST already defaults to 0.0.0.0. -# PROTOCOL_HEADER only for plain http. See below. -# BODY_SIZE_LIMIT set to 6M below. adapter-node 6 defaults to 512K, which -# would refuse a spreadsheet import (the API caps files -# at 5 MB) with a plain-text 413. -# -# The origin check is the one that fails quietly. SvelteKit compares the Origin -# header of every form POST with the origin the server thinks it serves, which -# adapter-node 6 builds from the request's Host header and the scheme `https`. -# (The ORIGIN variable that used to override this was removed in SvelteKit 3, -# so setting it does nothing.) Behind a TLS proxy that forwards Host, such as -# nginx with `proxy_set_header Host $host`, that is already right. If users -# reach the app over plain http, set PROTOCOL_HEADER=x-forwarded-proto and have -# the proxy send that header. Otherwise every form POST is answered `403 -# Cross-site POST form submissions are forbidden` while GETs still work, so the -# app looks healthy while all its form actions reject. - -FROM node:24-slim AS base -# Pinned so image builds are reproducible. -RUN corepack enable && corepack prepare pnpm@11.17.0 --activate -WORKDIR /app - -# Manifests only, so this layer caches on the lockfile rather than on source. -# pnpm-workspace.yaml carries the install settings (onlyBuiltDependencies, -# allowBuilds) that pnpm 10+ no longer reads from package.json. Without it -# @sentry/cli's postinstall is silently skipped and its binary never lands. -FROM base AS deps -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ -RUN pnpm install --frozen-lockfile - - -# The compose target. Source and node_modules arrive as mounts, so this stage -# deliberately copies no source of its own. -FROM deps AS dev -EXPOSE 5181 -CMD ["pnpm", "dev", "--host", "0.0.0.0"] - - -FROM deps AS builder -COPY . . -RUN pnpm build - - -# Runtime dependencies only. `build/` is NOT self-contained: SvelteKit keeps -# instrumentation.server.js external so OpenTelemetry can hook the process -# before anything else loads, and it imports @sentry/sveltekit by name. Without -# this stage the server dies at boot on ERR_MODULE_NOT_FOUND. -# -# --ignore-scripts because the `prepare` script shells out to svelte-kit, which -# is a devDependency and therefore absent here. package.json already tolerates -# that with `|| echo ''`; skipping it outright keeps the failure out of the log. -FROM base AS prod-deps -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ -RUN pnpm install --prod --frozen-lockfile --ignore-scripts - - -FROM node:24-slim AS runtime -WORKDIR /app -ENV NODE_ENV=production -ENV BODY_SIZE_LIMIT=6M - -COPY --from=prod-deps --chown=node:node /app/node_modules ./node_modules -COPY --from=builder --chown=node:node /app/build ./build -# The server reads its own name and version from here on startup. -COPY --chown=node:node package.json ./ - -# Present in the node images already. The server binds 3000, so it needs no -# privileged port and no reason to be root. -USER node - -EXPOSE 3000 -CMD ["node", "build/index.js"] diff --git a/frontend/README.md b/frontend/README.md index 38b3407a..d937833b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,41 +1,16 @@ -# BottleCRM web app +# React + Vite -The SvelteKit (Svelte 5) web client for BottleCRM. It has no database of its own: every page reads -and writes through the Django REST API in [`../backend`](../backend), the same API the Flutter app -in [`../mobile`](../mobile) uses. For what BottleCRM is, see the [project README](../README.md). +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. -## Run it locally +Currently, two official plugins are available: -Start the backend first (see [Development setup](../docs/contributing/development-setup.md)), then: +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) -```bash -cd frontend -cp .env.example .env # PUBLIC_DJANGO_API_URL points at the backend, http://localhost:8000 by default -pnpm install -pnpm run dev # http://localhost:5181 -``` +## React Compiler -The browser calls the API cross-origin, so the backend's `CORS_ALLOWED_ORIGINS` must include -`http://localhost:5181`. A missing entry shows up as a CORS error, not a 404. +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). -## Checks +## Expanding the Oxlint configuration -```bash -pnpm run check # svelte-check type checking -pnpm run lint # prettier + eslint -pnpm run test # vitest -pnpm run build # production build (adapter-node) -``` - -## Access control - -Org membership and role (`USER` or `ADMIN`) come from the server, and the backend is the only place -they are enforced; anything the UI hides is a convenience, not a control. Platform super admin is -the `is_superuser` flag on the user record, granted explicitly and never derived from an email -address. - -## More - -- [Manual setup](../docs/getting-started/manual-setup.md) and [Docker quick start](../docs/getting-started/docker-quick-start.md) -- [Contributing](../CONTRIBUTING.md) -- License: [MIT](../LICENSE) +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project. diff --git a/frontend/components.json b/frontend/components.json deleted file mode 100644 index f027956f..00000000 --- a/frontend/components.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://next.shadcn-svelte.com/schema.json", - "style": "default", - "tailwind": { - "config": "", - "css": "src/app.css", - "baseColor": "neutral" - }, - "aliases": { - "components": "$lib/components", - "utils": "$lib/utils", - "ui": "$lib/components/ui", - "hooks": "$lib/hooks" - }, - "typescript": false -} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 6b83ff0d..00000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,31 +0,0 @@ -import prettier from 'eslint-config-prettier'; -import js from '@eslint/js'; -import { includeIgnoreFile } from '@eslint/compat'; -import svelte from 'eslint-plugin-svelte'; -import globals from 'globals'; -import { fileURLToPath } from 'node:url'; - -const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); - -export default [ - includeIgnoreFile(gitignorePath), - js.configs.recommended, - ...svelte.configs.recommended, - prettier, - ...svelte.configs.prettier, - { - languageOptions: { - globals: { ...globals.browser, ...globals.node } - } - }, - { - rules: { - 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], - 'no-useless-assignment': 'warn', - 'svelte/require-each-key': 'warn', - 'svelte/no-navigation-without-resolve': 'warn', - 'svelte/prefer-svelte-reactivity': 'warn', - 'svelte/prefer-writable-derived': 'warn' - } - } -]; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..11c834a0 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + +
+ + + +No KYC documents submitted yet.
+ )} + + {/* Lightbox / Preview Modal Overlay */} + {previewDoc && ( +Preview is not supported directly for this file type ({previewDoc.filename}).
+ +Manage sellers, customers, logistics & payments
+- {description} -
- - -- Any column names work. You'll check how they match before anything is imported. -
- {#if error} -- Starting from scratch? - -
- {:else if step === 'match' && preview && file} -- We couldn't match any columns. Make sure the first row of your sheet holds the column names. -
- {/if} - -| Column in your file | -Sample values | -Import as | -
|---|---|---|
| {name} | -- {#if c.samples.length}{c.samples.join(' · ')}{:else}No values{/if} - | -- - | -
Updating the preview
- {/if} - {#each droppedLines(preview.dropped, preview.fields) as line (line)} -{line}
- {/each} - {#if preview.skipped.length} -- {rows(preview.skipped.length - SKIPPED_SHOWN)} more are in the download. -
- {/if} -- Some rows were skipped for a missing first or last name. A lead needs only one of the two, - so this file could go in as leads instead. - {#if onUseLeads} - - {/if} -
- {/if} -{summaryLine(nouns, preview)}
-
-
-
{line}
- {/each} - {#if result.skipped?.length} - {@const skipped = result.skipped} -{rows(skipped.length)} skipped.
-CSV format
-
- Required headers: name,
- status,
- priority. Optional:
- description, case_type, account_name, contact_emails, assigned_emails, team_names, tags
- (semicolon-separated), closed_on (YYYY-MM-DD).
-
| # | -Name | -Status | -Priority | -Account | -
|---|---|---|---|---|
| {row.row} | -{row.name} | -{row.status} | -{row.priority} | -- {row.account_id ? '✓' : '—'} - | -
- {preview.errors.length} error{preview.errors.length === 1 ? '' : 's'}, fix the CSV - before importing -
- -| Row | -Field | -Problem | -
|---|---|---|
| {err.row} | -{err.field} | -{err.message} | -
- Server rejected {commitErrors.length} row{commitErrors.length === 1 ? '' : 's'} during - import. The file may have changed since preview -
- -| Row | -Field | -Problem | -
|---|---|---|
| {err.row} | -{err.field} | -{err.message} | -
- Imported {commitResult.created} ticket{commitResult.created === 1 ? '' : 's'} -
-The tickets list has been refreshed.
-