- 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>
94 lines
2.9 KiB
JavaScript
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); });
|