Deployment Note: The frontend is deployed via austin-site at https://www.austinwallace.ca/trading-cards. The backend (Lambda, DynamoDB, S3) remains deployed from this repo. See the Deployment section below.
A web application for creating custom sports trading cards. Upload a photo, crop it with drag-and-drop, add player details, and generate a high-quality PNG card ready for printing or sharing.
- Drag-and-drop image cropping - Frame the perfect shot with intuitive controls
- Live preview - See your card come together in real-time
- Client-side rendering - High-quality 825x1125 PNG generation in the browser
- Save drafts - Continue work before submitting
- Serverless architecture - Scales automatically with AWS Lambda
| Layer | Technology |
|---|---|
| Frontend | React 19, Vite, TailwindCSS v4, TanStack Router & Query |
| Backend | Hono (Lambda), DynamoDB, S3 |
| Infrastructure | SST v3, CloudFront Router |
| Build | pnpm workspaces, Turbo, TypeScript |
┌─────────────────────────────────────────────────────────────────┐
│ CloudFront Router │
├──────────────┬───────────────────────┬──────────────────────────┤
│ /* │ /api/* │ /r/* & /c/* │
│ Static │ Lambda API │ S3 Media │
│ (React) │ (Hono) │ (renders/config) │
└──────────────┴───────────────────────┴──────────────────────────┘
│
▼
┌─────────────┐
│ DynamoDB │
│ Cards │
└─────────────┘
Data Flow:
- User creates a card draft and receives a card ID
- Uploads photo via presigned S3 URL
- Crops image using normalized coordinates (stored as 0-1 floats)
- Client renders the final card as PNG using Canvas API
- Uploads rendered PNG and submits card
- Node.js 20+
- pnpm 10+
- AWS CLI configured with a profile (for deployment)
# Clone the repository
git clone https://github.com/your-org/trading-card-app.git
cd trading-card-app
# Install dependencies
pnpm installRun two terminals simultaneously:
Terminal 1 - SST (infrastructure + Lambda)
AWS_PROFILE=prod npx sst devTerminal 2 - Vite (frontend)
cd client && pnpm devOpen http://localhost:5173 to view the app.
# Type check
pnpm type-check
# Lint
pnpm lint
# Build all packages
pnpm buildThis app uses a hybrid deployment:
- Frontend: Deployed via
austin-siterepo to https://www.austinwallace.ca/trading-cards - Backend: Deployed from this repo (Lambda, DynamoDB, S3)
Deploy backend only (from this repo):
AWS_PROFILE=prod npx sst deploy --stage productionDeploy frontend (from austin-site repo):
cd ~/dev/austin-site
AWS_PROFILE=prod npx sst deploy --stage productionBoth deployments are required for a fully functioning app.
trading-card-app/
├── client/ # React frontend
│ ├── src/
│ │ ├── App.tsx # Main card creation UI
│ │ ├── renderCard.ts # Canvas-based card renderer
│ │ └── router.tsx # TanStack Router setup
│ └── index.html
├── server/ # Hono API
│ └── src/
│ ├── index.ts # API routes
│ └── lambda.ts # AWS Lambda handler
├── shared/ # Shared TypeScript types
│ └── src/
│ ├── types/ # CardDesign, CropRect, etc.
│ └── constants.ts # Card dimensions
├── sst.config.ts # SST infrastructure
└── package.json # Monorepo root
| Method | Endpoint | Description |
|---|---|---|
POST |
/uploads/presign |
Get presigned URL for S3 upload |
POST |
/cards |
Create a new card draft |
GET |
/cards/:id |
Retrieve card by ID |
PATCH |
/cards/:id |
Update card details |
POST |
/cards/:id/submit |
Submit card for finalization |
draft → submitted → rendered
- Max file size: 15 MB
- Allowed types: JPEG, PNG, WebP (render must be PNG)
- Card dimensions: 825 x 1125 pixels
- Bleed (full card): 825 x 1125 (render canvas)
- Trim (red guide): 750 x 1050 — used as the canonical preview area for submitters
- Safe (blue guide): 675 x 975 — shown in the admin template preview only
The submitter cropper shows the red trim guide, and the live preview renders only the trim area (trim-aspect output). The admin template preview overlays the blue safe guide inside the trim frame.
The client uses environment variables injected at build time:
| Variable | Description |
|---|---|
VITE_API_URL |
Lambda function URL (dev only) |
VITE_ROUTER_URL |
CloudFront Router URL (dev only) |
In production, relative URLs are used (same-origin via CloudFront).
Husky + lint-staged runs on every commit:
- Full TypeScript type checking
- ESLint auto-fix on staged files
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Built with SST and Claude Code