← back to Costa Rica App
tmp/gen-shots.mjs
191 lines
// Costa Rica App Store screenshots — 5 web-rendered device-spec shots @ 1320x2868 (iPhone 6.9").
// Mirrors the HomesOnSpec approach (Playwright headless render of a styled HTML mock built
// from the app's REAL screens/branding). $0 (local render, no paid API).
// Real app: Expo/React Native, brand green #0a7d55, screens = directory / listing (BEST WAY
// TO CONTACT) / booking (card + SINPE Móvil) / My Trips / Invite friends.
import { chromium } from 'playwright';
import { writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dir = dirname(fileURLToPath(import.meta.url));
const HTML_DIR = join(__dir, 'shots-html');
const OUT_DIR = join(__dir, '..', 'store', 'screenshots');
mkdirSync(HTML_DIR, { recursive: true });
mkdirSync(OUT_DIR, { recursive: true });
const W = 1320, H = 2868; // iPhone 6.9" App Store spec (exact)
const G = '#0a7d55'; // brand green (from app source)
const GD = '#1a2b22', MUTE = '#6b756e', BG = '#f6f7f5', CARDBORDER = '#e8ebe6';
// Shared chrome: iOS status bar + green nav header + brand banner.
const shell = (title, banner, body) => `<!doctype html><html><head><meta charset="utf-8">
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased}
html,body{width:${W}px;height:${H}px;overflow:hidden}
body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',Roboto,Helvetica,Arial,sans-serif;background:${BG};color:${GD}}
.status{height:120px;background:${G};display:flex;align-items:flex-end;justify-content:space-between;padding:0 60px 14px;color:#fff;font-weight:600;font-size:40px}
.status .right{display:flex;gap:18px;align-items:center;font-size:34px}
.nav{background:${G};color:#fff;height:150px;display:flex;align-items:center;padding:0 48px;font-size:56px;font-weight:800;letter-spacing:.3px}
.nav .back{font-size:64px;font-weight:400;margin-right:28px}
.banner{background:${G};color:#fff;text-align:center;padding:70px 48px 84px}
.banner h1{font-size:78px;font-weight:900;line-height:1.05;letter-spacing:.5px}
.banner p{font-size:44px;opacity:.92;margin-top:26px;font-weight:500;line-height:1.3}
.body{padding:44px 48px}
.search{background:#fff;border:2px solid #e2e5e0;border-radius:22px;height:110px;display:flex;align-items:center;padding:0 40px;color:#9aa39c;font-size:42px;flex:1}
.chips{display:flex;gap:22px;margin:36px 0 12px;flex-wrap:wrap}
.chip{border:3px solid ${G};color:${G};border-radius:44px;padding:22px 44px;font-size:40px;font-weight:600;background:#fff}
.chip.on{background:${G};color:#fff}
.card{background:#fff;border:2px solid ${CARDBORDER};border-radius:28px;padding:40px;margin-top:30px;position:relative}
.card .name{font-size:52px;font-weight:800;color:${GD}}
.card .meta{color:${MUTE};font-size:38px;margin-top:12px}
.card .price{color:${G};font-size:50px;font-weight:800;margin-top:26px}
.card .price .per{color:${MUTE};font-weight:400;font-size:38px}
.card .star{position:absolute;top:40px;right:44px;color:#b8860b;font-size:44px;font-weight:700}
.badge{font-weight:800;text-transform:capitalize;font-size:42px}
</style></head><body>
<div class="status"><span>9:41</span><span class="right">●●● 5G ▮▮▮</span></div>
${body}
</body></html>`;
const listCard = (name, meta, price, per, star) => `
<div class="card">
${star ? `<div class="star">★ ${star}</div>` : ''}
<div class="name">${name}</div>
<div class="meta">${meta}</div>
<div class="price">${price}<span class="per"> / ${per}</span></div>
</div>`;
// ---- 1. Directory / browse ----
const s1 = shell('', '', `
<div class="banner"><h1>All of Costa Rica,<br>in your pocket</h1><p>Stays, tours & local services across all 84 cantones</p></div>
<div class="body" style="padding-top:36px">
<div style="display:flex;gap:24px;align-items:center">
<div class="search">Search stays, tours, services…</div>
<div style="width:110px;height:110px;background:#fff;border:2px solid #e2e5e0;border-radius:22px;display:flex;align-items:center;justify-content:center;font-size:48px">👥</div>
<div style="height:110px;background:${G};color:#fff;border-radius:22px;display:flex;align-items:center;padding:0 40px;font-size:42px;font-weight:600">Trips</div>
</div>
<div class="chips">
<div class="chip on">All</div><div class="chip">Stays</div><div class="chip">Tours</div><div class="chip">Eat</div><div class="chip">Beauty</div>
</div>
${listCard('Villa Pacífica — Tamarindo', 'Guanacaste · tourism hotel', '$189', 'night', '4.9')}
${listCard('Arenal Volcano Sunrise Hike', 'La Fortuna · tourism tour', '$65', 'person', '4.8')}
${listCard('Nosara Beachfront Casa', 'Guanacaste · tourism hotel', '$240', 'night', '4.9')}
${listCard('Soda Típica Manuel Antonio', 'Puntarenas · tourism restaurant', '$18', 'person', '4.7')}
${listCard('Monteverde Cloud Forest Zipline', 'Puntarenas · tourism tour', '$79', 'person', '4.9')}
</div>`);
// ---- 2. Listing detail — BEST WAY TO CONTACT ----
const cbtn = (bg, emoji, label) => `<div style="width:47%;min-height:180px;background:${bg};border-radius:30px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px;color:#fff"><div style="font-size:64px">${emoji}</div><div style="font-weight:800;font-size:44px;margin-top:12px">${label}</div></div>`;
const s2 = shell('', '', `
<div class="nav"><span class="back">‹</span>Listing</div>
<div class="body">
<div style="font-size:58px;font-weight:800;color:${GD}">Villa Pacífica — Tamarindo</div>
<div style="color:${MUTE};font-size:40px;margin-top:14px">Guanacaste · tourism hotel</div>
<div style="color:#b8860b;font-size:42px;margin-top:16px">★ 4.9</div>
<div style="margin-top:28px;line-height:1.35;color:#33413a;font-size:40px">Steps from the sand in Tamarindo. Ocean-view terrace, private pool, and sunset every night. Sleeps 6.</div>
<div style="margin-top:36px;background:#eef5f1;border-radius:28px;padding:44px">
<div style="font-size:56px;font-weight:800;color:${G}">$189 <span style="color:${MUTE};font-weight:400;font-size:40px">/ night</span></div>
<div style="color:#33413a;font-size:38px;margin-top:14px">+ $40 cleaning</div>
<div style="color:${MUTE};font-size:38px;margin-top:18px">Up to 6 guests · Instant book · Free cancellation</div>
</div>
<div style="color:${MUTE};font-size:38px;margin-top:24px">📍 Playa Tamarindo, Santa Cruz</div>
<div style="font-size:56px;font-weight:900;letter-spacing:2px;text-align:center;margin:56px 0 32px;color:${GD}">BEST WAY TO CONTACT</div>
<div style="display:flex;flex-wrap:wrap;gap:28px;justify-content:center">
${cbtn('#25D366','💬','WhatsApp')}${cbtn(G,'📞','Call')}${cbtn('#2b6cb0','✉️','Email')}${cbtn('#7a3cff','📨','Message')}
</div>
</div>
<div style="position:absolute;bottom:0;left:0;right:0;padding:44px 48px 90px"><div style="background:${G};height:150px;border-radius:30px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:50px;font-weight:700">Book now</div></div>`);
// ---- 3. Booking + payment (card / SINPE Móvil) ----
const prow = (k, v, bold) => `<div style="display:flex;justify-content:space-between;padding:14px 0;font-size:${bold?52:42}px;${bold?'font-weight:800;color:'+GD:'color:'+MUTE}"><span>${k}</span><span style="color:${bold?GD:'#1a2b22'}">${v}</span></div>`;
const s3 = shell('', '', `
<div class="nav"><span class="back">‹</span>Book</div>
<div class="body">
<div style="font-size:54px;font-weight:800;color:${GD}">Villa Pacífica — Tamarindo</div>
<div style="background:#fff;border:2px solid ${CARDBORDER};border-radius:28px;padding:40px 48px;margin-top:36px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div><div style="color:${MUTE};font-size:34px">Check-in</div><div style="font-weight:800;color:${GD};font-size:46px;margin-top:6px">Fri, Mar 6</div></div>
<div style="color:${MUTE};font-size:44px">→</div>
<div style="text-align:right"><div style="color:${MUTE};font-size:34px">Check-out</div><div style="font-weight:800;color:${GD};font-size:46px;margin-top:6px">Wed, Mar 11</div></div>
</div>
<div style="height:2px;background:#eee;margin:26px 0"></div>
<div style="display:flex;justify-content:space-between;color:#33413a;font-size:42px"><span>5 nights · 4 guests</span><span style="font-weight:700">$113/night</span></div>
</div>
<div style="background:#fff;border:2px solid ${CARDBORDER};border-radius:28px;padding:48px;margin-top:32px">
${prow('Subtotal','$567')}
${prow('Cleaning','$40')}
${prow('Service fee','$61')}
<div style="height:2px;background:#eee;margin:20px 0"></div>
${prow('Total','$668', true)}
</div>
<div style="margin-top:48px;font-weight:700;color:${GD};font-size:48px">Pay with</div>
<div style="background:${G};height:150px;border-radius:30px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:50px;font-weight:700;margin-top:32px">💳 Card (USD/CRC)</div>
<div style="background:#fff;border:4px solid ${G};height:150px;border-radius:30px;display:flex;align-items:center;justify-content:center;color:${G};font-size:50px;font-weight:700;margin-top:30px">📱 SINPE Móvil</div>
<div style="text-align:center;color:${MUTE};font-size:40px;margin-top:40px;line-height:1.35">Pay by international card or locally with SINPE Móvil — the way Costa Rica actually pays.</div>
<div style="display:flex;align-items:center;justify-content:center;gap:18px;background:#eef6f1;border-radius:26px;padding:34px;margin-top:40px;color:${GD};font-size:40px;font-weight:600">🛡️ Free cancellation before check-in · Booking protected</div>
</div>`);
// ---- 4. My Trips ----
const trip = (name, status, scolor, code, dates, total) => `
<div style="background:#fff;border:2px solid ${CARDBORDER};border-radius:28px;padding:40px;margin-top:30px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div style="font-size:50px;font-weight:700;color:${GD};flex:1">${name}</div>
<div class="badge" style="color:${scolor}">${status}</div>
</div>
<div style="color:${MUTE};font-size:36px;margin-top:16px">${code} · ${dates}</div>
<div style="color:${G};font-size:48px;font-weight:700;margin-top:22px">${total}</div>
</div>`;
const s4 = shell('', '', `
<div class="nav"><span class="back">‹</span>My Bookings</div>
<div class="body">
<div style="font-size:52px;font-weight:800;color:${GD};margin-bottom:8px">Your trips, all in one place</div>
${trip('Villa Pacífica — Tamarindo','Confirmed',G,'CR-8F42A','Sep 10 → Sep 13','$668')}
${trip('Arenal Volcano Sunrise Hike','Confirmed',G,'CR-2B19C','Sep 14','$130')}
${trip('Nosara Beachfront Casa','Pending','#b8860b','CR-9C55D','Oct 02 → Oct 06','$960')}
${trip('Monteverde Zipline','Completed','#356','CR-3A77E','Aug 22','$158')}
</div>`);
// ---- 5. Invite friends ----
const irow = (name, phone, on) => `
<div style="display:flex;align-items:center;background:#fff;border:2px solid ${CARDBORDER};border-radius:24px;padding:36px;margin-bottom:24px">
<div style="flex:1"><div style="font-weight:600;color:${GD};font-size:46px">${name}</div><div style="color:${MUTE};font-size:36px;margin-top:8px">${phone}</div></div>
${on ? `<div style="color:${G};font-weight:700;font-size:44px">● On app</div>` : `<div style="background:${G};color:#fff;font-weight:700;font-size:42px;padding:24px 44px;border-radius:20px">Invite</div>`}
</div>`;
const s5 = shell('', '', `
<div class="nav"><span class="back">‹</span>Invite friends</div>
<div class="body" style="padding-top:20px">
<div style="background:#fff;border-bottom:2px solid ${CARDBORDER};border-radius:24px;padding:44px;margin-bottom:36px">
<div style="font-size:56px;font-weight:800;color:${GD}">Bring your friends</div>
<div style="color:${MUTE};font-size:40px;margin-top:16px;line-height:1.3">See who's already here and invite the rest over WhatsApp. Your contacts are only used for you.</div>
<div style="background:${G};height:130px;border-radius:26px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:46px;font-weight:700;margin-top:36px">Import my contacts</div>
<div style="color:${G};font-weight:600;font-size:42px;margin-top:28px">Imported 214 · 12 already here 🎉</div>
</div>
${irow('Ana Jiménez','+506 8712 4490', true)}
${irow('Marco Solís','+506 8834 0021', false)}
${irow('Laura Vargas','+506 7099 6633', true)}
${irow('Diego Rojas','+506 8455 1180', false)}
</div>`);
const shots = [
['01-directory.png', s1],
['02-listing-contact.png', s2],
['03-booking.png', s3],
['04-trips.png', s4],
['05-invite.png', s5],
];
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 1 });
const page = await ctx.newPage();
for (const [file, html] of shots) {
const htmlPath = join(HTML_DIR, file.replace('.png', '.html'));
writeFileSync(htmlPath, html);
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle' });
await page.waitForTimeout(150);
await page.screenshot({ path: join(OUT_DIR, file), clip: { x: 0, y: 0, width: W, height: H } });
console.log('rendered', file);
}
await browser.close();
console.log('DONE');