cohorta/scripts/screenshot.mjs
Vadym Samoilenko a9a5fff659 feat: full visual rebrand + landing redesign + auth page refresh + email fix
- Landing: extract 513-line monolith into 12 focused section components
  (Hero, StatsBand, FeatureGrid, HowItWorks, LivePreview, Comparison,
  UseCases, Testimonials, Pricing, FAQ, FinalCTA, TrustBar)
- Auth pages: replace flat orange panel with animated live mock
  (real persona SVGs, typewriter messages, theme bars); Login label
  fixed to "Email or username"; Register wires ?plan= badge
- Brand: new Logo SVG (C-arc + 3 figures + wordmark/tagline), expanded
  palette tokens, fluid display type scale, framer-motion shared variants
- Header: scroll progress bar, removed non-functional language pill
- Footer: fixed all dead links, legal stubs, new logo
- Legal: /about /privacy /terms /cookies /gdpr real pages added
- Email: FROM_EMAIL default fixed to noreply@ai-impress.com (verified
  apex domain), HTML template rewritten to match new brand
- Tooling: Playwright screenshot script for visual self-check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:02:03 +01:00

94 lines
2.9 KiB
JavaScript

#!/usr/bin/env node
/**
* Visual self-check script — captures screenshots of landing/auth pages.
* Usage:
* node scripts/screenshot.mjs [path] [viewport]
* node scripts/screenshot.mjs / desktop → screenshots/landing-desktop.png
* node scripts/screenshot.mjs /login mobile → screenshots/login-mobile.png
* node scripts/screenshot.mjs --all → all pages, both viewports
*/
import { chromium } from '@playwright/test';
import { mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const screenshotsDir = join(__dirname, '..', 'screenshots');
mkdirSync(screenshotsDir, { recursive: true });
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
const VIEWPORTS = {
desktop: { width: 1440, height: 900 },
mobile: { width: 390, height: 844 },
};
const PAGES = [
{ path: '/', slug: 'landing', fullPage: true },
{ path: '/login', slug: 'login', fullPage: true },
{ path: '/register', slug: 'register', fullPage: true },
];
function slugify(p) {
return p === '/' ? 'landing' : p.replace(/\//g, '').replace(/[^a-z0-9]/g, '-');
}
async function screenshot(browser, pagePath, viewportName, fullPage = true) {
const viewport = VIEWPORTS[viewportName] || VIEWPORTS.desktop;
const context = await browser.newContext({ viewport });
const page = await context.newPage();
const url = BASE_URL + pagePath;
await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
// Scroll through the entire page to trigger whileInView animations
await page.evaluate(async () => {
await new Promise(resolve => {
let totalHeight = 0;
const distance = 400;
const timer = setInterval(() => {
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= document.body.scrollHeight) {
clearInterval(timer);
window.scrollTo(0, 0);
resolve(undefined);
}
}, 80);
});
});
// Wait for animations to settle after scroll
await page.waitForTimeout(1200);
const filename = `${slugify(pagePath)}-${viewportName}.png`;
const outPath = join(screenshotsDir, filename);
await page.screenshot({ path: outPath, fullPage });
console.log(`${filename}`);
await context.close();
return outPath;
}
async function main() {
const args = process.argv.slice(2);
const browser = await chromium.launch();
try {
if (args[0] === '--all') {
for (const { path: p, fullPage } of PAGES) {
for (const vp of Object.keys(VIEWPORTS)) {
await screenshot(browser, p, vp, fullPage);
}
}
} else {
const pagePath = args[0] || '/';
const viewportName = args[1] || 'desktop';
await screenshot(browser, pagePath, viewportName);
}
} finally {
await browser.close();
}
}
main().catch(err => { console.error(err); process.exit(1); });