Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e8b6b96
feat: add agenda page and integrate into navigation and footer
yokwejuste Jun 13, 2026
29d3078
feat: add speaker detail page and integrate speaker data into the agenda
yokwejuste Jun 13, 2026
db7db73
feat: enhance speaker detail layout and improve styling for better re…
yokwejuste Jun 13, 2026
5b86d1d
feat: refine speaker abstracts and update agenda subtitles for clarity
yokwejuste Jun 13, 2026
2bf1c6c
feat: implement agenda day toggle and enhance speaker detail layout w…
yokwejuste Jun 13, 2026
dcaa328
Merge remote-tracking branch 'origin/main' into feat/agenda
yokwejuste Jun 15, 2026
baf6c5c
feat: enhance agenda layout with new CSS styles and session grouping …
yokwejuste Jun 15, 2026
524a5c6
feat: update speakers section titles and subtitles for improved clarity
yokwejuste Jun 15, 2026
e4b9fc8
feat: populate agenda with full PyCon Cameroon 2026 schedule
yokwejuste Jun 15, 2026
e927f79
feat: add 33 confirmed speakers with photos to speakers page
yokwejuste Jun 15, 2026
6070fb1
feat: reorder speakers list with featured speakers first
yokwejuste Jun 15, 2026
87e8d4e
feat: add .gstack directory to .gitignore
yokwejuste Jun 15, 2026
4abee72
feat: improve agenda readability and link speaker names
yokwejuste Jun 15, 2026
ba8178a
feat: summarize speaker bios and remove Petr Andreev from agenda
yokwejuste Jun 15, 2026
5608abe
feat: remove Petr Andreev's photo from speakers directory
yokwejuste Jun 15, 2026
790cf5c
feat: sort speakers alphabetically and mark Sema's session as workshop
yokwejuste Jun 15, 2026
80c828c
feat: add Dr. Dorothée MAA mental-health plenary workshop
yokwejuste Jun 15, 2026
02bf0c2
fix: stack agenda session metadata for consistent alignment
yokwejuste Jun 15, 2026
4c547ee
fix: improve break-row text contrast in agenda for both themes
yokwejuste Jun 15, 2026
4d5d2c6
feat: remove Petr Andreev from speakers list
yokwejuste Jun 15, 2026
d576802
feat: make Sema's session a plenary mixed-audience workshop
yokwejuste Jun 16, 2026
0a1d555
feat: enhance agenda layout for mobile responsiveness
yokwejuste Jun 16, 2026
3b3b90f
feat: adjust agenda session card styles for improved layout
yokwejuste Jun 16, 2026
44b6d07
feat: update CSS for improved layout and background color handling
yokwejuste Jun 17, 2026
f1232cf
feat: simplify agenda session titles for clarity
yokwejuste Jun 17, 2026
48f4ef3
feat: simplify hero overlay backgrounds for improved clarity
yokwejuste Jun 17, 2026
3625f06
feat: update workshop titles and speakers for improved clarity
yokwejuste Jun 17, 2026
38bd964
feat: implement AgendaSchedule component for improved agenda display
yokwejuste Jun 17, 2026
904b991
feat: update agenda and speaker details to include multiple talks per…
yokwejuste Jun 17, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,4 @@ dist

# Mac
.DS_Store
.gstack/
Binary file added public/speakers/Adonis_Simo.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/speakers/Adrien_Sani.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/speakers/Ariane_Djeupang_J.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/speakers/Ayuk_Princelen_Tanyi.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/speakers/Caleb_Jephuneh.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/speakers/Claude_Ndanda.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/speakers/Dorothee_Maa.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/speakers/Emambou_Ulrich.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/speakers/Fabiol_Dikongue.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/speakers/Harmony_Elendu.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/speakers/Hypolit_Zeuchieu.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/speakers/Jean_Marc_Wogue.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/speakers/Jerry_Davis_Ndjana_Mengue.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/speakers/Johnpaul_Hampo.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/speakers/Kafui_Alordo.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/speakers/Kaizy_Anne_Kum.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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/speakers/Leslye_Nkwa.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/speakers/Linuce_Demanou.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/speakers/Lobga_Julius.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/speakers/Marcela_Djoukouo_Talotsing.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/speakers/Marielle_Daha.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/speakers/Muluh_Azinwi_Success_Ndahili.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/speakers/Mveng_Mboda_Pascal_Franck.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/speakers/Mvenyi_Donald.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/speakers/Ndongmo_Christian.webp
Binary file added public/speakers/Ngongang_Djanze_Nel_Aldric.webp
Binary file added public/speakers/Ntui_Raoul.webp
Binary file added public/speakers/Parkson_Tano_Daniel.webp
Binary file added public/speakers/Patrick_Nounga.webp
Binary file added public/speakers/Sema_Kumbela_Fombutu.webp
Binary file added public/speakers/Tayo_Tate_Desmond_Corentin.webp
Binary file added public/speakers/Vanessa_Manessong.webp
Binary file added public/speakers/Yannik_Kadjie.webp
Binary file added public/speakers/Yunwen_Eric.webp
4 changes: 4 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const CodeOfConduct = lazy(() => import('./pages/CodeOfConduct'));
const UbuCon = lazy(() => import('./pages/UbuCon'));
const FinancialAid = lazy(() => import('./pages/FinancialAid'));
const TouristSites = lazy(() => import('./pages/TouristSites'));
const Agenda = lazy(() => import('./pages/Agenda'));
const SpeakerDetail = lazy(() => import('./pages/SpeakerDetail'));

class ErrorBoundary extends Component {
constructor(props) {
Expand Down Expand Up @@ -86,6 +88,8 @@ function App() {
<Route path="ubucon" element={<LazyPage><UbuCon /></LazyPage>} />
<Route path="financial-aid" element={<LazyPage><FinancialAid /></LazyPage>} />
<Route path="tourist-sites" element={<LazyPage><TouristSites /></LazyPage>} />
<Route path="agenda" element={<LazyPage><Agenda /></LazyPage>} />
<Route path="speakers/:speakerId" element={<LazyPage><SpeakerDetail /></LazyPage>} />
<Route path="*" element={<LegacyRedirect />} />
</Route>

Expand Down
174 changes: 174 additions & 0 deletions src/components/AgendaSchedule.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { Link } from 'react-router-dom';
import { Clock, MapPin, User, Languages } from 'lucide-react';
import { TYPE_STYLES, LANG_LABELS } from '../data/agenda';
import { speakers } from '../data/speakers';
import { useLocalizedPath } from '../hooks/useLocalizedPath';

const normalizeName = (s) =>
(s || '')
.normalize('NFKD')
.replace(/[̀-ͯ]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, ' ')
.trim();

const NAME_ALIASES = {
'kamdem ulrich': 'kamdem-yamen-ulrich-laress-ulrich',
'tayo tate desmond': 'tayo-tate-desmond-corentin',
};

const speakerIdByName = {};
speakers.forEach((sp) => {
speakerIdByName[normalizeName(sp.name)] = sp.id;
});

const resolveSpeakerId = (name) => {
const key = normalizeName(name);
if (NAME_ALIASES[key]) return NAME_ALIASES[key];
if (speakerIdByName[key]) return speakerIdByName[key];
const tokens = key.split(' ').filter(Boolean);
const match = speakers.find((sp) => {
const spKey = normalizeName(sp.name);
return tokens.length > 1 && tokens.every((t) => spKey.includes(t));
});
return match ? match.id : null;
};

const groupByTimeSlot = (sessions) => {
const slots = [];
const indexByTime = new Map();

sessions.forEach((session) => {
const isBreak = session.type === 'break' || session.type === 'social';
if (isBreak) {
slots.push({ time: session.time, kind: 'break', sessions: [session] });
return;
}
if (indexByTime.has(session.time)) {
slots[indexByTime.get(session.time)].sessions.push(session);
return;
}
indexByTime.set(session.time, slots.length);
slots.push({ time: session.time, kind: 'sessions', sessions: [session] });
});

return slots;
};

const BreakRow = ({ session }) => (
<div style={{ display: 'flex', gap: 'var(--spacing-md)', alignItems: 'center', padding: '0.75rem 0' }}>
<span style={{ minWidth: '60px', fontSize: '0.82rem', fontWeight: 600, color: 'var(--color-text-secondary)', fontFamily: 'var(--font-ui)' }}>
{session.time}
</span>
<div style={{ flex: 1, height: '1px', background: 'var(--color-border)', position: 'relative' }}>
<span style={{
position: 'absolute',
left: '1rem',
top: '50%',
transform: 'translateY(-50%)',
background: 'var(--color-black)',
padding: '0 0.75rem',
fontSize: '0.8rem',
fontWeight: 500,
color: 'var(--color-text-secondary)',
fontFamily: 'var(--font-ui)',
whiteSpace: 'nowrap',
}}>
{session.title}{session.room ? ` · ${session.room}` : ''}
</span>
</div>
</div>
);

const SessionCard = ({ session, accentColor }) => {
const { l } = useLocalizedPath();
const style = TYPE_STYLES[session.type] || TYPE_STYLES.talk;
const color = accentColor || style.color;
const speakerId = session.speaker ? resolveSpeakerId(session.speaker) : null;

return (
<div className="card agenda-session-card" style={{
padding: 'var(--spacing-md)',
borderLeft: `4px solid ${color}`,
margin: 0,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 'var(--spacing-sm)', flexWrap: 'wrap', marginBottom: 'var(--spacing-xs)' }}>
<h4 style={{ margin: 0, fontSize: '1rem', color: 'var(--color-text-primary)', fontFamily: 'var(--font-ui)', fontWeight: 700, lineHeight: 1.35 }}>{session.title}</h4>
<span style={{
fontSize: '0.72rem',
fontWeight: 700,
padding: '3px 12px',
borderRadius: '50px',
background: color,
color: 'white',
whiteSpace: 'nowrap',
fontFamily: 'var(--font-ui)',
letterSpacing: '0.05em',
textTransform: 'uppercase',
}}>
{style.label}
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{session.speaker && (
speakerId ? (
<Link
to={l(`/speakers/${speakerId}`)}
style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '0.83rem', color: 'var(--color-orange)', fontFamily: 'var(--font-ui)', fontWeight: 600, textDecoration: 'none' }}
>
<User size={13} style={{ color: 'var(--color-orange)', flexShrink: 0 }} /> {session.speaker}
</Link>
) : (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '0.83rem', color: 'var(--color-text-secondary)', fontFamily: 'var(--font-ui)' }}>
<User size={13} style={{ color: 'var(--color-orange)', flexShrink: 0 }} /> {session.speaker}
</span>
)
)}
{session.room && (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '0.83rem', color: 'var(--color-text-secondary)', fontFamily: 'var(--font-ui)' }}>
<MapPin size={13} style={{ color: 'var(--color-orange)', flexShrink: 0 }} /> {session.room}{session.track ? ` · ${session.track}` : ''}
</span>
)}
{session.lang && (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '0.83rem', color: 'var(--color-text-secondary)', fontFamily: 'var(--font-ui)' }}>
<Languages size={13} style={{ color: 'var(--color-orange)', flexShrink: 0 }} /> {LANG_LABELS[session.lang]}
</span>
)}
</div>
</div>
);
};

const TimeSlot = ({ slot, accentColor }) => {
const parallel = slot.sessions.length > 1;

return (
<div className="agenda-slot">
<div className="agenda-slot-time">
<Clock size={13} style={{ opacity: 0.7 }} />
<span>{slot.time}</span>
</div>
<div className={`agenda-slot-tracks${parallel ? ' is-parallel' : ''}`}>
{slot.sessions.map((session, i) => (
<SessionCard key={i} session={session} accentColor={accentColor} />
))}
</div>
</div>
);
};

const AgendaSchedule = ({ sessions, accentColor }) => {
const slots = groupByTimeSlot(sessions);

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-sm)' }}>
{slots.map((slot, i) => (
slot.kind === 'break'
? <BreakRow key={i} session={slot.sessions[0]} />
: <TimeSlot key={i} slot={slot} accentColor={accentColor} />
))}
</div>
);
};

export default AgendaSchedule;
1 change: 1 addition & 0 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const Footer = () => {
<div>
<h4 className="footer-title">{t('footer.program')}</h4>
<div className="footer-links">
<Link to={l('/agenda')}>{t('footer.agenda')}</Link>
<Link to={l('/speakers')}>{t('footer.speakers')}</Link>
<Link to={l('/speakers#guidelines')}>{t('footer.proposalGuidelines')}</Link>
<Link to={l('/venue')}>{t('footer.venue')}</Link>
Expand Down
2 changes: 2 additions & 0 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const Navbar = () => {

<div className={`nav-links ${isOpen ? 'active' : ''}`} id="navLinks">
<NavLink to={l('/about')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.about')}</NavLink>
<NavLink to={l('/agenda')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.agenda')}</NavLink>
<NavLink to={l('/speakers')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.speakers')}</NavLink>
<NavLink to={l('/sponsor')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.sponsor')}</NavLink>
<NavLink to={l('/attend')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.attend')}</NavLink>
Expand Down Expand Up @@ -90,6 +91,7 @@ const Navbar = () => {
</div>
<div className="nav-drawer-links">
<NavLink to={l('/about')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.about')}</NavLink>
<NavLink to={l('/agenda')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.agenda')}</NavLink>
<NavLink to={l('/speakers')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.speakers')}</NavLink>
<NavLink to={l('/sponsor')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.sponsor')}</NavLink>
<NavLink to={l('/attend')} className={({ isActive }) => isActive ? "active" : ""}>{t('nav.attend')}</NavLink>
Expand Down
2 changes: 1 addition & 1 deletion src/components/TrackSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const TrackSection = ({ id, bgClass, logo, logoAlt, logoBg, title, titleGradient
{description.map((p, i) => <p key={i}>{p}</p>)}
<div className="mt-md flex gap-sm flex-wrap">
<Link to={l('/speakers')} className="btn btn-primary" style={color !== 'var(--color-orange)' ? { background: color } : {}}>
{t('data.tracks.submitTalk', { label: ctaLabel })}
{t('data.tracks.viewSpeakers', { label: ctaLabel })}
</Link>
<Link to={l('/attend')} className="btn btn-secondary">
{t('data.tracks.attendTrack', { label: ctaLabel })}
Expand Down
Loading
Loading