Skip to content
Merged
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
Binary file added public/images/report/cover.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/report/day2-04.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/extra-1.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/extra-2.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/extra-3.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-01.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-02.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-03.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-04.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-05.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-06.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-07.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-08.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-09.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-10.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-11.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-12.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-13.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-14.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-15.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-16.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-17.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-18.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-19.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-20.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/report/gallery-21.webp
Binary file added public/images/report/gallery-22.webp
Binary file added public/images/report/gallery-23.webp
Binary file added public/images/report/gallery-24.webp
Binary file added public/images/report/speaker.webp
Binary file modified public/reports/pycon-cameroon-2026-report-en.pdf
Binary file not shown.
Binary file modified public/reports/pycon-cameroon-2026-report-fr.pdf
Binary file not shown.
35 changes: 24 additions & 11 deletions scripts/build-report-pdf.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ const work = mkdtempSync(join(tmpdir(), 'pycon-report-'));

const asset = (path) => pathToFileURL(join(publicDir, path)).href;
const photo = (day, n) => pathToFileURL(join(work, `${day}-${String(n).padStart(2, '0')}.jpg`)).href;
const extraPhoto = (name) => pathToFileURL(join(work, `${name}.jpg`)).href;

// ponytail: Chrome embeds WebP uncompressed in PDFs, so photos go in as JPEG via macOS `sips`; swap for another converter off macOS.
const convertPhotos = () => {
const dir = join(publicDir, 'images/report');
for (const file of readdirSync(dir).filter((f) => f.endsWith('.webp'))) {
execFileSync('sips', ['-Z', '1400', '-s', 'format', 'jpeg', '-s', 'formatOptions', '75', join(dir, file), '--out', join(work, file.replace(/\.webp$/, '.jpg'))], { stdio: 'ignore' });
execFileSync('sips', ['-Z', file.startsWith('gallery-') ? '600' : '1200', '-s', 'format', 'jpeg', '-s', 'formatOptions', '70', join(dir, file), '--out', join(work, file.replace(/\.webp$/, '.jpg'))], { stdio: 'ignore' });
}
};

Expand Down Expand Up @@ -71,12 +72,13 @@ const build = (lang) => {
`${r.programTitle} ${r.programHighlight}`,
`${r.daysTitle} ${r.daysHighlight}`,
`${r.sponsorsTitle} ${r.sponsorsHighlight}`,
`${r.pdf.galleryTitle} ${r.pdf.galleryHighlight}`,
`${r.aidTitle} ${r.aidHighlight}`,
`${r.partnersTitle} ${r.partnersHighlight}`,
];

const cover = page(`
<div class="cover-photo" style="background-image:url('${photo('day3', 1)}')"></div>
<div class="cover-photo" style="background-image:url('${extraPhoto('cover')}')"></div>
<div class="cover-shade"></div>
<div class="cover-top">
<img src="${asset('images/branding/python-cameroon-logo.webp')}" alt="" class="cover-logo">
Expand All @@ -96,7 +98,7 @@ const build = (lang) => {
<ol class="toc">
${contents.map((c, i) => `<li><span>${String(i + 1).padStart(2, '0')}</span>${esc(c)}</li>`).join('')}
</ol>
<img class="foreword-photo" src="${photo('day1', 2)}" alt="">
<img class="foreword-photo" src="${photo('day1', 7)}" alt="">
</div>
<div>
${sectionTitle(1, r.summaryTitle, r.summaryHighlight)}
Expand All @@ -105,8 +107,9 @@ const build = (lang) => {
<p>${esc(r.summaryP3)}</p>
</div>
</div>
<div class="strip">
<img src="${photo('day1', 3)}" alt=""><img src="${photo('day2', 2)}" alt=""><img src="${photo('day3', 3)}" alt="">
<div class="strip fill">
<img src="${photo('day2', 7)}" alt=""><img src="${extraPhoto('speaker')}" alt=""><img src="${photo('day3', 9)}" alt="">
<img src="${extraPhoto('extra-1')}" alt=""><img src="${extraPhoto('extra-2')}" alt=""><img src="${extraPhoto('extra-3')}" alt="">
</div>`, { n: 2, footer });

const numbers = page(`
Expand All @@ -124,7 +127,7 @@ const build = (lang) => {
</div>
<div class="two-col fill">
${bars(r.speakersByCountry, speakerCountries.map(([country, count]) => ({ label: country ? r.countryNames?.[country] ?? country : r.countryNotListed, count })))}
<img class="panel-photo" src="${photo('day2', 1)}" alt="">
<img class="panel-photo" src="${photo('day2', 9)}" alt="">
</div>`, { n: 3, footer });

const programme = page(`
Expand All @@ -136,7 +139,7 @@ const build = (lang) => {
${bars(r.topicsTitle, sessionTopics.slice(0, 6).map(([label, count]) => ({ label: r.topicNames?.[label] ?? label, count })))}
</div>
<div class="strip tall">
${[['day1', 4], ['day2', 3], ['day2', 6], ['day3', 3]].map(([day, n]) => `<img src="${photo(day, n)}" alt="">`).join('')}
${[['day3', 8], ['day1', 8], ['day2', 8], ['day1', 9]].map(([day, n]) => `<img src="${photo(day, n)}" alt="">`).join('')}
</div>`, { n: 4, footer });

const days = DAYS.map((day, i) => {
Expand Down Expand Up @@ -172,13 +175,19 @@ const build = (lang) => {
<h3 class="sub">${esc(r.pdf.sponsorTiers)}</h3>
${tierBlocks}`, { n: 8, footer });

const gallery = page(`
${sectionTitle(6, r.pdf.galleryTitle, r.pdf.galleryHighlight)}
<div class="gallery">
${Array.from({ length: 24 }, (_, i) => `<img src="${extraPhoto(`gallery-${String(i + 1).padStart(2, '0')}`)}" alt="">`).join('')}
</div>`, { n: 9, footer });

const closing = page(`
${sectionTitle(6, r.aidTitle, r.aidHighlight)}
${sectionTitle(7, r.aidTitle, r.aidHighlight)}
<p class="muted">${esc(r.aidSubtitle)}</p>
<div class="visibility aid">
${r.aid.map((v) => `<div class="vis"><h4>${esc(v.title)}</h4><p>${esc(v.text)}</p></div>`).join('')}
</div>
${sectionTitle(7, r.partnersTitle, r.partnersHighlight)}
${sectionTitle(8, r.partnersTitle, r.partnersHighlight)}
<p class="muted">${esc(r.partnersSubtitle)}</p>
<div class="partners">
${partnerCommunities.map((p) => `<div class="partner"><img src="${asset(p.image.slice(1))}" alt=""><span>${esc(p.name)}</span></div>`).join('')}
Expand All @@ -193,7 +202,7 @@ const build = (lang) => {
</div>
<p class="closing-line gradient">${esc(r.pdf.closingTitle)}</p>
</div>
<div class="toghu bottom"></div>`, { n: 9, cls: 'last' });
<div class="toghu bottom"></div>`, { n: 10, cls: 'last' });

return `<!doctype html>
<html lang="${lang}">
Expand Down Expand Up @@ -248,6 +257,10 @@ h3 { font-size: 13pt; margin-bottom: 3mm; }
.foreword-photo { width: 100%; height: 62mm; object-fit: cover; object-position: center top; border-radius: 2mm; }
.strip { margin-top: auto; padding-top: 6mm; display: grid; grid-template-columns: repeat(3, 1fr); gap: 3mm; }
.strip img { width: 100%; height: 48mm; object-fit: cover; object-position: center top; border-radius: 2mm; }
.gallery { flex: 1; min-height: 0; display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(6, 1fr); gap: 2mm; margin-top: 2mm; }
.gallery img { width: 100%; height: 100%; min-height: 0; object-fit: cover; object-position: center top; border-radius: 1.5mm; }
.strip.fill { flex: 1; min-height: 0; grid-template-rows: 1fr 1fr; }
.strip.fill img { height: 100%; }
.strip.tall { flex: 1; min-height: 0; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.strip.tall img { height: 100%; }

Expand Down Expand Up @@ -308,7 +321,7 @@ h3 { font-size: 13pt; margin-bottom: 3mm; }
</style>
</head>
<body>
${[cover, foreword, numbers, programme, ...days, sponsors, closing].join('\n')}
${[cover, foreword, numbers, programme, ...days, sponsors, gallery, closing].join('\n')}
</body>
</html>`;
};
Expand Down
10 changes: 6 additions & 4 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,10 +793,10 @@
"titleHighlight": "Event Report",
"subtitle": "Three days of Python, Ubuntu and open source in Yaound\u00e9 \u2014 what happened, who made it possible, and what comes next.",
"meta": "September 17\u201319, 2026 \u00b7 Girafe H\u00f4tel, Yaound\u00e9, Cameroon",
"heroAlt": "Attendees of PyCon & UbuCon Cameroon 2026 gathered for the closing group photo",
"heroAlt": "A group of attendees in front of the PyCon & UbuCon Cameroon 2026 banner",
"summaryTitle": "Executive",
"summaryHighlight": "Summary",
"summaryP1": "PyCon Cameroon 2026 brought the country's Python community together for three days at the Girafe H\u00f4tel in Yaound\u00e9, from September 17 to 19. For the first time, the conference was paired with UbuCon Cameroon, a full day dedicated to Ubuntu and open source, co-organized with the local Canonical user group.",
"summaryP1": "PyCon Cameroon 2026, the first PyCon ever held in Cameroon, brought the country's Python community together for three days at the Girafe H\u00f4tel in Yaound\u00e9, from September 17 to 19. The conference was also paired with UbuCon Cameroon, a full day dedicated to Ubuntu and open source, co-organized with the local Canonical user group.",
"summaryP2": "The programme mixed keynotes, talks across three parallel rooms, hands-on workshops, a bilingual panel on tech jobs and AI, lightning talks and community games. Sessions ran in English, French and both, reflecting the bilingual nature of Cameroon's tech scene.",
"summaryP3": "None of this happens without our sponsors, partner communities, speakers and volunteers. This report is our thank-you, and an open account of what their support made possible.",
"glanceTitle": "At a glance",
Expand Down Expand Up @@ -855,7 +855,7 @@
"UbuCon Cameroon group photo at the sponsor wall",
"A full room for UbuCon Cameroon",
"The workshop hall, laptops open",
"A career talk in the main hall",
"Adonis Simo's opening keynote on building a SaaS in Africa",
"Lunch together between workshops",
"Attendees working through a hands-on workshop",
"Friends made at UbuCon",
Expand Down Expand Up @@ -923,7 +923,9 @@
{ "title": "Transport", "text": "Travel costs to Yaound\u00e9 covered for speakers and grant recipients coming from other regions." }
],
"pdf": {
"kicker": "Event Report",
"kicker": "Event Report \u00b7 First Edition",
"galleryTitle": "Photo",
"galleryHighlight": "Gallery",
"edition": "PyCon & UbuCon Cameroon 2026",
"contents": "Contents",
"foreword": "Foreword",
Expand Down
10 changes: 6 additions & 4 deletions src/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -794,10 +794,10 @@
"titleHighlight": "2026",
"subtitle": "Trois jours de Python, d'Ubuntu et d'open source \u00e0 Yaound\u00e9 : ce qui s'est pass\u00e9, qui l'a rendu possible et la suite.",
"meta": "17\u201319 septembre 2026 \u00b7 Girafe H\u00f4tel, Yaound\u00e9, Cameroun",
"heroAlt": "Les participants de PyCon & UbuCon Cameroon 2026 r\u00e9unis pour la photo de groupe de cl\u00f4ture",
"heroAlt": "Un groupe de participants devant la banni\u00e8re PyCon & UbuCon Cameroon 2026",
"summaryTitle": "Synth\u00e8se",
"summaryHighlight": "g\u00e9n\u00e9rale",
"summaryP1": "PyCon Cameroon 2026 a r\u00e9uni la communaut\u00e9 Python du pays pendant trois jours au Girafe H\u00f4tel de Yaound\u00e9, du 17 au 19 septembre. Pour la premi\u00e8re fois, la conf\u00e9rence \u00e9tait associ\u00e9e \u00e0 UbuCon Cameroon, une journ\u00e9e enti\u00e8re consacr\u00e9e \u00e0 Ubuntu et \u00e0 l'open source, co-organis\u00e9e avec le groupe d'utilisateurs Canonical local.",
"summaryP1": "PyCon Cameroon 2026, toute premi\u00e8re PyCon organis\u00e9e au Cameroun, a r\u00e9uni la communaut\u00e9 Python du pays pendant trois jours au Girafe H\u00f4tel de Yaound\u00e9, du 17 au 19 septembre. La conf\u00e9rence \u00e9tait aussi associ\u00e9e \u00e0 UbuCon Cameroon, une journ\u00e9e enti\u00e8re consacr\u00e9e \u00e0 Ubuntu et \u00e0 l'open source, co-organis\u00e9e avec le groupe d'utilisateurs Canonical local.",
"summaryP2": "Le programme a m\u00eal\u00e9 keynotes, talks r\u00e9partis sur trois salles en parall\u00e8le, ateliers pratiques, un panel bilingue sur l'emploi tech et l'IA, des lightning talks et des jeux communautaires. Les sessions se sont tenues en anglais, en fran\u00e7ais et dans les deux langues, \u00e0 l'image de la sc\u00e8ne tech bilingue du Cameroun.",
"summaryP3": "Rien de tout cela n'est possible sans nos sponsors, communaut\u00e9s partenaires, intervenants et b\u00e9n\u00e9voles. Ce rapport est notre fa\u00e7on de les remercier, et un compte rendu transparent de ce que leur soutien a permis.",
"glanceTitle": "En bref",
Expand Down Expand Up @@ -856,7 +856,7 @@
"Photo de groupe UbuCon Cameroon devant le mur des sponsors",
"Une salle comble pour UbuCon Cameroon",
"La salle des ateliers, ordinateurs ouverts",
"Un talk carri\u00e8re dans la grande salle",
"La keynote d'ouverture d'Adonis Simo sur la cr\u00e9ation d'un SaaS en Afrique",
"D\u00e9jeuner ensemble entre deux ateliers",
"Des participants en plein atelier pratique",
"Des amiti\u00e9s n\u00e9es \u00e0 UbuCon",
Expand Down Expand Up @@ -946,7 +946,9 @@
{ "title": "Transport", "text": "Les frais de voyage vers Yaound\u00e9 pris en charge pour les speakers et boursiers venus d'autres r\u00e9gions." }
],
"pdf": {
"kicker": "Rapport d'activit\u00e9",
"kicker": "Rapport d'activit\u00e9 \u00b7 Premi\u00e8re \u00e9dition",
"galleryTitle": "Galerie",
"galleryHighlight": "photos",
"edition": "PyCon & UbuCon Cameroon 2026",
"contents": "Sommaire",
"foreword": "Avant-propos",
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Report.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const Report = () => {

<section className="section bg-dark" id="summary">
<div className="container">
<img src={photoSrc('day3', 0)} alt={t('report.heroAlt')} className="report-hero" />
<img src="/images/report/cover.webp" alt={t('report.heroAlt')} className="report-hero" />
<div className="grid grid-2" style={{ alignItems: 'center', gap: 'var(--spacing-xl)', marginTop: 'var(--spacing-xl)' }}>
<div>
<h2 style={{ marginBottom: 'var(--spacing-md)' }}>
Expand Down
Loading