Commit de9ab004 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(proposal): Pro Max screenshots, the full feature list, and a real A4 print

The app screenshots came from a 16:9 emulator, so every phone in the
proposal looked squat. They are recaptured on a 1320x2868 @3x emulator — the
iPhone 16 Pro Max's own 440x956pt screen — so the frame now reads as a
modern phone.

The scope section listed what was out of scope, which read as the offer not
covering the club. It is replaced by an inventory of what is built and
working today, checked against both repos: 52 member-app features and 45
across the portal and the gate scanner, grouped into cards, under the four
pillars and the counts (51 app screens, 34 portal sections, 33 wizards,
6 roles). Items the guide implies but the app has no screen for — uploading
a transfer proof, requesting a plan, autopay — are left out.

Print is now true A4. One print stylesheet serves both the browser's Print
and generate-pdf.mjs: printer-safe margins, a cover page, dark bands printed
light, grids held at their desktop columns, every app and portal screenshot
in captioned galleries built into the page, page numbers in the footer, and
breaks that keep sections whole. 17 pages.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 969b2048
This diff is collapsed.
/** /**
* Renders the proposal microsite to PDF. * Renders the proposal microsite to an A4 PDF.
* *
* The site is built for scrolling: the phone shows one app screenshot at a * All the print layout lives in css/styles.css under @media print, and the
* time and the browser frame one portal screenshot at a time. On paper that * screenshot galleries are built into the page by js/app.js — so this script
* would hide almost every screen, so both viewers are replaced by labelled * and the browser's own Print produce the same document. All it adds is
* grids of all their screenshots before printing. * page numbers in the footer.
* *
* node generate-pdf.mjs # from the working tree * node generate-pdf.mjs # from the working tree
* node generate-pdf.mjs --live # from the deployed site * node generate-pdf.mjs --live # from the deployed site
...@@ -20,85 +20,45 @@ const SRC = LIVE ...@@ -20,85 +20,45 @@ const SRC = LIVE
: 'file://' + path.join(__dirname, 'index.html'); : 'file://' + path.join(__dirname, 'index.html');
const OUT = path.join(__dirname, 'عرض-تطبيق-الأعضاء-نادي-الصيد-المصري.pdf'); const OUT = path.join(__dirname, 'عرض-تطبيق-الأعضاء-نادي-الصيد-المصري.pdf');
const wait = ms => new Promise(r => setTimeout(r, ms)); // Kept equal to the @page margins in css/styles.css.
const MARGIN = { top: '14mm', bottom: '16mm', left: '12mm', right: '12mm' };
const FOOTER = `
<div style="width:100%; padding:0 12mm; font-size:7.5px; color:#7C8AA0; direction:rtl;
display:flex; justify-content:space-between; font-family:'Geeza Pro','Arial',sans-serif;">
<span>نادي الصيد المصري — تطبيق الأعضاء وبوابة الإدارة · عرض فني ومالي</span>
<span dir="ltr"><span class="pageNumber"></span> / <span class="totalPages"></span></span>
</div>`;
(async () => { (async () => {
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
headless: 'new', headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--allow-file-access-from-files', '--font-render-hinting=none'], args: ['--no-sandbox', '--disable-setuid-sandbox', '--allow-file-access-from-files', '--font-render-hinting=none'],
}); });
const page = await browser.newPage(); const page = await browser.newPage();
await page.setViewport({ width: 1240, height: 1754, deviceScaleFactor: 2 }); await page.setViewport({ width: 1240, height: 1754, deviceScaleFactor: 2 });
console.log('· loading', LIVE ? 'live site' : 'working tree'); console.log('· loading', LIVE ? 'live site' : 'working tree');
await page.goto(SRC, { waitUntil: 'networkidle0', timeout: 90000 }); await page.goto(SRC, { waitUntil: 'networkidle0', timeout: 90000 });
await page.emulateMediaType('print'); await page.emulateMediaType('print');
await page.evaluate(() => window.dispatchEvent(new Event('beforeprint')));
console.log('· expanding screenshot viewers'); console.log('· waiting for gallery images and fonts');
await page.evaluate(() => { await page.waitForFunction(() => {
document.querySelectorAll('.reveal').forEach(el => el.classList.add('in')); const imgs = [...document.querySelectorAll('.print-app img, .print-portal img')];
const nav = document.getElementById('nav'); if (nav) nav.remove(); return imgs.length > 0 && imgs.every(i => i.complete && i.naturalWidth > 0);
}, { timeout: 90000 });
/* --- app: the single-phone viewer goes; the all-screens grid stays and
becomes the printed gallery, with its images loaded eagerly. --- */
const proto = document.querySelector('#app .proto');
if (proto) proto.remove();
document.querySelectorAll('#shot-grid img').forEach(img => { img.loading = 'eager'; });
/* --- portal: button row + single frame -> every screen, captioned --- */
const btns = [...document.querySelectorAll('.pn-item[data-pshot]')];
const shots = btns.map(b => ({ k: b.dataset.pshot, t: b.textContent.replace(/^\s*\S+\s+/, '').trim() }));
const row = btns.length ? btns[0].parentElement : null;
const frame = document.querySelector('#portal .browser');
const note = document.getElementById('pshot-note');
if (frame) {
frame.insertAdjacentHTML('beforebegin', `
<div class="print-portal">
${shots.map(s => `
<figure>
<img src="screenshots/portal/${s.k}.jpg" alt="">
<figcaption>${s.t}</figcaption>
</figure>`).join('')}
</div>`);
frame.remove();
}
if (row) row.remove();
if (note) note.remove();
document.querySelectorAll('details').forEach(d => d.open = true);
const css = document.createElement('style');
css.textContent = `
/* Six across keeps all seventeen screens on one page; at five the last
row spilled onto a page of its own. */
.shot-grid{ grid-template-columns:repeat(6,1fr); gap:14px 12px; }
.shot-grid button{ border:0; padding:0; box-shadow:none; }
.shot-grid img{ border:1px solid var(--line); }
.print-portal{ display:grid; grid-template-columns:repeat(2,1fr); gap:22px 18px; margin-top:26px; }
.print-portal figure{ margin:0; break-inside:avoid; }
.print-portal img{ display:block; width:100%; height:auto; border:1px solid var(--line); border-radius:10px; }
.print-portal figcaption{ margin-top:7px; font-family:var(--f-head); font-weight:700; font-size:.82rem; color:var(--navy); }
`;
document.head.appendChild(css);
});
console.log('· waiting for images');
await page.waitForFunction(
() => [...document.images].every(img => img.complete && img.naturalWidth > 0),
{ timeout: 60000 },
);
await page.evaluate(() => document.fonts.ready); await page.evaluate(() => document.fonts.ready);
await wait(1200);
console.log('· printing'); console.log('· printing A4');
await page.pdf({ await page.pdf({
path: OUT, path: OUT,
width: '1240px', format: 'A4',
height: '1754px',
printBackground: true, printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 }, margin: MARGIN,
displayHeaderFooter: false, displayHeaderFooter: true,
headerTemplate: '<span></span>',
footerTemplate: FOOTER,
}); });
await browser.close(); await browser.close();
......
This diff is collapsed.
...@@ -212,4 +212,56 @@ ...@@ -212,4 +212,56 @@
} }
pBtns.forEach(function (b) { b.addEventListener('click', function () { showPortal(b.dataset.pshot); }); }); pBtns.forEach(function (b) { b.addEventListener('click', function () { showPortal(b.dataset.pshot); }); });
showPortal('dashboard'); showPortal('dashboard');
/* ---------- feature counts ----------
Counted from the lists themselves, so a band's number can never drift
from what the band actually shows. */
document.querySelectorAll('[data-count-of]').forEach(function (el) {
var band = document.getElementById(el.dataset.countOf);
if (band) el.textContent = band.querySelectorAll('.chips li').length;
});
/* ---------- print galleries ----------
On screen each viewer shows one screenshot at a time; paper needs all of
them. Both galleries live in the page (hidden on screen), so the browser's
own Print produces the same A4 document as generate-pdf.mjs. Their images
attach after load so they never compete with the first paint. */
var appWrap = document.querySelector('#app .wrap');
if (appWrap) {
var pa = document.createElement('div');
pa.className = 'print-app';
pa.innerHTML = shotBtns.map(function (b) {
var k = b.dataset.shot, n = SHOTS[k];
return n ? '<figure><img data-src="screenshots/app/' + k + '.jpg" alt="' + n.t + '">' +
'<figcaption>' + n.t + '</figcaption></figure>' : '';
}).join('');
appWrap.appendChild(pa);
}
var portalWrap = document.querySelector('#portal .wrap');
if (portalWrap) {
var pp = document.createElement('div');
pp.className = 'print-portal';
pp.innerHTML = pBtns.map(function (b, i) {
var k = b.dataset.pshot, n = PSHOTS[k];
return n ? '<figure' + (i === 0 ? ' class="lead"' : '') + '>' +
'<img data-src="screenshots/portal/' + k + '.jpg" alt="' + n.t + '">' +
'<figcaption><b>' + n.t + '</b><span>' + n.p + '</span></figcaption></figure>' : '';
}).join('');
portalWrap.insertBefore(pp, portalWrap.querySelector('.note'));
}
function loadPrintImages() {
document.querySelectorAll('.print-app img[data-src], .print-portal img[data-src]').forEach(function (img) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
});
}
if (document.readyState === 'complete') setTimeout(loadPrintImages, 300);
else window.addEventListener('load', function () { setTimeout(loadPrintImages, 300); });
// Closed <details> print as bare questions; open them all for paper.
window.addEventListener('beforeprint', function () {
loadPrintImages();
document.querySelectorAll('details').forEach(function (d) { d.open = true; });
});
})(); })();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment