Skip to content

Commit 988b587

Browse files
committed
Refactor Home, Speakers, Sponsor, UbuCon, Venue pages; implement TrackSection component for UbuCon and Django tracks, add lazy loading to images, and enhance external link security. Update Vite config for better chunking.
Signed-off-by: Steve Yonkeu <yokwejuste@yahoo.com>
1 parent 02d4255 commit 988b587

18 files changed

Lines changed: 576 additions & 656 deletions

index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,19 @@
3636
<meta name="twitter:image" content="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z1cwq558qdzf0x8o8uuv.jpg">
3737
<meta name="twitter:site" content="@PythonCameroon">
3838

39+
<!-- Preload critical background images -->
40+
<link rel="preload" as="image" href="/images/patterns/97568bbec02a103e305ee0d2bbaa6106.webp">
41+
<link rel="preload" as="image" href="/images/patterns/Toghu.webp">
42+
3943
<!-- Fonts -->
4044
<link rel="preconnect" href="https://fonts.googleapis.com">
4145
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
4246
<link
4347
href="https://fonts.googleapis.com/css2?family=Carter+One&family=Kalam:wght@300;400;700&family=Montserrat:wght@400;500;600;700&family=Permanent+Marker&display=swap"
4448
rel="stylesheet">
49+
<style>
50+
@font-face { font-display: swap; }
51+
</style>
4552
</head>
4653

4754
<body>

src/components/PartnerCard.jsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react';
2+
3+
const PartnerCard = ({ partner }) => (
4+
<a
5+
href={partner.link}
6+
target="_blank"
7+
rel="noopener noreferrer"
8+
className="card animate-on-scroll slide-up"
9+
style={{
10+
display: 'flex',
11+
flexDirection: 'column',
12+
alignItems: 'center',
13+
justifyContent: 'center',
14+
padding: 'var(--spacing-md)',
15+
textAlign: 'center',
16+
textDecoration: 'none',
17+
transition: 'transform 0.3s ease',
18+
width: '100%',
19+
}}
20+
>
21+
<img
22+
src={partner.image}
23+
alt={partner.name}
24+
loading="lazy"
25+
style={{
26+
width: '120px',
27+
height: '120px',
28+
objectFit: 'contain',
29+
marginBottom: 'var(--spacing-sm)',
30+
border: '2px solid var(--color-border)',
31+
borderRadius: '20px',
32+
padding: '8px',
33+
}}
34+
/>
35+
<span style={{ fontWeight: 600, fontSize: '0.9rem' }}>
36+
{partner.name}
37+
</span>
38+
</a>
39+
);
40+
41+
export default PartnerCard;

src/components/TeamCard.jsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import LazyImage from './LazyImage';
3+
4+
const LinkedInIcon = () => (
5+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
6+
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
7+
</svg>
8+
);
9+
10+
const WebIcon = () => (
11+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
12+
<circle cx="12" cy="12" r="10"></circle>
13+
<line x1="2" y1="12" x2="22" y2="12"></line>
14+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
15+
</svg>
16+
);
17+
18+
const TeamCard = ({ member }) => (
19+
<div
20+
className="card team-card animate-on-scroll slide-up"
21+
style={{ flex: '0 1 calc(33.333% - var(--spacing-lg))', minWidth: '280px' }}
22+
>
23+
<div
24+
className="team-card-img"
25+
style={{
26+
background: member.image
27+
? "none"
28+
: `linear-gradient(135deg, var(--color-${member.color}), var(--color-${member.gradient}))`,
29+
display: "flex",
30+
alignItems: "center",
31+
justifyContent: "center",
32+
fontSize: "3rem",
33+
padding: 0,
34+
overflow: "hidden",
35+
}}
36+
>
37+
{member.image ? (
38+
<LazyImage
39+
src={member.image}
40+
alt={member.name}
41+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
42+
/>
43+
) : (
44+
"👤"
45+
)}
46+
</div>
47+
<h4 className="team-card-name">{member.name}</h4>
48+
<p className="team-card-role">{member.role}</p>
49+
<div className="team-card-social">
50+
<a href={member.linkedin} target="_blank" rel="noopener noreferrer" title="LinkedIn">
51+
<LinkedInIcon />
52+
</a>
53+
{member.website && (
54+
<a href={member.website} target="_blank" rel="noopener noreferrer" title="Website">
55+
<WebIcon />
56+
</a>
57+
)}
58+
</div>
59+
</div>
60+
);
61+
62+
export default TeamCard;

src/components/TrackSection.jsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
import { Link } from 'react-router-dom';
3+
4+
const TrackSection = ({ id, bgClass, logo, logoAlt, logoBg, label, title, titleGradient, color, description, highlights, ctaLabel, ctaLink }) => (
5+
<section className={`section ${bgClass || ''}`} id={id}>
6+
<div className="container">
7+
<div className="grid grid-2" style={{ alignItems: 'center', gap: 'var(--spacing-xl)' }}>
8+
<div>
9+
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-md)', marginBottom: 'var(--spacing-md)' }}>
10+
<img
11+
src={logo}
12+
alt={logoAlt}
13+
loading="lazy"
14+
style={{
15+
width: '80px', height: '80px', objectFit: 'contain', borderRadius: '12px',
16+
border: '2px solid var(--color-border)', padding: '6px',
17+
...(logoBg ? { background: logoBg } : {}),
18+
}}
19+
/>
20+
<div>
21+
<span style={{ fontSize: '0.85rem', color, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '1px' }}>New Track</span>
22+
<h2 style={{ margin: 0 }}>{title} <span className="text-gradient">{titleGradient}</span></h2>
23+
</div>
24+
</div>
25+
{description.map((p, i) => <p key={i}>{p}</p>)}
26+
<div className="mt-md flex gap-sm flex-wrap">
27+
<Link to="/speakers" className="btn btn-primary" style={color !== 'var(--color-orange)' ? { background: color } : {}}>
28+
Submit a {ctaLabel} Talk
29+
</Link>
30+
<Link to="/attend" className="btn btn-secondary">
31+
Attend {ctaLabel} Track
32+
</Link>
33+
</div>
34+
</div>
35+
<div className="card" style={{ padding: 'var(--spacing-lg)' }}>
36+
<h3 style={{ marginBottom: 'var(--spacing-md)', color }}>{ctaLabel} Track Highlights</h3>
37+
<ul style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-sm)' }}>
38+
{highlights.map((item, i) => (
39+
<li key={i} style={{ display: 'flex', gap: 'var(--spacing-sm)' }}>
40+
<span style={{ color, fontWeight: 700 }}>{String(i + 1).padStart(2, '0')}</span>
41+
<div>
42+
<strong>{item.title}</strong>
43+
<p style={{ fontSize: '0.9rem', marginBottom: 0 }}>{item.text}</p>
44+
</div>
45+
</li>
46+
))}
47+
</ul>
48+
</div>
49+
</div>
50+
</div>
51+
</section>
52+
);
53+
54+
export default TrackSection;

src/components/VenueCard.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
3+
const VenueCard = ({ venue }) => (
4+
<div className="card animate-on-scroll slide-up">
5+
<div className="card-img">
6+
<img src={venue.image} alt={venue.name} loading="lazy" />
7+
</div>
8+
<h4>{venue.name}</h4>
9+
<p className="card-text">{venue.description}</p>
10+
</div>
11+
);
12+
13+
export default VenueCard;

src/data/partners.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const partnerCommunities = [
2+
{ name: "UbuCon Cameroon", image: "/images/partners/canonical-cm.webp", link: "#" },
3+
{ name: "Django Cameroon", image: "/images/partners/djcmr.webp", link: "https://djangocameroon.org" },
4+
{ name: "Angular Cameroon", image: "/images/partners/angular-cm.webp", link: "https://ngcameroon.org" },
5+
{ name: "AWS User Group Douala", image: "/images/partners/aws-ug-dla.webp", link: "https://dla.awscmr.com" },
6+
{ name: "GDG Yaounde", image: "/images/partners/gdg-yde.webp", link: "https://gdg.community.dev/gdg-yaounde" },
7+
{ name: "Onyani", image: "/images/partners/onyani.webp", link: "https://onyani.com" },
8+
{ name: "Reckot", image: "/images/partners/reckot.webp", link: "https://reckot.com" },
9+
{ name: "Microsoft User Group Cameroon", image: "/images/partners/msf-ug-cm.webp", link: "https://t.me/+uOFiAi93Sr42Nzhk" },
10+
];

src/data/sponsors.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
export const internationalTiers = [
2+
{
3+
name: 'Diamond',
4+
className: 'diamond',
5+
price: 'USD 5,000',
6+
description: 'The ultimate sponsorship package for maximum visibility and impact.',
7+
features: [
8+
'Premium booth placement',
9+
'Keynote speaking opportunity',
10+
'2 speaking slots',
11+
'10 conference tickets',
12+
'Large branding on signage & materials',
13+
'Top-tier logo placement on website',
14+
'Dedicated sponsor page on website',
15+
'Mention in all press releases',
16+
],
17+
btnClass: 'btn btn-primary btn-diamond',
18+
btnStyle: { width: '100%' },
19+
},
20+
{
21+
name: 'Gold',
22+
className: 'gold',
23+
price: 'USD 3,500',
24+
description: 'Excellent visibility and engagement opportunities.',
25+
features: [
26+
'Exhibition booth',
27+
'1 speaking slot',
28+
'5 conference tickets',
29+
'Medium branding on signage & materials',
30+
'Tier 2 logo placement on website',
31+
'Mention in press releases',
32+
],
33+
btnClass: 'btn btn-primary btn-gold',
34+
btnStyle: { width: '100%', background: 'var(--color-dark)', border: '1px solid var(--color-gold)', color: 'var(--color-gold)' },
35+
},
36+
{
37+
name: 'Silver',
38+
className: 'silver',
39+
price: 'USD 2,000',
40+
description: 'Great value for companies looking to support the community.',
41+
features: [
42+
'1 speaking slot',
43+
'3 conference tickets',
44+
'Tier 3 logo placement on website',
45+
],
46+
btnClass: 'btn btn-primary btn-silver',
47+
btnStyle: { width: '100%', background: 'var(--color-dark)', border: '1px solid var(--color-text-secondary)' },
48+
},
49+
{
50+
name: 'Bronze',
51+
className: 'bronze',
52+
price: 'USD 1,000',
53+
description: 'Ideal for startups and smaller organizations.',
54+
features: [
55+
'1 speaking slot',
56+
'1 conference ticket',
57+
'Company logo on website',
58+
'Branding on select materials',
59+
],
60+
btnClass: 'btn btn-primary btn-bronze',
61+
btnStyle: { width: '100%', background: 'var(--color-dark)', border: '1px solid #cd7f32', color: '#cd7f32' },
62+
},
63+
{
64+
name: 'Community',
65+
className: 'community',
66+
price: 'USD 500',
67+
description: 'Ideal for startups and smaller organizations.',
68+
features: [
69+
'1 conference ticket',
70+
'Company logo on website',
71+
'Branding on select materials',
72+
],
73+
btnClass: 'btn btn-primary',
74+
btnStyle: { width: '100%', background: 'var(--color-dark)', border: '1px solid var(--color-green)', color: 'var(--color-green)' },
75+
},
76+
];
77+
78+
export const localTiers = [
79+
{
80+
name: 'Diamond',
81+
className: 'diamond',
82+
price: 'XAF 1,000,000',
83+
priceNote: 'Approx. $2,500 USD',
84+
description: 'The ultimate sponsorship package for maximum visibility and impact.',
85+
features: [
86+
'Premium booth placement',
87+
'Keynote speaking opportunity',
88+
'2 speaking slots',
89+
'10 conference tickets',
90+
'Large branding on signage & materials',
91+
'Top-tier logo placement on website',
92+
'Dedicated sponsor page on website',
93+
'Mention in all press releases',
94+
],
95+
btnClass: 'btn btn-primary btn-diamond',
96+
btnStyle: { width: '100%' },
97+
},
98+
{
99+
name: 'Gold',
100+
className: 'gold',
101+
price: 'XAF 500,000',
102+
priceNote: 'Approx. $1,000 USD',
103+
description: 'Excellent visibility and engagement opportunities.',
104+
features: [
105+
'Exhibition booth',
106+
'1 speaking slot',
107+
'5 conference tickets',
108+
'Medium branding on signage & materials',
109+
'Tier 2 logo placement on website',
110+
'Mention in press releases',
111+
],
112+
btnClass: 'btn btn-primary btn-gold',
113+
btnStyle: { width: '100%', background: 'var(--color-dark)', border: '1px solid var(--color-gold)', color: 'var(--color-gold)' },
114+
},
115+
{
116+
name: 'Silver',
117+
className: 'silver',
118+
price: 'XAF 375,000',
119+
priceNote: 'Approx. $700 USD',
120+
description: 'Great value for companies looking to support the community.',
121+
features: [
122+
'1 speaking slot',
123+
'3 conference tickets',
124+
'Conference signage with branding',
125+
'Tier 3 logo placement on website',
126+
],
127+
btnClass: 'btn btn-primary btn-silver',
128+
btnStyle: { width: '100%', background: 'var(--color-dark)', border: '1px solid var(--color-text-secondary)' },
129+
},
130+
{
131+
name: 'Bronze',
132+
className: 'bronze',
133+
price: 'XAF 100,000',
134+
priceNote: 'Approx. $200 USD',
135+
description: 'Ideal for startups and smaller organizations.',
136+
features: [
137+
'1 speaking slot',
138+
'1 conference ticket',
139+
'Company logo on website',
140+
'Branding on select materials',
141+
],
142+
btnClass: 'btn btn-primary btn-bronze',
143+
btnStyle: { width: '100%', background: 'var(--color-dark)', border: '1px solid #cd7f32', color: '#cd7f32' },
144+
},
145+
{
146+
name: 'Community',
147+
className: 'community',
148+
price: 'XAF 50,000',
149+
priceNote: 'Approx. $100 USD',
150+
description: 'Ideal for startups and smaller organizations.',
151+
features: [
152+
'1 conference ticket',
153+
'Company logo on website',
154+
'Branding on select materials',
155+
],
156+
btnClass: 'btn btn-primary',
157+
btnStyle: { width: '100%', background: 'var(--color-dark)', border: '1px solid var(--color-green)', color: 'var(--color-green)' },
158+
},
159+
];

0 commit comments

Comments
 (0)