diff --git a/package.json b/package.json index d76518d..ff52dc6 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "vite build", "lint": "eslint .", "preview": "vite preview", + "report:pdf": "node scripts/build-report-pdf.mjs", "deploy": "npm run build && firebase deploy", "deploy:preview": "npm run build && firebase hosting:channel:deploy preview" }, diff --git a/public/images/report/day1-01.webp b/public/images/report/day1-01.webp new file mode 100644 index 0000000..e7a98e8 Binary files /dev/null and b/public/images/report/day1-01.webp differ diff --git a/public/images/report/day1-02.webp b/public/images/report/day1-02.webp new file mode 100644 index 0000000..f0e4012 Binary files /dev/null and b/public/images/report/day1-02.webp differ diff --git a/public/images/report/day1-03.webp b/public/images/report/day1-03.webp new file mode 100644 index 0000000..98b1ea8 Binary files /dev/null and b/public/images/report/day1-03.webp differ diff --git a/public/images/report/day1-04.webp b/public/images/report/day1-04.webp new file mode 100644 index 0000000..d9c7033 Binary files /dev/null and b/public/images/report/day1-04.webp differ diff --git a/public/images/report/day1-05.webp b/public/images/report/day1-05.webp new file mode 100644 index 0000000..4636657 Binary files /dev/null and b/public/images/report/day1-05.webp differ diff --git a/public/images/report/day1-06.webp b/public/images/report/day1-06.webp new file mode 100644 index 0000000..db18b63 Binary files /dev/null and b/public/images/report/day1-06.webp differ diff --git a/public/images/report/day1-07.webp b/public/images/report/day1-07.webp new file mode 100644 index 0000000..4cc668a Binary files /dev/null and b/public/images/report/day1-07.webp differ diff --git a/public/images/report/day1-08.webp b/public/images/report/day1-08.webp new file mode 100644 index 0000000..798e569 Binary files /dev/null and b/public/images/report/day1-08.webp differ diff --git a/public/images/report/day1-09.webp b/public/images/report/day1-09.webp new file mode 100644 index 0000000..bf621f6 Binary files /dev/null and b/public/images/report/day1-09.webp differ diff --git a/public/images/report/day2-01.webp b/public/images/report/day2-01.webp new file mode 100644 index 0000000..b334be1 Binary files /dev/null and b/public/images/report/day2-01.webp differ diff --git a/public/images/report/day2-02.webp b/public/images/report/day2-02.webp new file mode 100644 index 0000000..c26f0fe Binary files /dev/null and b/public/images/report/day2-02.webp differ diff --git a/public/images/report/day2-03.webp b/public/images/report/day2-03.webp new file mode 100644 index 0000000..04b713b Binary files /dev/null and b/public/images/report/day2-03.webp differ diff --git a/public/images/report/day2-04.webp b/public/images/report/day2-04.webp new file mode 100644 index 0000000..ffa2b69 Binary files /dev/null and b/public/images/report/day2-04.webp differ diff --git a/public/images/report/day2-05.webp b/public/images/report/day2-05.webp new file mode 100644 index 0000000..d34e2c7 Binary files /dev/null and b/public/images/report/day2-05.webp differ diff --git a/public/images/report/day2-06.webp b/public/images/report/day2-06.webp new file mode 100644 index 0000000..b33cbee Binary files /dev/null and b/public/images/report/day2-06.webp differ diff --git a/public/images/report/day2-07.webp b/public/images/report/day2-07.webp new file mode 100644 index 0000000..66712ba Binary files /dev/null and b/public/images/report/day2-07.webp differ diff --git a/public/images/report/day2-08.webp b/public/images/report/day2-08.webp new file mode 100644 index 0000000..142902b Binary files /dev/null and b/public/images/report/day2-08.webp differ diff --git a/public/images/report/day2-09.webp b/public/images/report/day2-09.webp new file mode 100644 index 0000000..bad8062 Binary files /dev/null and b/public/images/report/day2-09.webp differ diff --git a/public/images/report/day3-01.webp b/public/images/report/day3-01.webp new file mode 100644 index 0000000..d7ae552 Binary files /dev/null and b/public/images/report/day3-01.webp differ diff --git a/public/images/report/day3-02.webp b/public/images/report/day3-02.webp new file mode 100644 index 0000000..2a9c9d5 Binary files /dev/null and b/public/images/report/day3-02.webp differ diff --git a/public/images/report/day3-03.webp b/public/images/report/day3-03.webp new file mode 100644 index 0000000..3663dc0 Binary files /dev/null and b/public/images/report/day3-03.webp differ diff --git a/public/images/report/day3-04.webp b/public/images/report/day3-04.webp new file mode 100644 index 0000000..b7dfe77 Binary files /dev/null and b/public/images/report/day3-04.webp differ diff --git a/public/images/report/day3-05.webp b/public/images/report/day3-05.webp new file mode 100644 index 0000000..1564681 Binary files /dev/null and b/public/images/report/day3-05.webp differ diff --git a/public/images/report/day3-06.webp b/public/images/report/day3-06.webp new file mode 100644 index 0000000..66e03ce Binary files /dev/null and b/public/images/report/day3-06.webp differ diff --git a/public/images/report/day3-07.webp b/public/images/report/day3-07.webp new file mode 100644 index 0000000..0a98c7a Binary files /dev/null and b/public/images/report/day3-07.webp differ diff --git a/public/images/report/day3-08.webp b/public/images/report/day3-08.webp new file mode 100644 index 0000000..3e9ab77 Binary files /dev/null and b/public/images/report/day3-08.webp differ diff --git a/public/images/report/day3-09.webp b/public/images/report/day3-09.webp new file mode 100644 index 0000000..b92cded Binary files /dev/null and b/public/images/report/day3-09.webp differ diff --git a/public/reports/pycon-cameroon-2026-report-en.pdf b/public/reports/pycon-cameroon-2026-report-en.pdf new file mode 100644 index 0000000..36663d0 Binary files /dev/null and b/public/reports/pycon-cameroon-2026-report-en.pdf differ diff --git a/public/reports/pycon-cameroon-2026-report-fr.pdf b/public/reports/pycon-cameroon-2026-report-fr.pdf new file mode 100644 index 0000000..1b3c823 Binary files /dev/null and b/public/reports/pycon-cameroon-2026-report-fr.pdf differ diff --git a/public/sponsors/aws.svg b/public/sponsors/aws.svg new file mode 100644 index 0000000..4715937 --- /dev/null +++ b/public/sponsors/aws.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/scripts/build-report-pdf.mjs b/scripts/build-report-pdf.mjs new file mode 100644 index 0000000..874f446 --- /dev/null +++ b/scripts/build-report-pdf.mjs @@ -0,0 +1,336 @@ +import { execFileSync } from 'node:child_process'; +import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { agenda, DAYS, sessionTitle } from '../src/data/agenda.js'; +import { sponsorsByTier, sponsorsList } from '../src/data/sponsorsList.js'; +import { partnerCommunities } from '../src/data/partners.js'; +import { sponsorshipDeckUrl } from '../src/data/sponsors.js'; +import { + attendeeTotal, + dayBadge, + headlineStats, + sessionFormats, + sessionLanguages, + sessionTopics, + speakerCountries, +} from '../src/data/reportStats.js'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const publicDir = join(root, 'public'); +const outDir = join(publicDir, 'reports'); +const chrome = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + +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; + +// 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' }); + } +}; + +const esc = (value) => + String(value).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); + +const bars = (title, rows) => { + const max = Math.max(...rows.map((r) => r.count)); + return ` +
+

${esc(title)}

+ ${rows.map((r) => ` +
+
${esc(r.label)}${r.count}
+
+
`).join('')} +
`; +}; + +const page = (body, { n, cls = '', footer }) => ` +
+ ${body} + ${footer ? `` : ''} +
`; + +const sectionTitle = (num, title, highlight) => ` +
${String(num).padStart(2, '0')}
+

${esc(title)} ${esc(highlight)}

`; + +const build = (lang) => { + const i18n = JSON.parse(readFileSync(join(root, `src/i18n/${lang}.json`), 'utf8')); + const r = i18n.report; + const footer = r.pdf.footer; + const contents = [ + r.pdf.foreword, + r.pdf.byNumbers, + `${r.programTitle} ${r.programHighlight}`, + `${r.daysTitle} ${r.daysHighlight}`, + `${r.sponsorsTitle} ${r.sponsorsHighlight}`, + `${r.aidTitle} ${r.aidHighlight}`, + `${r.partnersTitle} ${r.partnersHighlight}`, + ]; + + const cover = page(` +
+
+
+ + ${esc(r.pdf.website)} +
+
+
${esc(r.pdf.kicker)}
+

PyCon & UbuCon
Cameroon 2026

+

${esc(r.meta)}

+
+
`, { n: 1, cls: 'cover' }); + + const foreword = page(` +
+
+
${esc(r.pdf.contents)}
+
    + ${contents.map((c, i) => `
  1. ${String(i + 1).padStart(2, '0')}${esc(c)}
  2. `).join('')} +
+ +
+
+ ${sectionTitle(1, r.summaryTitle, r.summaryHighlight)} +

${esc(r.summaryP1)}

+

${esc(r.summaryP2)}

+

${esc(r.summaryP3)}

+
+
+
+ +
`, { n: 2, footer }); + + const numbers = page(` + ${sectionTitle(2, r.pdf.byNumbers, '')} +
+
${attendeeTotal}
+
+
${esc(r.stats.attendees)}
+

${esc(r.audienceSubtitle)}

+
+
+
+ ${headlineStats.filter((s) => s.key !== 'attendees').map((s) => ` +
${s.value}
${esc(r.stats[s.key])}
`).join('')} +
+
+ ${bars(r.speakersByCountry, speakerCountries.map(([country, count]) => ({ label: country ? r.countryNames?.[country] ?? country : r.countryNotListed, count })))} + +
`, { n: 3, footer }); + + const programme = page(` + ${sectionTitle(3, r.programTitle, r.programHighlight)} +

${esc(r.programSubtitle)}

+
+ ${bars(r.formatsTitle, sessionFormats.map(([type, count]) => ({ label: r.formats[type], count })))} + ${bars(r.languagesTitle, sessionLanguages.map(([key, count]) => ({ label: r.languages[key], count })))} + ${bars(r.topicsTitle, sessionTopics.slice(0, 6).map(([label, count]) => ({ label: r.topicNames?.[label] ?? label, count })))} +
+
+ ${[['day1', 4], ['day2', 3], ['day2', 6], ['day3', 3]].map(([day, n]) => ``).join('')} +
`, { n: 4, footer }); + + const days = DAYS.map((day, i) => { + const d = r[day.key]; + const keys = agenda[day.key].filter((s) => s.id && (s.type === 'keynote' || s.type === 'panel')); + return page(` + ${i === 0 ? sectionTitle(4, r.daysTitle, r.daysHighlight) : ''} +
+ ${esc(dayBadge(day, r.dayWord, lang))} +

${esc(d.title)}

+
+

${esc(d.text)}

+

${esc(r.keySessions)}: ${keys.map((s) => `${esc(sessionTitle(s, lang))} (${esc(s.speaker)})`).join(' · ')}

+
+ ${[1, 2, 3, 4, 5, 6].map((n) => `
${esc(d.photos[n - 1])}
`).join('')} +
`, { n: 5 + i, footer }); + }); + + const tierBlocks = sponsorsByTier(sponsorsList).map(({ tier, sponsors }) => ` +
+
${esc(i18n.sponsors.tierLabels[tier])}
+
+ ${sponsors.map((s) => ``).join('')} +
+
`).join(''); + + const sponsors = page(` + ${sectionTitle(5, r.sponsorsTitle, r.sponsorsHighlight)} +

${esc(r.sponsorsSubtitle)}

+
+ ${r.visibility.map((v) => `

${esc(v.title)}

${esc(v.text)}

`).join('')} +
+

${esc(r.pdf.sponsorTiers)}

+ ${tierBlocks}`, { n: 8, footer }); + + const closing = page(` + ${sectionTitle(6, r.aidTitle, r.aidHighlight)} +

${esc(r.aidSubtitle)}

+
+ ${r.aid.map((v) => `

${esc(v.title)}

${esc(v.text)}

`).join('')} +
+ ${sectionTitle(7, r.partnersTitle, r.partnersHighlight)} +

${esc(r.partnersSubtitle)}

+
+ ${partnerCommunities.map((p) => `
${esc(p.name)}
`).join('')} +
+
+

${esc(r.thanksTitle)} ${esc(r.thanksHighlight)}

+

${esc(r.thanksText)}

+
+ Emailorganizers@pythoncameroon.org + Web${esc(r.pdf.website)} + ${esc(r.sponsorDeck)}${esc(sponsorshipDeckUrl.replace(/^https?:\/\//, ''))} +
+

${esc(r.pdf.closingTitle)}

+
+
`, { n: 9, cls: 'last' }); + + return ` + + + +${esc(r.pdf.edition)} · ${esc(r.pdf.kicker)} + + + + +${[cover, foreword, numbers, programme, ...days, sponsors, closing].join('\n')} + +`; +}; + +mkdirSync(outDir, { recursive: true }); +try { + convertPhotos(); + for (const lang of ['en', 'fr']) { + const html = join(work, `report-${lang}.html`); + const pdf = join(outDir, `pycon-cameroon-2026-report-${lang}.pdf`); + writeFileSync(html, build(lang)); + execFileSync(chrome, [ + '--headless=new', + '--disable-gpu', + '--no-pdf-header-footer', + '--allow-file-access-from-files', + '--virtual-time-budget=20000', + `--print-to-pdf=${pdf}`, + pathToFileURL(html).href, + ], { stdio: 'inherit' }); + console.log(`Wrote ${pdf}`); + } +} finally { + rmSync(work, { recursive: true, force: true }); +} diff --git a/src/App.jsx b/src/App.jsx index f0a5180..dd7603d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -22,6 +22,7 @@ const TouristSites = lazy(() => import('./pages/TouristSites')); const Agenda = lazy(() => import('./pages/Agenda')); const SpeakerDetail = lazy(() => import('./pages/SpeakerDetail')); const SessionDetail = lazy(() => import('./pages/SessionDetail')); +const Report = lazy(() => import('./pages/Report')); class ErrorBoundary extends Component { constructor(props) { @@ -94,6 +95,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index c9df7f6..623863a 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -67,6 +67,7 @@ const Footer = () => {

{t('footer.program')}

{t('footer.agenda')} + {t('footer.report')} {t('footer.speakers')} {t('footer.proposalGuidelines')} {t('footer.venue')} diff --git a/src/components/SponsorShowcase.jsx b/src/components/SponsorShowcase.jsx index ca17abb..da52214 100644 --- a/src/components/SponsorShowcase.jsx +++ b/src/components/SponsorShowcase.jsx @@ -77,7 +77,7 @@ const SilverCard = ({ sponsor, lang }) => (
); -// Shared logo-tile layout for the "bronze", "community", and "deployment" tiers. +// Shared logo-tile layout for the "bronze", "community", and "hosting" tiers. // The description lives in a floating popover shown on hover/focus // (desktop) or tap (touch devices) rather than inline. const LogoTile = ({ sponsor, tier, lang, t }) => { @@ -237,27 +237,10 @@ const SponsorShowcase = ({ sponsors = defaultSponsorsList }) => { ))} )} - - {tier === 'bronze' && ( -
- {tierSponsors.map((sponsor) => ( - - ))} -
- )} - - {tier === 'community' && ( -
- {tierSponsors.map((sponsor) => ( - - ))} -
- )} - - {tier === 'deployment' && ( + {!['diamond', 'gold', 'silver'].includes(tier) && (
{tierSponsors.map((sponsor) => ( - + ))}
)} diff --git a/src/data/reportStats.js b/src/data/reportStats.js new file mode 100644 index 0000000..06c2ec7 --- /dev/null +++ b/src/data/reportStats.js @@ -0,0 +1,30 @@ +import { agenda, DAYS } from './agenda.js'; +import { speakers } from './speakers.js'; +import { sponsorsList } from './sponsorsList.js'; +import { partnerCommunities } from './partners.js'; + +export const attendeeTotal = '457+'; + +export const reportSessions = Object.values(agenda).flat().filter((s) => s.id); + +export const countBy = (items, key) => + Object.entries(items.reduce((acc, item) => ({ ...acc, [key(item)]: (acc[key(item)] || 0) + 1 }), {})) + .sort((a, b) => b[1] - a[1]); + +export const sessionFormats = countBy(reportSessions, (s) => s.type); +export const sessionTopics = countBy(reportSessions, (s) => s.category); +export const sessionLanguages = countBy(reportSessions, (s) => (s.lang === 'en' || s.lang === 'fr' ? s.lang : 'bilingual')); +export const speakerCountries = countBy(speakers, (s) => s.country || ''); + +export const headlineStats = [ + { key: 'attendees', value: attendeeTotal }, + { key: 'speakers', value: speakers.length }, + { key: 'sessions', value: reportSessions.length }, + { key: 'days', value: DAYS.length }, + { key: 'countries', value: speakerCountries.filter(([country]) => country).length }, + { key: 'sponsors', value: sponsorsList.length }, + { key: 'partners', value: partnerCommunities.length }, +]; + +export const dayBadge = (day, dayWord, lang) => + `${dayWord} ${DAYS.indexOf(day) + 1} · ${new Date(`${day.date} 12:00`).toLocaleDateString(lang, { day: 'numeric', month: 'long', year: 'numeric' })}`; diff --git a/src/data/speakers.js b/src/data/speakers.js index dc9ab77..bed63e4 100644 --- a/src/data/speakers.js +++ b/src/data/speakers.js @@ -324,6 +324,7 @@ export const speakers = [ name: 'Veliswa Boya', photo: '/speakers/Veliswa_Boya.webp', title: 'Developer Advocate at AWS', + country: 'South Africa', keynote: true, bio: 'Senior Developer Advocate at AWS, working especially with (but not limited to) developers in the sub-Saharan Africa region. Formerly an AWS Community Hero, she was the first woman out of Africa to be named an AWS Hero. Her background spans software development, business and systems analysis, solutions architecture, and cloud engineering, and she loves working with people who are new to tech.', linkedin: 'https://www.linkedin.com/in/veliswa-boya/', diff --git a/src/data/sponsorsList.js b/src/data/sponsorsList.js index 1fe7017..c38b8f2 100644 --- a/src/data/sponsorsList.js +++ b/src/data/sponsorsList.js @@ -1,10 +1,10 @@ -export const TIERS = ['diamond', 'gold', 'silver', 'bronze', 'community', 'deployment']; +export const TIERS = ['diamond', 'gold', 'silver', 'dinner', 'bronze', 'community', 'hosting']; /** * @typedef {Object} Sponsor * @property {string} id - unique, slug * @property {string} name - * @property {'diamond'|'gold'|'silver'|'bronze'|'community'|'deployment'} tier + * @property {'diamond'|'gold'|'silver'|'dinner'|'bronze'|'community'|'hosting'} tier * @property {string} logo - square, ~400x400, transparent or white background * @property {string} [website] - optional * @property {{ fr: string, en: string }} description - 1 to 3 sentences @@ -13,7 +13,7 @@ export const TIERS = ['diamond', 'gold', 'silver', 'bronze', 'community', 'deplo // { // id: 'acme', // unique, slug // name: 'ACME Corp', -// tier: 'gold', // 'diamond' | 'gold' | 'silver' | 'bronze' | 'community' | 'deployment' +// tier: 'gold', // 'diamond' | 'gold' | 'silver' | 'bronze' | 'community' | 'hosting' // logo: '/sponsors/acme.webp', // carré, ~400×400, fond transparent ou blanc // website: 'https://acme.example', // optionnel // description: { fr: '…', en: '…' } // 1 à 3 phrases @@ -32,6 +32,17 @@ export const sponsorsList = [ en: "Canonical delivers secure, enterprise-grade open-source solutions used by millions of developers and organizations worldwide. Its flagship product, Ubuntu, is the world's most used open-source OS and a go-to platform for cloud infrastructure and Python development. Canonical also offers MicroK8s, LXD, and Ubuntu Pro.", }, }, + { + id: 'aws', + name: 'AWS', + tier: 'dinner', + logo: '/sponsors/aws.svg', + website: 'https://aws.amazon.com', + description: { + fr: "Amazon Web Services (AWS) est la plateforme cloud d'Amazon, qui propose plus de 200 services — calcul, stockage, bases de données, IA et machine learning — utilisés par des millions de clients dans le monde. AWS soutient les développeurs Python avec son SDK Boto3 et AWS Lambda. Pour PyCon Cameroon 2026, AWS a offert le dîner des speakers.", + en: "Amazon Web Services (AWS) is Amazon's cloud platform, offering over 200 services — compute, storage, databases, AI and machine learning — used by millions of customers worldwide. AWS supports Python developers with its Boto3 SDK and AWS Lambda. For PyCon Cameroon 2026, AWS hosted the speaker dinner.", + }, + }, { id: 'apify', name: 'Apify', @@ -134,7 +145,7 @@ export const sponsorsList = [ { id: 'pythonanywhere', name: 'PythonAnywhere', - tier: 'deployment', + tier: 'hosting', logo: '/sponsors/pythonanywhere.svg', website: 'https://www.pythonanywhere.com', description: { diff --git a/src/i18n/en.json b/src/i18n/en.json index 480c5ac..24cb11d 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -28,6 +28,7 @@ "builtBy": "Built by", "terms": "Terms and Conditions", "privacy": "Privacy Policy", + "report": "2026 Report", "healthSafety": "Health & Safety" }, "home": { @@ -255,17 +256,19 @@ "diamond": "Diamond Sponsor", "gold": "Gold Sponsors", "silver": "Silver Sponsors", + "dinner": "Speaker Dinner Sponsor", "bronze": "Bronze Sponsors", "community": "Community Sponsors", - "deployment": "Deployment Sponsors" + "hosting": "Hosting Sponsors" }, "tierBadge": { "diamond": "Diamond", "gold": "Gold", "silver": "Silver", + "dinner": "Speaker dinner", "bronze": "Bronze", "community": "Community", - "deployment": "Deployment" + "hosting": "Hosting" }, "visitWebsite": "Visit website", "joinList": "Want to join this list?", @@ -783,5 +786,151 @@ "Lead of Canonical User Group Cameroon" ] } + }, + "report": { + "title": "2026", + "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", + "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.", + "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", + "stats": { + "days": "Days", + "sessions": "Sessions", + "speakers": "Speakers", + "countries": "Speaker countries", + "sponsors": "Sponsors", + "partners": "Partner communities", + "attendees": "Attendees" + }, + "programTitle": "The", + "programHighlight": "Programme", + "programSubtitle": "How the scheduled sessions were spread across formats, topics and languages.", + "formatsTitle": "Formats", + "topicsTitle": "Topics", + "languagesTitle": "Languages", + "formats": { + "keynote": "Keynotes", + "talk": "Talks", + "workshop": "Workshops", + "panel": "Panel" + }, + "languages": { + "en": "English", + "fr": "French", + "bilingual": "Bilingual" + }, + "daysTitle": "Day by", + "daysHighlight": "Day", + "daysSubtitle": "Highlights and photos from each day of the conference.", + "dayWord": "Day", + "keySessions": "Key sessions", + "seeAgenda": "Full agenda", + "openPhoto": "Open full-size photo", + "day1": { + "title": "PyCon opens", + "text": "Attendees queued outside the venue from early morning for registration and welcome coffee. After the opening ceremony, Sarah Abderemane's keynote set the tone, and the day split into three rooms covering web and security, AI and data science, and DevOps and cloud: microservices with Django and FastAPI, a vector search engine built from scratch, Terraform for Django on AWS, ASGI, MCP servers in Python and IoT for car diagnostics. The day closed with a workshop on burnout and cognitive overload, followed by community games.", + "photos": [ + "Packed main hall in front of the PyCon & UbuCon Cameroon backdrop", + "Opening keynote on the main stage", + "Attendees arriving for registration outside the venue", + "A full room during a morning session", + "Lunch break: attendees digging in", + "Hands up from the audience during an interactive session", + "Energy on stage in front of the sponsor wall", + "Attendees taking notes during a talk", + "Thanking our sponsors on the main screen" + ] + }, + "day2": { + "title": "UbuCon Cameroon", + "text": "Day 2 was dedicated to UbuCon Cameroon, co-organized with the Canonical user group. Adonis Simo opened with a keynote on building a SaaS in Africa for the world, followed by a day of hands-on sessions: a roadmap to open source for Cameroonian developers, Django with Valkey for caching and real-time features, Daniele Procida on career success and early-career jobs, Google Workspace AI, two-factor authentication with Django, and the Open Documentation Academy.", + "photos": [ + "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", + "Lunch together between workshops", + "Attendees working through a hands-on workshop", + "Friends made at UbuCon", + "Conference swag handed out on stage", + "All smiles at the sponsor wall" + ] + }, + "day3": { + "title": "PyCon closes", + "text": "Veliswa Boya's keynote on keeping the human in the loop as AI writes more code opened the final day. Three rooms then covered Python in the browser, concurrency, Metabase dashboards, creative coding with Matplotlib, AI workflows and reactive Django. A bilingual panel on tech jobs and AI brought together Daniele Procida, Veliswa Boya, Leslye Nkwa and Marc Daha, followed by talks on security, smart agriculture and WhatsApp bots. Open lightning talks and a live graffiti artwork led into the closing ceremony, awards and group photos.", + "photos": [ + "Closing group photo with the whole conference", + "Speakers, organizers and volunteers on stage at the closing ceremony", + "The bilingual panel on tech jobs and AI", + "A performance during the closing celebrations", + "The live graffiti artwork created during the conference", + "Plates full on the last day", + "Attendees adding their signatures to the artwork", + "An attendee takes the mic", + "Celebrating the end of three great days" + ] + }, + "sponsorsTitle": "Thank you to our", + "sponsorsHighlight": "Sponsors", + "sponsorsSubtitle": "Your support funded the venue, meals, swag, financial aid and the experience above. Here is where your brand showed up.", + "visibilityTitle": "Brand visibility", + "visibility": [ + { + "title": "Stage backdrop & photo wall", + "text": "Sponsor logos sat behind every keynote and in the most-photographed spot of the event, visible in the official group photos." + }, + { + "title": "Roll-up banners", + "text": "Sponsor roll-ups stood in the main hall and breakout rooms for all three days." + }, + { + "title": "On-screen thank-you", + "text": "Sponsors were thanked from the main stage, with their logos on the big screen." + }, + { + "title": "Website", + "text": "Every sponsor is listed with a description and link on the conference website, including this report." + } + ], + "partnersTitle": "Partner", + "partnersHighlight": "Communities", + "partnersSubtitle": "The local communities that helped spread the word, bring attendees and run sessions.", + "thanksTitle": "Thank", + "thanksHighlight": "You", + "thanksText": "To every speaker, volunteer, organizer and attendee who made PyCon & UbuCon Cameroon 2026 happen: thank you. Want to support the next edition, or have questions about this report? Get in touch.", + "contact": "Contact the organizers", + "sponsorDeck": "Sponsorship deck", + "download": "Download the PDF report", + "audienceTitle": "The", + "audienceHighlight": "Audience", + "audienceSubtitle": "Everyone who attended in person across the three days, and where our speakers came from.", + "speakersByCountry": "Speakers by country", + "countryNotListed": "Not listed", + "aidTitle": "Financial", + "aidHighlight": "Aid", + "aidSubtitle": "Part of the budget went straight back to the community, so cost was not the reason someone missed PyCon Cameroon.", + "aid": [ + { "title": "Ticket grants", "text": "Free conference tickets for students and community members who could not afford one." }, + { "title": "Accommodation", "text": "Hotel nights in Yaound\u00e9 for speakers and grant recipients travelling from other cities and countries." }, + { "title": "Transport", "text": "Travel costs to Yaound\u00e9 covered for speakers and grant recipients coming from other regions." } + ], + "pdf": { + "kicker": "Event Report", + "edition": "PyCon & UbuCon Cameroon 2026", + "contents": "Contents", + "foreword": "Foreword", + "byNumbers": "By the numbers", + "footer": "PyCon & UbuCon Cameroon 2026 \u00b7 Event Report", + "sponsorTiers": "Our sponsors", + "closingTitle": "Let's build the next edition together", + "website": "cm.pycon.org/2026" + } } } diff --git a/src/i18n/fr.json b/src/i18n/fr.json index f88f84d..aeab72e 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -28,6 +28,7 @@ "builtBy": "Cr\u00e9\u00e9 par", "terms": "Conditions g\u00e9n\u00e9rales", "privacy": "Politique de confidentialit\u00e9", + "report": "Rapport 2026", "healthSafety": "Sant\u00e9 & S\u00e9curit\u00e9" }, "home": { @@ -255,17 +256,19 @@ "diamond": "Sponsor Diamond", "gold": "Sponsors Gold", "silver": "Sponsors Silver", + "dinner": "Sponsor du d\u00eener des speakers", "bronze": "Sponsors Bronze", "community": "Sponsors Communaut\u00e9", - "deployment": "Sponsors D\u00e9ploiement" + "hosting": "Sponsors H\u00e9bergement" }, "tierBadge": { "diamond": "Diamond", "gold": "Gold", "silver": "Silver", + "dinner": "D\u00eener speakers", "bronze": "Bronze", "community": "Community", - "deployment": "Deployment" + "hosting": "H\u00e9bergement" }, "visitWebsite": "Visiter le site", "joinList": "Vous souhaitez rejoindre cette liste ?", @@ -784,5 +787,173 @@ "Responsable du groupe d'utilisateurs Canonical Cameroun" ] } + }, + "report": { + "title": "Rapport", + "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", + "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.", + "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", + "stats": { + "days": "Jours", + "sessions": "Sessions", + "speakers": "Intervenants", + "countries": "Pays des intervenants", + "sponsors": "Sponsors", + "partners": "Communaut\u00e9s partenaires", + "attendees": "Participants" + }, + "programTitle": "Le", + "programHighlight": "Programme", + "programSubtitle": "R\u00e9partition des sessions programm\u00e9es par format, th\u00e8me et langue.", + "formatsTitle": "Formats", + "topicsTitle": "Th\u00e8mes", + "languagesTitle": "Langues", + "formats": { + "keynote": "Keynotes", + "talk": "Talks", + "workshop": "Ateliers", + "panel": "Panel" + }, + "languages": { + "en": "Anglais", + "fr": "Fran\u00e7ais", + "bilingual": "Bilingue" + }, + "daysTitle": "Jour apr\u00e8s", + "daysHighlight": "jour", + "daysSubtitle": "Les temps forts et les photos de chaque journ\u00e9e.", + "dayWord": "Jour", + "keySessions": "Sessions phares", + "seeAgenda": "Agenda complet", + "openPhoto": "Ouvrir la photo en grand", + "day1": { + "title": "Ouverture de PyCon", + "text": "D\u00e8s le matin, les participants faisaient la queue devant le lieu pour l'enregistrement et le caf\u00e9 d'accueil. Apr\u00e8s la c\u00e9r\u00e9monie d'ouverture, la keynote de Sarah Abderemane a donn\u00e9 le ton, puis la journ\u00e9e s'est r\u00e9partie sur trois salles : web et s\u00e9curit\u00e9, IA et data science, DevOps et cloud. Au programme : microservices avec Django et FastAPI, un moteur de recherche vectoriel construit de z\u00e9ro, Terraform pour Django sur AWS, ASGI, serveurs MCP en Python et IoT pour le diagnostic automobile. La journ\u00e9e s'est conclue par un atelier sur le burnout et la surcharge cognitive, suivi de jeux communautaires.", + "photos": [ + "La grande salle comble devant la banni\u00e8re PyCon & UbuCon Cameroon", + "Keynote d'ouverture sur la sc\u00e8ne principale", + "Arriv\u00e9e des participants pour l'enregistrement devant le lieu", + "Une salle pleine pendant une session du matin", + "Pause d\u00e9jeuner : les participants passent \u00e0 table", + "Mains lev\u00e9es dans le public pendant une session interactive", + "De l'\u00e9nergie sur sc\u00e8ne devant le mur des sponsors", + "Des participants prennent des notes pendant un talk", + "Remerciements aux sponsors sur l'\u00e9cran principal" + ] + }, + "day2": { + "title": "UbuCon Cameroon", + "text": "Le deuxi\u00e8me jour \u00e9tait consacr\u00e9 \u00e0 UbuCon Cameroon, co-organis\u00e9e avec le groupe d'utilisateurs Canonical. Adonis Simo a ouvert avec une keynote sur la cr\u00e9ation d'un SaaS en Afrique pour le monde, suivie d'une journ\u00e9e de sessions pratiques : une feuille de route vers l'open source pour les d\u00e9veloppeurs camerounais, Django avec Valkey pour le cache et le temps r\u00e9el, Daniele Procida sur la r\u00e9ussite professionnelle et le premier emploi, l'IA dans Google Workspace, l'authentification \u00e0 deux facteurs avec Django et l'Open Documentation Academy.", + "photos": [ + "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", + "D\u00e9jeuner ensemble entre deux ateliers", + "Des participants en plein atelier pratique", + "Des amiti\u00e9s n\u00e9es \u00e0 UbuCon", + "Distribution de goodies sur sc\u00e8ne", + "Tout sourire devant le mur des sponsors" + ] + }, + "day3": { + "title": "Cl\u00f4ture de PyCon", + "text": "La keynote de Veliswa Boya sur la place de l'humain quand l'IA \u00e9crit de plus en plus de code a ouvert la derni\u00e8re journ\u00e9e. Trois salles ont ensuite abord\u00e9 Python dans le navigateur, la concurrence, les tableaux de bord Metabase, le creative coding avec Matplotlib, les workflows IA et Django r\u00e9actif. Un panel bilingue sur l'emploi tech et l'IA a r\u00e9uni Daniele Procida, Veliswa Boya, Leslye Nkwa et Marc Daha, suivi de talks sur la s\u00e9curit\u00e9, l'agriculture intelligente et les bots WhatsApp. Des lightning talks ouverts et une fresque graffiti r\u00e9alis\u00e9e en direct ont pr\u00e9c\u00e9d\u00e9 la c\u00e9r\u00e9monie de cl\u00f4ture, les r\u00e9compenses et les photos de groupe.", + "photos": [ + "Photo de groupe de cl\u00f4ture avec toute la conf\u00e9rence", + "Intervenants, organisateurs et b\u00e9n\u00e9voles sur sc\u00e8ne \u00e0 la c\u00e9r\u00e9monie de cl\u00f4ture", + "Le panel bilingue sur l'emploi tech et l'IA", + "Une performance pendant les festivit\u00e9s de cl\u00f4ture", + "La fresque graffiti r\u00e9alis\u00e9e pendant la conf\u00e9rence", + "Assiettes bien remplies le dernier jour", + "Des participants signent la fresque", + "Un participant prend le micro", + "C\u00e9l\u00e9bration de la fin de trois belles journ\u00e9es" + ] + }, + "sponsorsTitle": "Merci \u00e0 nos", + "sponsorsHighlight": "Sponsors", + "sponsorsSubtitle": "Votre soutien a financ\u00e9 le lieu, les repas, les goodies, l'aide financi\u00e8re et tout ce que vous voyez ci-dessus. Voici o\u00f9 votre marque \u00e9tait visible.", + "visibilityTitle": "Visibilit\u00e9 de la marque", + "visibility": [ + { + "title": "Fond de sc\u00e8ne & mur photo", + "text": "Les logos des sponsors \u00e9taient derri\u00e8re chaque keynote et sur l'espace le plus photographi\u00e9 de l'\u00e9v\u00e9nement, visibles sur les photos de groupe officielles." + }, + { + "title": "Roll-ups", + "text": "Les roll-ups des sponsors \u00e9taient install\u00e9s dans la grande salle et les salles parall\u00e8les pendant les trois jours." + }, + { + "title": "Remerciements \u00e0 l'\u00e9cran", + "text": "Les sponsors ont \u00e9t\u00e9 remerci\u00e9s depuis la sc\u00e8ne principale, logos sur grand \u00e9cran." + }, + { + "title": "Site web", + "text": "Chaque sponsor figure avec une description et un lien sur le site de la conf\u00e9rence, y compris dans ce rapport." + } + ], + "partnersTitle": "Communaut\u00e9s", + "partnersHighlight": "partenaires", + "partnersSubtitle": "Les communaut\u00e9s locales qui ont relay\u00e9 l'\u00e9v\u00e9nement, mobilis\u00e9 les participants et anim\u00e9 des sessions.", + "thanksTitle": "Merci", + "thanksHighlight": "\u00e0 tous", + "thanksText": "\u00c0 chaque intervenant, b\u00e9n\u00e9vole, organisateur et participant qui a fait vivre PyCon & UbuCon Cameroon 2026 : merci. Vous souhaitez soutenir la prochaine \u00e9dition, ou avez des questions sur ce rapport ? \u00c9crivez-nous.", + "contact": "Contacter les organisateurs", + "sponsorDeck": "Dossier de sponsoring", + "download": "T\u00e9l\u00e9charger le rapport PDF", + "audienceTitle": "Le", + "audienceHighlight": "Public", + "audienceSubtitle": "Toutes les personnes pr\u00e9sentes sur place pendant les trois jours, et d'o\u00f9 venaient nos intervenants.", + "speakersByCountry": "Intervenants par pays", + "countryNotListed": "Non renseign\u00e9", + "countryNames": { + "Cameroon": "Cameroun", + "United Kingdom": "Royaume-Uni", + "Senegal": "S\u00e9n\u00e9gal", + "Rwanda": "Rwanda", + "France": "France", + "South Africa": "Afrique du Sud", + "Nigeria": "Nigeria" + }, + "topicNames": { + "Machine Learning & AI": "Machine learning & IA", + "Community": "Communaut\u00e9", + "Web Development": "D\u00e9veloppement web", + "Open Source": "Open source", + "Python Core": "Python core", + "DevOps & Infrastructure": "DevOps & infrastructure", + "IoT & Hardware": "IoT & mat\u00e9riel", + "Social Impact": "Impact social", + "Ubuntu & Linux": "Ubuntu & Linux", + "Education": "\u00c9ducation", + "Cybersecurity": "Cybers\u00e9curit\u00e9" + }, + "aidTitle": "Aide", + "aidHighlight": "financi\u00e8re", + "aidSubtitle": "Une partie du budget est retourn\u00e9e directement \u00e0 la communaut\u00e9, pour que le co\u00fbt n'emp\u00eache personne de venir \u00e0 PyCon Cameroon.", + "aid": [ + { "title": "Bourses de billets", "text": "Des billets gratuits pour les \u00e9tudiants et membres de la communaut\u00e9 qui ne pouvaient pas s'en offrir un." }, + { "title": "H\u00e9bergement", "text": "Des nuits d'h\u00f4tel \u00e0 Yaound\u00e9 pour les speakers et boursiers venus d'autres villes et pays." }, + { "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", + "edition": "PyCon & UbuCon Cameroon 2026", + "contents": "Sommaire", + "foreword": "Avant-propos", + "byNumbers": "En chiffres", + "footer": "PyCon & UbuCon Cameroon 2026 \u00b7 Rapport d'activit\u00e9", + "sponsorTiers": "Nos sponsors", + "closingTitle": "Construisons ensemble la prochaine \u00e9dition", + "website": "cm.pycon.org/2026" + } } } diff --git a/src/index.css b/src/index.css index 350557b..ab2e883 100644 --- a/src/index.css +++ b/src/index.css @@ -3085,10 +3085,14 @@ img { color: var(--color-green); } -.sponsor-tier-heading-label.deployment { +.sponsor-tier-heading-label.hosting { color: #9aa5b1; } +.sponsor-tier-heading-label.dinner { + color: var(--color-yellow); +} + .sponsor-tier-heading-line { flex: 1; height: 1px; @@ -3126,10 +3130,14 @@ img { color: var(--color-green); } -.sponsor-showcase-badge.deployment { +.sponsor-showcase-badge.hosting { color: #9aa5b1; } +.sponsor-showcase-badge.dinner { + color: var(--color-yellow); +} + .sponsor-showcase-name { margin-bottom: var(--spacing-xs); } @@ -3267,7 +3275,7 @@ a.sponsor-showcase-name.silver-link:hover { margin-bottom: 0; } -/* Bronze, Community & Deployment (shared logo-tile layout) */ +/* Bronze, Community & Hosting (shared logo-tile layout) */ .sponsor-showcase-tile { display: flex; flex-direction: column; @@ -3884,3 +3892,144 @@ a.sponsor-showcase-tile-name:hover { height: 220px; } } + +.report-hero { + width: 100%; + aspect-ratio: 21 / 9; + object-fit: cover; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); +} + +.report-stats { + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); +} + +.report-stats .stat-number { + font-size: 2.5rem; +} + +.report-hero-number { + font-family: var(--font-tribal); + font-size: clamp(5rem, 14vw, 9rem); + line-height: 1; +} + +.report-bar + .report-bar { + margin-top: var(--spacing-sm); +} + +.report-bar-label { + display: flex; + justify-content: space-between; + gap: var(--spacing-xs); + font-size: 0.9rem; + color: var(--color-text-secondary); + margin-bottom: 4px; +} + +.report-bar-track { + height: 8px; + border-radius: 999px; + background: var(--color-dark-alt); + overflow: hidden; +} + +.report-bar-fill { + height: 100%; + border-radius: inherit; +} + +.report-day + .report-day { + margin-top: var(--spacing-xl); +} + +.report-day-head { + max-width: 860px; + margin-bottom: var(--spacing-md); +} + +.report-day-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 999px; + background: var(--color-orange); + color: white; + font-size: 0.85rem; + font-weight: 600; + margin-bottom: var(--spacing-xs); +} + +.report-day-keys { + font-size: 0.95rem; + color: var(--color-text-secondary); +} + +.report-day-keys a { + color: var(--color-orange); +} + +.report-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + grid-auto-rows: clamp(150px, 16vw, 220px); + grid-auto-flow: dense; + gap: var(--spacing-sm); +} + +.report-photo { + position: relative; + display: block; + overflow: hidden; + border-radius: var(--radius-md); +} + +.report-photo-wide { + grid-column: span 2; + grid-row: span 2; +} + +.report-photo .lazy-image-wrapper, +.report-photo img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center top; + transition: transform 0.4s ease; +} + +.report-photo:hover img, +.report-photo:focus-visible img { + transform: scale(1.05); +} + +.report-photo-caption { + position: absolute; + inset: auto 0 0 0; + padding: var(--spacing-sm) var(--spacing-xs) var(--spacing-xs); + background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); + color: white; + font-size: 0.85rem; + opacity: 0; + transition: opacity 0.3s ease; +} + +.report-photo:hover .report-photo-caption, +.report-photo:focus-visible .report-photo-caption { + opacity: 1; +} + +@media (max-width: 600px) { + .report-gallery { + grid-auto-rows: 55vw; + } + + .report-photo-wide { + grid-column: 1 / -1; + grid-row: auto; + } + + .report-photo-caption { + opacity: 1; + } +} diff --git a/src/pages/Report.jsx b/src/pages/Report.jsx new file mode 100644 index 0000000..fdf246b --- /dev/null +++ b/src/pages/Report.jsx @@ -0,0 +1,253 @@ +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Download, Mail, FileText } from 'lucide-react'; +import { useLocalizedPath } from '../hooks/useLocalizedPath'; +import useScrollAnimation from '../hooks/useScrollAnimation'; +import LazyImage from '../components/LazyImage'; +import PartnerCard from '../components/PartnerCard'; +import SponsorShowcase from '../components/SponsorShowcase'; +import { agenda, DAYS, sessionTitle, TYPE_STYLES } from '../data/agenda'; +import { partnerCommunities } from '../data/partners'; +import { sponsorshipDeckUrl } from '../data/sponsors'; +import { attendeeTotal, dayBadge, headlineStats, sessionFormats, sessionLanguages, sessionTopics, speakerCountries } from '../data/reportStats'; + +const PHOTOS_PER_DAY = 9; +const photoSrc = (day, index) => `/images/report/${day}-${String(index + 1).padStart(2, '0')}.webp`; + +const BarList = ({ title, rows }) => { + const max = Math.max(...rows.map((row) => row.count)); + return ( +
+

{title}

+ {rows.map(({ label, count, color }) => ( +
+
+ {label} + {count} +
+
+
+
+
+ ))} +
+ ); +}; + +const Report = () => { + useScrollAnimation(); + const { t, i18n } = useTranslation(); + const { l } = useLocalizedPath(); + + return ( + <> +
+
+

{t('report.title')} {t('report.titleHighlight')}

+

{t('report.subtitle')}

+

+ {t('report.meta')} +

+ + {t('report.download')} + +
+
+ +
+
+ {t('report.heroAlt')} +
+
+

+ {t('report.summaryTitle')} {t('report.summaryHighlight')} +

+

{t('report.summaryP1')}

+

{t('report.summaryP2')}

+

{t('report.summaryP3')}

+
+
+

{t('report.glanceTitle')}

+
+ {headlineStats.map(({ key, value }) => ( +
+
{value}
+
{t(`report.stats.${key}`)}
+
+ ))} +
+
+
+
+
+ +
+
+
+

{t('report.audienceTitle')} {t('report.audienceHighlight')}

+

{t('report.audienceSubtitle')}

+
+
+
+
{attendeeTotal}
+
{t('report.stats.attendees')}
+
+ ({ label: country ? t(`report.countryNames.${country}`, country) : t('report.countryNotListed'), count, color: 'var(--color-orange)' }))} + /> +
+ +
+

{t('report.aidTitle')} {t('report.aidHighlight')}

+

{t('report.aidSubtitle')}

+
+
+ {t('report.aid', { returnObjects: true }).map((item) => ( +
+

{item.title}

+

{item.text}

+
+ ))} +
+
+
+ +
+ +
+
+
+

{t('report.programTitle')} {t('report.programHighlight')}

+

{t('report.programSubtitle')}

+
+
+ ({ label: t(`report.formats.${type}`), count, color: TYPE_STYLES[type]?.color }))} + /> + ({ label: t(`report.topicNames.${label}`, label), count, color: 'var(--color-orange)' }))} + /> + ({ label: t(`report.languages.${lang}`), count, color: 'var(--color-green)' }))} + /> +
+
+
+ +
+
+
+

{t('report.daysTitle')} {t('report.daysHighlight')}

+

{t('report.daysSubtitle')}

+
+ + {DAYS.map((day) => { + const captions = t(`report.${day.key}.photos`, { returnObjects: true }); + const highlights = agenda[day.key].filter((s) => s.id && (s.type === 'keynote' || s.type === 'panel')); + return ( +
+
+ {dayBadge(day, t('report.dayWord'), i18n.language)} +

{t(`report.${day.key}.title`)}

+

{t(`report.${day.key}.text`)}

+

+ {t('report.keySessions')}:{' '} + {highlights.map((s, i) => ( + + {i > 0 && ' · '} + {sessionTitle(s, i18n.language)} ({s.speaker}) + + ))} +

+
+
+ {Array.from({ length: PHOTOS_PER_DAY }, (_, i) => ( + + + {captions[i]} + + ))} +
+
+ ); + })} + +
+ {t('report.seeAgenda')} +
+
+
+ +
+ + + + +
+
+
+

{t('report.partnersTitle')} {t('report.partnersHighlight')}

+

{t('report.partnersSubtitle')}

+
+
+ {partnerCommunities.map((partner) => ( + + ))} +
+
+
+ +
+
+

{t('report.thanksTitle')} {t('report.thanksHighlight')}

+

+ {t('report.thanksText')} +

+ +
+
+ + ); +}; + +export default Report;