diff --git a/.github/readme-assets/banner.png b/.github/readme-assets/banner.png new file mode 100644 index 0000000..6e8de2d Binary files /dev/null and b/.github/readme-assets/banner.png differ diff --git a/README.md b/README.md index 25bcc86..bf55048 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,27 @@ -

- CardGoose 🃏🪿 -

+

+ CardGoose — board games for Print & Play and Tabletop Simulator +

-

A tool to make board games for Print & Play and Tabletop Simulator.

+

+ A tool to make board games for Print & Play and Tabletop Simulator. +

+ +--- + +## About + +Designing board games is hard. It requires **rapid iteration** and the ability to make changes **on-the-fly**. CardGoose makes it easier with data-driven components and custom templates to build your ideas in minutes. + +## Key Features + +- Import data from Google Sheets & refresh for live updates +- Component layout editor for card design + - Deck preview to evaluate changes +- Custom images +- PDF export +- TTS export (coming soon) + +# Getting started ## Prerequisites @@ -19,51 +38,40 @@ Pick one path. They differ by **where Postgres and object/queue services run**, not by repo layout. + | Mode | Frontend | API & worker | Database | Buckets & queue | | --------------- | -------- | ------------ | -------- | --------------------- | | **Fully local** | Your Mac | Your Mac | Docker | LocalStack (emulated) | | **Hybrid** | Your Mac | Your Mac | AWS RDS | AWS S3 & SQS | -| **Production** | ECS\* | ECS | AWS RDS | AWS S3 & SQS | +| **Production** | ECS | ECS | AWS RDS | AWS S3 & SQS | + -\*The API container can serve the built SPA in production (`NODE_ENV=production`). Until you add a stable URL (ALB, CloudFront, etc.), you may use the task public IP for smoke tests. +The API container can serve the built SPA in production (`NODE_ENV=production`). Until you add a stable URL (ALB, CloudFront, etc.), you may use the task public IP for smoke tests. ### Fully local (Docker Postgres + LocalStack) Use this when you want **no AWS calls**: everything emulated or on your machine. 1. **Start backing services** (Postgres on host port **5433**, LocalStack on **4566**): - - ```bash + ```bash pnpm docker:up - ``` - + ``` Optional: `docker compose build` if you use the `api` / `worker` compose services. - -2. **Configure** [`.env.local.example`](.env.local.example) → **`.env.local`** at the repo root using the **“full local stack”** block: `DATABASE_URL` pointing at `localhost:5433`, `AWS_ENDPOINT_URL=http://localhost:4566`, and bucket/queue names **`cardgoose-*`** (they must match [`docker-compose.yml`](docker-compose.yml) and [`docker/localstack-ready.d/init-aws.sh`](docker/localstack-ready.d/init-aws.sh)). - +2. **Configure** `[.env.local.example](.env.local.example)` → `**.env.local`** at the repo root using the **“full local stack”** block: `DATABASE_URL` pointing at `localhost:5433`, `AWS_ENDPOINT_URL=http://localhost:4566`, and bucket/queue names `**cardgoose-*`** (they must match `[docker-compose.yml](docker-compose.yml)` and `[docker/localstack-ready.d/init-aws.sh](docker/localstack-ready.d/init-aws.sh)`). 3. **Migrations:** - - ```bash + ```bash pnpm migrate:deploy - ``` - + ``` 4. **Run three processes** (three terminals from the repo root): - - ```bash + ```bash pnpm dev:api pnpm dev:frontend - ``` - - ```bash - cd worker - PYTHONPATH=src python3 -m baker.main - ``` - + ``` 5. Open the URL Vite prints (often `http://localhost:5173`). -Do **not** set `VITE_API_URL` in `frontend/.env.local` if the Vite dev server should proxy `/api` and `/health` to `http://localhost:3001` (see [`frontend/vite.config.ts`](frontend/vite.config.ts)). +Do **not** set `VITE_API_URL` in `frontend/.env.local` if the Vite dev server should proxy `/api` and `/health` to `http://localhost:3001` (see `[frontend/vite.config.ts](frontend/vite.config.ts)`). -**PDF exports:** set **`RENDER_URL`** in `.env.local` to the exact origin Vite prints (including port). If the worker runs in Docker and Vite on the host, use something like `http://host.docker.internal:5173`. Restart the worker after changes. +**PDF exports:** set `**RENDER_URL`** in `.env.local` to the exact origin Vite prints (including port). If the worker runs in Docker and Vite on the host, use something like `http://host.docker.internal:5173`. Restart the worker after changes. --- @@ -71,16 +79,16 @@ Do **not** set `VITE_API_URL` in `frontend/.env.local` if the Vite dev server sh Use this for day-to-day work: **Vite, the API, and the worker on your laptop** with **real RDS, S3, and SQS** in `us-east-1`. You get hot reload without deploying containers. -**1. Terraform (occasional)** — in [`infra/envs/prod`](infra/envs/prod): +**1. Terraform (occasional)** — in `[infra/envs/prod](infra/envs/prod)`: -- Allow your laptop to reach RDS: set `rds_dev_access_cidrs` without committing real IPs — e.g. `export TF_VAR_rds_dev_access_cidrs='["YOUR.PUBLIC.IP/32"]'` before `terraform apply`, or copy [`infra/envs/prod/rds.auto.tfvars.example`](infra/envs/prod/rds.auto.tfvars.example) to **`rds.auto.tfvars`** (gitignored). Update when your IP changes. +- Allow your laptop to reach RDS: set `rds_dev_access_cidrs` without committing real IPs — e.g. `export TF_VAR_rds_dev_access_cidrs='["YOUR.PUBLIC.IP/32"]'` before `terraform apply`, or copy `[infra/envs/prod/rds.auto.tfvars.example](infra/envs/prod/rds.auto.tfvars.example)` to `**rds.auto.tfvars`** (gitignored). Update when your IP changes. - Set `ecs_desired_count = 0` for the **worker** service if you run the Python worker locally (avoids two consumers on the same SQS queue and idle Fargate cost). - Run `terraform apply`. -**2. Root `.env.local`** — copy [`.env.local.example`](.env.local.example) and fill **real** values: +**2. Root `.env.local`** — copy `[.env.local.example](.env.local.example)` and fill **real** values: -- **`DATABASE_URL`** — RDS host, user `forge`, password from `terraform output -raw rds_master_password`, database name from your RDS instance (by default Terraform uses the `project_name` value, e.g. `cardboardforge`). -- **`S3_BUCKET_ASSETS`**, **`S3_BUCKET_EXPORTS`**, **`SQS_QUEUE_URL`** — from `terraform output` (`assets_bucket_name`, `exports_bucket_name`, `pdf_queue_url`). +- `**DATABASE_URL`** — RDS host, user `forge`, password from `terraform output -raw rds_master_password`, database name from your RDS instance (by default Terraform uses the `project_name` value, e.g. `cardboardforge`). +- `**S3_BUCKET_ASSETS**`, `**S3_BUCKET_EXPORTS**`, `**SQS_QUEUE_URL**` — from `terraform output` (`assets_bucket_name`, `exports_bucket_name`, `pdf_queue_url`). Do **not** set `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_ENDPOINT_URL` for real AWS: use the default credential chain (`~/.aws/credentials` or SSO). @@ -96,7 +104,7 @@ pnpm migrate:deploy **Queue contention:** if the **ECS worker** is scaled up, it shares the same SQS URL as your laptop. For predictable local PDF debugging, keep ECS worker desired count at **0** while testing locally. -**Frontend-only against a cloud API:** set `VITE_API_URL=http://:3001` in `frontend/.env.local` and run `pnpm dev:frontend` only. Find the task IP in the ECS console (see [`frontend/.env.local.example`](frontend/.env.local.example)). This is still “hybrid” from the browser’s perspective (local UI, remote API). +**Frontend-only against a cloud API:** set `VITE_API_URL=http://:3001` in `frontend/.env.local` and run `pnpm dev:frontend` only. Find the task IP in the ECS console (see `[frontend/.env.local.example](frontend/.env.local.example)`). This is still “hybrid” from the browser’s perspective (local UI, remote API). **Troubleshooting RDS:** if Prisma cannot connect, confirm your IP is in `rds_dev_access_cidrs`, re-apply Terraform, and that `DATABASE_URL` matches `terraform output -raw rds_endpoint`. @@ -104,34 +112,32 @@ pnpm migrate:deploy ### Production (deployed on AWS) -**Production** means the **API and worker run on ECS Fargate**, with **RDS, S3, and SQS** provisioned by Terraform. CI can build and push images and force new deployments (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)). - -1. **Bootstrap and apply** — follow [infra/BOOTSTRAP.md](infra/BOOTSTRAP.md) for remote state, then apply [`infra/envs/prod`](infra/envs/prod). Resource names, buckets, queues, and the RDS database name follow your Terraform variables (defaults in this repo still use the historical `cardboardforge` prefix for AWS resources). +**Production** means the **API and worker run on ECS Fargate**, with **RDS, S3, and SQS** provisioned by Terraform. CI can build and push images and force new deployments (see `[.github/workflows/ci.yml](.github/workflows/ci.yml)`). -2. **Runtime config** — ECS tasks receive env vars from Terraform (not from `.env.local`). For **one-off Prisma operations** against production RDS (migrations, introspection), copy [`.env.production.example`](.env.production.example) to **`.env.production`** (never commit) with values that match your live stack: - - ```bash +1. **Bootstrap and apply** — follow [infra/BOOTSTRAP.md](infra/BOOTSTRAP.md) for remote state, then apply `[infra/envs/prod](infra/envs/prod)`. Resource names, buckets, queues, and the RDS database name follow your Terraform variables (defaults in this repo still use the historical `cardboardforge` prefix for AWS resources). +2. **Runtime config** — ECS tasks receive env vars from Terraform (not from `.env.local`). For **one-off Prisma operations** against production RDS (migrations, introspection), copy `[.env.production.example](.env.production.example)` to `**.env.production`** (never commit) with values that match your live stack: + ```bash pnpm migrate:deploy:prod - ``` - + ``` 3. **Deploys** — pushes to `main` run lint, tests, Terraform validate, Docker builds, and (with secrets configured) ECR push + ECS `update-service --force-new-deployment`. Adjust branch rules in the workflow if your default branch differs. - 4. **Smoke checks** — hit `/health` on a running API task; confirm `service` identifies as `cardgoose-api`. Scale worker and API services in ECS per load and cost. --- ## Environment files + | File | Purpose | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| [`.env.local.example`](.env.local.example) | Template for **local** dev — copy to **`.env.local`** at the repo root. | -| [`.env.production.example`](.env.production.example) | Template for **production DB / ops** — copy to **`.env.production`** for Prisma and tooling (never commit). | +| `[.env.local.example](.env.local.example)` | Template for **local** dev — copy to `**.env.local`** at the repo root. | +| `[.env.production.example](.env.production.example)` | Template for **production DB / ops** — copy to `**.env.production`** for Prisma and tooling (never commit). | + -The API loads **`.env.local`** when `NODE_ENV` is not `production`. Prisma CLI uses `dotenv -e ../.env.local` or `../.env.production` via the `prisma:deploy` scripts. The Baker worker loads the same **`.env.local`** when run locally (`python-dotenv`; existing environment variables win). +The API loads `**.env.local**` when `NODE_ENV` is not `production`. Prisma CLI uses `dotenv -e ../.env.local` or `../.env.production` via the `prisma:deploy` scripts. The Baker worker loads the same `**.env.local**` when run locally (`python-dotenv`; existing environment variables win). -If you still have a single **`.env`** from an older setup, merge into `.env.local` and remove `.env`. +If you still have a single `**.env**` from an older setup, merge into `.env.local` and remove `.env`. -**Docker Compose API:** `docker compose up api` runs `prisma migrate deploy` before `node` (see [`docker-compose.yml`](docker-compose.yml)). +**Docker Compose API:** `docker compose up api` runs `prisma migrate deploy` before `node` (see `[docker-compose.yml](docker-compose.yml)`). --- @@ -144,3 +150,4 @@ pnpm test:all cd infra && terraform fmt -check -recursive cd infra/envs/prod && terraform init -input=false && terraform validate ``` + diff --git a/frontend/index.html b/frontend/index.html index 0ac0493..2f29742 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,8 +8,8 @@ content="Design and print tabletop card games with CardGoose." /> CardGoose - - + + - + @@ -25,7 +25,7 @@ name="twitter:description" content="Design and print tabletop card games with CardGoose." /> - +
diff --git a/frontend/public/cardgoose-favicon.svg b/frontend/public/cardgoose-favicon.svg new file mode 100644 index 0000000..912fbaa --- /dev/null +++ b/frontend/public/cardgoose-favicon.svg @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/App.css b/frontend/src/App.css index 3c7267d..cbe4e2b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1655,8 +1655,17 @@ button.zone-row-body:hover { font-weight: 500; } -.studio-shell-brand-text { - white-space: nowrap; +.studio-shell-brand--mark-only { + gap: 0; + line-height: 0; + padding: 4px 0; +} + +.studio-shell-header--dash .studio-shell-row { + min-height: 56px; + padding-left: 14px; + padding-right: 20px; + align-items: center; } .studio-shell-logo { @@ -1667,6 +1676,15 @@ button.zone-row-body:hover { color: var(--accent); } +.brand-logo-mark svg { + display: block; +} + +/* Defeat black fills from exports (invisible on dark chrome); parent sets color */ +.brand-logo-mark path { + fill: currentColor; +} + .studio-shell-left { display: flex; align-items: center; diff --git a/frontend/src/assets/cardgoose-mark.svg b/frontend/src/assets/cardgoose-mark.svg new file mode 100644 index 0000000..e0b12af --- /dev/null +++ b/frontend/src/assets/cardgoose-mark.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/cardgoose-square.svg b/frontend/src/assets/cardgoose-square.svg new file mode 100644 index 0000000..5c9fc63 --- /dev/null +++ b/frontend/src/assets/cardgoose-square.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/BrandLogo.tsx b/frontend/src/components/BrandLogo.tsx index 888b90a..c85ef7e 100644 --- a/frontend/src/components/BrandLogo.tsx +++ b/frontend/src/components/BrandLogo.tsx @@ -1,5 +1,4 @@ -/** CardGoose mark — served from `public/cardgoose-logo.png`. */ -export const BRAND_LOGO_SRC = '/cardgoose-logo.png'; +import cardgooseMarkSvg from '../assets/cardgoose-mark.svg?raw'; type BrandLogoProps = { /** CSS height in pixels; width follows aspect ratio. */ @@ -9,14 +8,73 @@ type BrandLogoProps = { alt?: string; }; +/** Normalize any exported SVG so it always renders: strip XML PI, replace root <svg>, theme fill. */ +function buildLogoHtml(raw: string, heightPx: number): string { + let s = raw.replace(/^\uFEFF/, '').replace(/<\?xml[^?]*\?>\s*/i, ''); + + const openMatch = s.match(/^]*)>/i); + if (!openMatch) { + return s; + } + + const attrStr = openMatch[1] ?? ''; + let viewBoxStr: string | null = null; + const vb = attrStr.match(/\bviewBox\s*=\s*["']([^"']*)["']/i); + if (vb?.[1]) { + viewBoxStr = vb[1].trim(); + } + + let viewW = 1704; + let viewH = 905; + if (viewBoxStr) { + const p = viewBoxStr.split(/\s+/).map(Number); + if (p.length === 4 && p.every((n) => !Number.isNaN(n))) { + viewW = p[2]; + viewH = p[3]; + } + } else { + const w = attrStr.match(/\bwidth\s*=\s*["']?(\d+(?:\.\d+)?)/i); + const h = attrStr.match(/\bheight\s*=\s*["']?(\d+(?:\.\d+)?)/i); + if (w && h) { + viewW = parseFloat(w[1]); + viewH = parseFloat(h[1]); + viewBoxStr = `0 0 ${viewW} ${viewH}`; + } + } + + if (!viewBoxStr) { + viewBoxStr = `0 0 ${viewW} ${viewH}`; + } + + const widthPx = (heightPx * viewW) / viewH; + + s = s.replace( + /^]*>/i, + `` + ); + + s = s.replace(/\bfill\s*=\s*["']#000000["']/gi, 'fill="currentColor"'); + s = s.replace(/\bfill\s*=\s*["']black["']/gi, 'fill="currentColor"'); + + return s; +} + export function BrandLogo({ heightPx, className, alt = '' }: BrandLogoProps) { + const svgHtml = buildLogoHtml(cardgooseMarkSvg, heightPx); + return ( - {alt} ); } diff --git a/frontend/src/components/StudioAppBar.tsx b/frontend/src/components/StudioAppBar.tsx index e1d4a5e..5469091 100644 --- a/frontend/src/components/StudioAppBar.tsx +++ b/frontend/src/components/StudioAppBar.tsx @@ -144,13 +144,13 @@ function DashboardBar() {
layoutEditor?.onNavigateHomeClick(e)} + aria-label="CardGoose home" > - + - CardGoose
@@ -188,7 +188,7 @@ function ProjectTabsBar() { aria-label="Home" > - + {projectName} @@ -224,7 +224,7 @@ function ProjectLoadingBar() {
- + @@ -306,7 +306,7 @@ function EditorBar() { aria-label="Home" > - +