Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ wasm/target/
# Wandb
**/wandb/

# Screenshots
/screenshots/

# Remotion
remotion/node_modules/
remotion/out/
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,53 @@ The project is configured for automatic WASM builds in CI/CD:
- **Vercel**: Automatically builds WASM during deployment (see `vercel.json` and `vercel-build.sh`)

For detailed deployment instructions, see [README-DEPLOYMENT.md](README-DEPLOYMENT.md).

## Native Apps (iOS/Android)

The project uses Capacitor for native mobile builds.

### Building for iOS

```bash
# Build static export
bun run build:native

# Sync with native project
bun run cap:sync

# Open in Xcode
bun run cap:ios
```

From Xcode: Product → Archive → Distribute App

### Building for Android

```bash
bun run build:native
bun run cap:sync
bun run cap:android
```

## App Store Screenshots

Automated screenshot generation for App Store Connect using Playwright.

```bash
# Start dev server
bun run dev

# Generate screenshots (in another terminal)
bun run screenshots
```

Screenshots are saved to `screenshots/appstore/` organized by device:

| Folder | Device | Resolution |
|--------|--------|------------|
| `iPhone-6.9/` | iPhone 16 Pro Max | 1320 x 2868 |
| `iPhone-6.5/` | iPhone 11 Pro Max | 1242 x 2688 |
| `iPhone-5.5/` | iPhone 8 Plus | 1242 x 2208 |
| `iPad-12.9/` | iPad Pro 12.9" | 2048 x 2732 |

Screens captured: Home, Play, AI vs AI, Simulation
10 changes: 10 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "knuckletrainer",
"version": "0.1.0",
"version": "1.5.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand All @@ -19,7 +19,8 @@
"generate-app-icons": "node scripts/generate-app-icons.mjs",
"cap:sync": "bunx cap sync",
"cap:ios": "bunx cap open ios",
"cap:android": "bunx cap open android"
"cap:android": "bunx cap open android",
"screenshots": "bun run scripts/appstore-screenshots.ts"
},
"dependencies": {
"@capacitor/android": "^8.0.0",
Expand All @@ -43,13 +44,15 @@
},
"devDependencies": {
"@capacitor/cli": "^8.0.0",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"oxfmt": "0.18.0",
"oxlint": "1.33.0",
"playwright": "^1.57.0",
"sharp": "^0.34.5",
"tailwindcss": "^4",
"typescript": "^5",
Expand Down
117 changes: 117 additions & 0 deletions scripts/appstore-screenshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* App Store Screenshot Generator
*
* Captures screenshots at required iOS device sizes for App Store Connect.
* Run with: bun run screenshots
*
* Prerequisites:
* - Dev server running: bun run dev
* - Playwright installed: bun add -d playwright
*/

import { chromium, type Page } from "playwright";
import { mkdir } from "fs/promises";
import { join } from "path";

// App Store required device sizes
const DEVICES = {
"iPhone-6.9": {
name: "iPhone 16 Pro Max",
width: 1320,
height: 2868,
scale: 3,
},
"iPhone-6.5": {
name: "iPhone 11 Pro Max",
width: 1242,
height: 2688,
scale: 3,
},
"iPhone-5.5": {
name: "iPhone 8 Plus",
width: 1242,
height: 2208,
scale: 3,
},
"iPad-12.9": {
name: "iPad Pro 12.9",
width: 2048,
height: 2732,
scale: 2,
},
} as const;

// Screens to capture
const SCREENS = [
{ name: "01-home", path: "/" },
{ name: "02-play", path: "/play" },
{ name: "03-ai-vs-ai", path: "/ai-vs-ai" },
{ name: "04-simulation", path: "/simulation" },
] as const;

const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
const OUTPUT_DIR = join(process.cwd(), "screenshots", "appstore");

async function waitForApp(page: Page) {
await page.waitForLoadState("networkidle");
// Extra time for animations to settle
await page.waitForTimeout(1000);
}

async function takeScreenshots() {
console.log("📸 App Store Screenshot Generator\n");
console.log(`Base URL: ${BASE_URL}`);
console.log(`Output: ${OUTPUT_DIR}\n`);

// Create output directories
for (const deviceKey of Object.keys(DEVICES)) {
await mkdir(join(OUTPUT_DIR, deviceKey), { recursive: true });
}

const browser = await chromium.launch();

for (const [deviceKey, device] of Object.entries(DEVICES)) {
console.log(`\n📱 ${device.name} (${device.width}x${device.height})`);

const context = await browser.newContext({
viewport: {
width: Math.round(device.width / device.scale),
height: Math.round(device.height / device.scale),
},
deviceScaleFactor: device.scale,
isMobile: deviceKey.startsWith("iPhone"),
hasTouch: true,
});

const page = await context.newPage();

for (const screen of SCREENS) {
const url = `${BASE_URL}${screen.path}`;
console.log(` → ${screen.name}`);

await page.goto(url);
await waitForApp(page);

const filename = `${screen.name}.png`;
await page.screenshot({
path: join(OUTPUT_DIR, deviceKey, filename),
fullPage: false,
});
}

await context.close();
}

await browser.close();

console.log("\n✅ Screenshots saved to:", OUTPUT_DIR);
console.log("\nDevice folders:");
for (const [key, device] of Object.entries(DEVICES)) {
console.log(` ${key}/ - ${device.name}`);
}
}

takeScreenshots().catch((error) => {
console.error("Screenshot generation failed:", error);
process.exit(1);
});
Loading