diff --git a/public/sponsors/.gitkeep b/public/sponsors/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/sponsors/apify.svg b/public/sponsors/apify.svg new file mode 100644 index 0000000..c8894c8 --- /dev/null +++ b/public/sponsors/apify.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/sponsors/bpd.webp b/public/sponsors/bpd.webp new file mode 100644 index 0000000..3b4bfa3 Binary files /dev/null and b/public/sponsors/bpd.webp differ diff --git a/public/sponsors/canonical.webp b/public/sponsors/canonical.webp new file mode 100644 index 0000000..388c175 Binary files /dev/null and b/public/sponsors/canonical.webp differ diff --git a/public/sponsors/chaoss.webp b/public/sponsors/chaoss.webp new file mode 100644 index 0000000..16566aa Binary files /dev/null and b/public/sponsors/chaoss.webp differ diff --git a/public/sponsors/dsf.webp b/public/sponsors/dsf.webp new file mode 100644 index 0000000..e8036e3 Binary files /dev/null and b/public/sponsors/dsf.webp differ diff --git a/public/sponsors/jetbrains.webp b/public/sponsors/jetbrains.webp new file mode 100644 index 0000000..c5c5de5 Binary files /dev/null and b/public/sponsors/jetbrains.webp differ diff --git a/src/App.jsx b/src/App.jsx index c433afa..934f9b1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,7 @@ const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); const Speakers = lazy(() => import('./pages/Speakers')); const Sponsor = lazy(() => import('./pages/Sponsor')); +const Sponsors = lazy(() => import('./pages/Sponsors')); const TicketSponsor = lazy(() => import('./pages/TicketSponsor')); const Grants = lazy(() => import('./pages/Grants')); const Attend = lazy(() => import('./pages/Attend')); @@ -81,6 +82,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index f5c9c20..b1a32bd 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -54,7 +54,7 @@ const Navbar = () => { isActive ? "active" : ""}>{t('nav.about')} isActive ? "active" : ""}>{t('nav.agenda')} isActive ? "active" : ""}>{t('nav.speakers')} - isActive ? "active" : ""}>{t('nav.sponsor')} + isActive ? "active" : ""}>{t('nav.sponsor')} isActive ? "active" : ""}>{t('nav.attend')} isActive ? "active" : ""}>{t('nav.venue')} isActive ? "active" : ""} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> @@ -91,7 +91,7 @@ const Navbar = () => { isActive ? "active" : ""}>{t('nav.about')} isActive ? "active" : ""}>{t('nav.agenda')} isActive ? "active" : ""}>{t('nav.speakers')} - isActive ? "active" : ""}>{t('nav.sponsor')} + isActive ? "active" : ""}>{t('nav.sponsor')} isActive ? "active" : ""}>{t('nav.attend')} isActive ? "active" : ""}>{t('nav.venue')} isActive ? "active" : ""} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> diff --git a/src/components/SponsorMarquee.jsx b/src/components/SponsorMarquee.jsx new file mode 100644 index 0000000..91b46b5 --- /dev/null +++ b/src/components/SponsorMarquee.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { sponsorsList } from '../data/sponsorsList'; +import { useLocalizedPath } from '../hooks/useLocalizedPath'; + +const SponsorMarquee = () => { + const { t } = useTranslation(); + const { l } = useLocalizedPath(); + + if (sponsorsList.length === 0) return null; + + const repeats = Math.max(1, Math.ceil(12 / sponsorsList.length)); + const logos = Array.from({ length: repeats }).flatMap((_, i) => + sponsorsList.map((sponsor) => ({ ...sponsor, key: `${sponsor.id}-${i}` })) + ); + + return ( +
+
+
+

{t('home.sponsorsTitle')} {t('home.sponsorsHighlight')}

+
+
+
+
+
+ {logos.map((sponsor) => ( +
+ {sponsor.name} +
+ ))} +
+ +
+
+
+ + {t('home.sponsorsAll')} + +
+
+ ); +}; + +export default SponsorMarquee; diff --git a/src/components/SponsorShowcase.jsx b/src/components/SponsorShowcase.jsx new file mode 100644 index 0000000..05f7ca1 --- /dev/null +++ b/src/components/SponsorShowcase.jsx @@ -0,0 +1,270 @@ +import React, { useEffect, useId, useRef, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { ArrowUpRight } from 'lucide-react'; +import { sponsorsByTier, sponsorsList as defaultSponsorsList } from '../data/sponsorsList'; +import { useLocalizedPath } from '../hooks/useLocalizedPath'; + +const getDescription = (sponsor, lang) => + sponsor.description?.[lang] ?? sponsor.description?.en ?? ''; + +const DiamondCard = ({ sponsor, t, lang }) => ( +
+
+ {sponsor.name} +
+
+ {t('sponsors.tierBadge.diamond')} +

{sponsor.name}

+

{getDescription(sponsor, lang)}

+ {sponsor.website && ( + + {t('sponsors.visitWebsite')} + + )} +
+
+); + +const GoldCard = ({ sponsor, t, lang }) => ( +
+
+
+ {sponsor.name} +
+
+

{sponsor.name}

+ {t('sponsors.tierBadge.gold')} +
+
+

{getDescription(sponsor, lang)}

+ {sponsor.website && ( + + {t('sponsors.visitWebsite')} + + )} +
+); + +const SilverCard = ({ sponsor, lang }) => ( +
+
+ {sponsor.name} +
+ {sponsor.website ? ( + + {sponsor.name} + + ) : ( +

{sponsor.name}

+ )} +

{getDescription(sponsor, lang)}

+
+); + +// Shared logo-tile layout for the "bronze" and "community" 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 }) => { + const description = getDescription(sponsor, lang); + const popoverId = useId(); + const tileRef = useRef(null); + const [open, setOpen] = useState(false); + const [isBelow, setIsBelow] = useState(false); + + const isTouchDevice = () => + typeof window !== 'undefined' && window.matchMedia?.('(hover: none)').matches; + + const updatePlacement = () => { + const top = tileRef.current?.getBoundingClientRect().top ?? Infinity; + setIsBelow(top < 300); + }; + + const handleTileClick = (e) => { + if (e.target.closest('a')) return; // let real links navigate normally + if (!isTouchDevice()) return; + updatePlacement(); + setOpen((v) => !v); + }; + + useEffect(() => { + if (!open) return undefined; + + const handlePointerDown = (e) => { + if (!tileRef.current?.contains(e.target)) setOpen(false); + }; + const handleKeyDown = (e) => { + if (e.key === 'Escape') setOpen(false); + }; + document.addEventListener('pointerdown', handlePointerDown); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('pointerdown', handlePointerDown); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [open]); + + return ( +
+
+ {sponsor.name} +
+ {sponsor.website ? ( + + {sponsor.name} + + ) : ( + {sponsor.name} + )} + {description && ( +
+
+
+ +
+
+
{sponsor.name}
+ + {t(`sponsors.tierBadge.${tier}`)} + +
+
+

{description}

+ {sponsor.website && ( + + {t('sponsors.visitWebsite')} + + )} +
+ )} +
+ ); +}; + +const SponsorShowcase = ({ sponsors = defaultSponsorsList }) => { + const { t, i18n } = useTranslation(); + const { l } = useLocalizedPath(); + const groups = sponsorsByTier(sponsors); + + if (groups.length === 0) { + return ( +
+
+
+

{t('sponsors.emptyTitle')}

+

{t('sponsors.emptyText')}

+ + {t('sponsors.becomeSponsor')} + +
+
+
+ ); + } + + return ( +
+
+ {groups.map(({ tier, sponsors: tierSponsors }) => ( +
+
+ + {t(`sponsors.tierLabels.${tier}`)} + + +
+ + {tier === 'diamond' && ( +
+ {tierSponsors.map((sponsor) => ( + + ))} +
+ )} + + {tier === 'gold' && ( +
+ {tierSponsors.map((sponsor) => ( + + ))} +
+ )} + + {tier === 'silver' && ( +
+ {tierSponsors.map((sponsor) => ( + + ))} +
+ )} + + {tier === 'bronze' && ( +
+ {tierSponsors.map((sponsor) => ( + + ))} +
+ )} + + {tier === 'community' && ( +
+ {tierSponsors.map((sponsor) => ( + + ))} +
+ )} +
+ ))} + +
+

{t('sponsors.joinList')}

+ + {t('sponsors.becomeSponsor')} + +
+
+
+ ); +}; + +export default SponsorShowcase; diff --git a/src/data/sponsorsList.js b/src/data/sponsorsList.js new file mode 100644 index 0000000..f92d0c8 --- /dev/null +++ b/src/data/sponsorsList.js @@ -0,0 +1,95 @@ +export const TIERS = ['diamond', 'gold', 'silver', 'bronze', 'community']; + +/** + * @typedef {Object} Sponsor + * @property {string} id - unique, slug + * @property {string} name + * @property {'diamond'|'gold'|'silver'|'bronze'|'community'} tier + * @property {string} logo - square, ~400x400, transparent or white background + * @property {string} [website] - optional + * @property {{ fr: string, en: string }} description - 1 to 3 sentences + */ + +// { +// id: 'acme', // unique, slug +// name: 'ACME Corp', +// tier: 'gold', // 'diamond' | 'gold' | 'silver' | 'bronze' | 'community' +// logo: '/sponsors/acme.webp', // carré, ~400×400, fond transparent ou blanc +// website: 'https://acme.example', // optionnel +// description: { fr: '…', en: '…' } // 1 à 3 phrases +// } + +/** @type {Sponsor[]} */ +export const sponsorsList = [ + { + id: 'canonical', + name: 'Canonical', + tier: 'diamond', + logo: '/sponsors/canonical.webp', + website: 'https://canonical.com', + description: { + fr: "Canonical fournit des solutions open source sécurisées de niveau entreprise, auxquelles font confiance des millions de développeurs et les plus grandes organisations mondiales. Sa vaste gamme de produits s'articule autour d'Ubuntu — le système d'exploitation open source le plus utilisé au monde et la plateforme de référence pour l'infrastructure cloud et le développement Python. L'entreprise propose également des technologies cloud-native avancées, telles que MicroK8s, LXD, ainsi que la couverture de sécurité complète d'Ubuntu Pro.", + en: "Canonical delivers secure, enterprise-grade open-source solutions trusted by millions of developers and leading global organizations. Its extensive portfolio is anchored by Ubuntu — the world's most widely used open-source operating system and the go-to platform for cloud infrastructure and seamless Python development. Canonical also offers advanced cloud-native technologies, including MicroK8s, LXD, and the comprehensive enterprise security coverage of Ubuntu Pro.", + }, + }, + { + id: 'apify', + name: 'Apify', + tier: 'bronze', + logo: '/sponsors/apify.svg', + website: 'https://apify.com', + description: { + fr: "Apify fournit une plateforme avancée de web scraping et d'automatisation à laquelle font confiance des milliers de développeurs et d'organisations orientées données à travers le monde. Son vaste écosystème propose plus d'un millier de programmes cloud prêts à l'emploi appelés Actors — les solutions de référence pour l'extraction de données à grande échelle et l'automatisation des processus. L'entreprise offre également de puissants outils de développement, notamment le framework open source Crawlee et un SDK Python dédié pour une intégration fluide.", + en: 'Apify provides an advanced web scraping and automation platform trusted by thousands of developers and data-driven organizations worldwide. Its extensive ecosystem features over a thousand ready-to-use cloud programs called Actors — the go-to solutions for scalable web data extraction and workflow automation. The company also offers robust developer tools, including the open-source Crawlee framework and a dedicated Python SDK for seamless integration into modern tech stacks.', + }, + }, + { + id: 'black-python-devs', + name: 'Black Python Devs', + tier: 'community', + logo: '/sponsors/bpd.webp', + website: 'https://blackpythondevs.com', + description: { + fr: "Black Python Devs cultive une communauté mondiale dynamique dédiée au soutien des développeurs noirs et de couleur à travers l'industrie technologique. Son vaste réseau fournit des ressources essentielles, notamment du mentorat et des opportunités de développement professionnel — l'écosystème de référence pour autonomiser les voix sous-représentées dans le domaine de Python. L'organisation milite également pour la diversité et l'inclusion, favorisant un espace collaboratif pour l'évolution de carrière et l'excellence technique.", + en: "Black Python Devs cultivates a thriving, global community dedicated to supporting Black and people of color developers across the tech industry. Its extensive network provides vital resources, including mentorship and professional development opportunities — the go-to ecosystem for empowering underrepresented voices in Python. The organization also advocates for diversity and inclusion, fostering a collaborative space for career growth and technical excellence.", + }, + }, + { + id: 'chaoss', + name: 'CHAOSS', + tier: 'community', + logo: '/sponsors/chaoss.webp', + website: 'https://chaoss.community', + description: { + fr: "CHAOSS crée des métriques standardisées et des logiciels d'analyse utilisés par les gestionnaires de communautés, les équipes d'ingénierie et les écosystèmes open source du monde entier pour évaluer la santé et la pérennité des communautés. Son écosystème d'outils comprend GrimoireLab et Augur — les plateformes de référence pour collecter et visualiser les données des projets open source. Le projet de la Linux Foundation propose également le tableau de bord 8Knot, ainsi que des méthodologies complètes pour analyser l'engagement des contributeurs.", + en: "CHAOSS creates standard metrics and analytics software used by community managers, engineering teams, and open source ecosystems worldwide to evaluate community health and sustainability. Its ecosystem of tools includes GrimoireLab and Augur — the go-to platforms for collecting and visualizing open source project data. The Linux Foundation project also offers the 8Knot dashboard, alongside comprehensive methodologies for understanding contributor engagement.", + }, + }, + { + id: 'dsf', + name: 'Django Software Foundation', + tier: 'community', + logo: '/sponsors/dsf.webp', + website: 'https://www.djangoproject.com/foundation/', + description: { + fr: "La Django Software Foundation (DSF) protège et fait progresser l'écosystème open source Django, soutenant des millions de développeurs et d'organisations qui conçoivent des applications web modernes à travers le monde. Elle pilote Django — le framework web Python de référence pour créer des applications sécurisées, évolutives et maintenables, conçu pour les perfectionnistes sous pression de délais. La fondation finance également le développement communautaire, octroie des bourses pour les événements et anime une communauté mondiale dynamique et inclusive.", + en: "The Django Software Foundation (DSF) protects and advances the open-source Django ecosystem, supporting millions of developers and organizations powering modern web applications globally. It stewards Django — the go-to Python web framework for building secure, scalable, and maintainable web applications with perfectionists under tight deadlines. The foundation also funds community development, provides event grants, and nurtures a vibrant, inclusive global developer community.", + }, + }, + { + id: 'jetbrains', + name: 'JetBrains', + tier: 'community', + logo: '/sponsors/jetbrains.webp', + website: 'https://www.jetbrains.com', + description: { + fr: "JetBrains crée des outils de développement logiciel intelligents utilisés par plus de 11,4 millions de professionnels et 88 des 100 plus grandes entreprises du classement Fortune Global. Sa gamme de plus de 30 produits comprend des IDE primés, dont PyCharm — l'IDE de référence pour un développement Python productif et efficace. L'entreprise propose également l'assistant de codage basé sur l'IA de JetBrains, ainsi que l'agent de codage Junie.", + en: "JetBrains creates intelligent software development tools used by over 11.4 million professionals and 88 Fortune Global Top 100 companies. Its lineup of more than 30 products includes award-winning IDEs, including PyCharm — the go-to IDE for productive and efficient Python development. The company also offers the JetBrains AI-powered coding assistant, and coding agent Junie.", + }, + }, +]; + +export const sponsorsByTier = (list) => + TIERS.map((tier) => ({ tier, sponsors: list.filter((s) => s.tier === tier) })).filter( + (g) => g.sponsors.length + ); diff --git a/src/i18n/en.json b/src/i18n/en.json index 36121af..31cd549 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -3,7 +3,7 @@ "about": "About", "agenda": "Agenda", "speakers": "Speakers", - "sponsor": "Sponsor", + "sponsor": "Sponsors", "attend": "Attend", "venue": "Venue", "ubucon": "UbuCon" @@ -75,7 +75,11 @@ "readyToJoin": "Ready to Join Us?", "readyToJoinText": "Be part of Cameroon's first PyCon and help shape the future of Python in Africa.", "submitATalk": "Submit a Talk", - "sponsorshipDeck": "Sponsorship Deck" + "sponsorshipDeck": "Sponsorship Deck", + "sponsorsTitle": "Our", + "sponsorsHighlight": "Sponsors", + "sponsorsAll": "See all our sponsors", + "sponsorsAria": "Our sponsors' logos" }, "about": { "title": "About", @@ -240,6 +244,30 @@ "getInvolvedText": "Join companies that are leading the way in supporting the Python community in Africa.", "contactSponsor": "Contact Us for Sponsorship" }, + "sponsors": { + "title": "Our", + "titleHighlight": "Sponsors", + "subtitle": "They make PyCon Cameroon 2026 possible. Thank you!", + "becomeSponsor": "Become a sponsor", + "tierLabels": { + "diamond": "Diamond Sponsor", + "gold": "Gold Sponsors", + "silver": "Silver Sponsors", + "bronze": "Bronze Sponsors", + "community": "Community Sponsors" + }, + "tierBadge": { + "diamond": "Diamond", + "gold": "Gold", + "silver": "Silver", + "bronze": "Bronze", + "community": "Community" + }, + "visitWebsite": "Visit website", + "joinList": "Want to join this list?", + "emptyTitle": "2026 sponsors coming soon", + "emptyText": "Your company could be the first one featured here." + }, "attend": { "title": "Attend", "titleHighlight": "PyCon", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index cf3462c..a3dd4d9 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -3,7 +3,7 @@ "about": "\u00c0 propos", "agenda": "Agenda", "speakers": "Intervenants", - "sponsor": "Sponsoriser", + "sponsor": "Sponsors", "attend": "Participer", "venue": "Lieu", "ubucon": "UbuCon" @@ -75,7 +75,11 @@ "readyToJoin": "Pr\u00eat \u00e0 nous rejoindre ?", "readyToJoinText": "Faites partie du premier PyCon du Cameroun et contribuez \u00e0 fa\u00e7onner l'avenir de Python en Afrique.", "submitATalk": "Soumettre une conf\u00e9rence", - "sponsorshipDeck": "Dossier de sponsoring" + "sponsorshipDeck": "Dossier de sponsoring", + "sponsorsTitle": "Nos", + "sponsorsHighlight": "Sponsors", + "sponsorsAll": "Voir tous nos sponsors", + "sponsorsAria": "Logos de nos sponsors" }, "about": { "title": "\u00c0 propos de", @@ -240,6 +244,30 @@ "getInvolvedText": "Rejoignez les entreprises qui ouvrent la voie en soutenant la communaut\u00e9 Python en Afrique.", "contactSponsor": "Contactez-nous pour le sponsoring" }, + "sponsors": { + "title": "Nos", + "titleHighlight": "Sponsors", + "subtitle": "Ils rendent PyCon Cameroun 2026 possible. Merci !", + "becomeSponsor": "Devenir sponsor", + "tierLabels": { + "diamond": "Sponsor Diamond", + "gold": "Sponsors Gold", + "silver": "Sponsors Silver", + "bronze": "Sponsors Bronze", + "community": "Sponsors Communaut\u00e9" + }, + "tierBadge": { + "diamond": "Diamond", + "gold": "Gold", + "silver": "Silver", + "bronze": "Bronze", + "community": "Community" + }, + "visitWebsite": "Visiter le site", + "joinList": "Vous souhaitez rejoindre cette liste ?", + "emptyTitle": "Les sponsors 2026 arrivent", + "emptyText": "Votre entreprise pourrait \u00eatre la premi\u00e8re affich\u00e9e ici." + }, "attend": { "title": "Participer \u00e0", "titleHighlight": "PyCon", diff --git a/src/index.css b/src/index.css index a783369..7e686a3 100644 --- a/src/index.css +++ b/src/index.css @@ -295,6 +295,38 @@ color: #8b5a2b; } +[data-theme="light"] .sponsor-showcase-card.diamond { + border-color: #00b8d4; +} + +[data-theme="light"] .sponsor-showcase-badge.diamond, +[data-theme="light"] .sponsor-tier-heading-label.diamond { + color: #00838f; +} + +[data-theme="light"] .sponsor-showcase-card.gold { + border-color: #c9a800; +} + +[data-theme="light"] .sponsor-showcase-badge.gold, +[data-theme="light"] .sponsor-tier-heading-label.gold { + color: #b8860b; +} + +[data-theme="light"] .sponsor-showcase-card.silver { + border-color: #909090; +} + +[data-theme="light"] .sponsor-showcase-badge.silver, +[data-theme="light"] .sponsor-tier-heading-label.silver { + color: #666666; +} + +[data-theme="light"] .sponsor-showcase-badge.bronze, +[data-theme="light"] .sponsor-tier-heading-label.bronze { + color: #8b5a2b; +} + [data-theme="light"] .btn-tier.btn-diamond { background: #00b8d4; color: #ffffff; @@ -3003,6 +3035,434 @@ img { font-weight: bold; } +/* =================================== + Sponsor showcase + =================================== */ +.sponsor-tier-block { + margin-bottom: var(--spacing-lg); +} + +.sponsor-tier-block:last-of-type { + margin-bottom: var(--spacing-md); +} + +.sponsor-tier-heading { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); +} + +.sponsor-tier-heading-label { + font-size: 0.875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 2px; + white-space: nowrap; +} + +.sponsor-tier-heading-label.diamond { + color: #b9f2ff; +} + +.sponsor-tier-heading-label.gold { + color: #ffd700; +} + +.sponsor-tier-heading-label.silver { + color: #c0c0c0; +} + +.sponsor-tier-heading-label.bronze { + color: #cd7f32; +} + +.sponsor-tier-heading-label.community { + color: var(--color-green); +} + +.sponsor-tier-heading-line { + flex: 1; + height: 1px; + background: var(--color-border); +} + +.sponsor-showcase-badge { + display: inline-block; + border: 1px solid currentColor; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 2px; + padding: 4px 12px; + border-radius: 50px; + margin-bottom: var(--spacing-xs); +} + +.sponsor-showcase-badge.diamond { + color: #b9f2ff; +} + +.sponsor-showcase-badge.gold { + color: #ffd700; +} + +.sponsor-showcase-badge.silver { + color: #c0c0c0; +} + +.sponsor-showcase-badge.bronze { + color: #cd7f32; +} + +.sponsor-showcase-badge.community { + color: var(--color-green); +} + +.sponsor-showcase-name { + margin-bottom: var(--spacing-xs); +} + +.sponsor-showcase-desc { + color: var(--color-text-secondary); + margin-bottom: var(--spacing-sm); +} + +.sponsor-showcase-link { + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--color-orange); + font-weight: 600; +} + +.sponsor-showcase-link:hover { + text-decoration: underline; +} + +/* Diamond */ +.sponsor-showcase-diamond-list { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.sponsor-showcase-card.diamond { + display: flex; + align-items: center; + gap: var(--spacing-lg); + background: linear-gradient(145deg, var(--color-dark) 0%, var(--color-dark-alt) 100%); + border: 2px solid #b9f2ff; + padding: var(--spacing-lg); +} + +.sponsor-showcase-logo.diamond { + width: 220px; + height: 220px; + background: #ffffff; + border-radius: 20px; + padding: 12px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.sponsor-showcase-logo img { + width: 100%; + height: 100%; + object-fit: contain; +} + +@media (max-width: 768px) { + .sponsor-showcase-card.diamond { + flex-direction: column; + text-align: center; + } + + .sponsor-showcase-logo.diamond { + width: 160px; + height: 160px; + margin: 0 auto; + } +} + +/* Gold */ +.sponsor-showcase-card.gold { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + border: 2px solid #ffd700; +} + +.sponsor-showcase-gold-head { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.sponsor-showcase-logo.gold { + width: 96px; + height: 96px; + background: #ffffff; + border-radius: 16px; + padding: 10px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.sponsor-showcase-card.gold .sponsor-showcase-name { + font-size: 1.5rem; +} + +.sponsor-showcase-card.gold .sponsor-showcase-desc { + font-size: 0.95rem; +} + +/* Silver */ +.sponsor-showcase-card.silver { + border: 1px solid #c0c0c0; + padding: 1.5rem; + text-align: center; +} + +.sponsor-showcase-logo.silver { + width: 72px; + height: 72px; + background: #ffffff; + border-radius: 12px; + padding: 8px; + margin: 0 auto var(--spacing-sm); + display: flex; + align-items: center; + justify-content: center; +} + +.sponsor-showcase-card.silver .sponsor-showcase-name { + display: block; + font-size: 1.1rem; + font-weight: 600; +} + +a.sponsor-showcase-name.silver-link:hover { + color: var(--color-orange); +} + +.sponsor-showcase-card.silver .sponsor-showcase-desc { + font-size: 0.9rem; + color: var(--color-text-muted); + margin-bottom: 0; +} + +/* Bronze & Community (shared logo-tile layout) */ +.sponsor-showcase-tile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: var(--spacing-md); + position: relative; + overflow: visible; + z-index: 1; +} + +@media (hover: hover) { + .sponsor-showcase-tile:hover { + z-index: 30; + } +} + +.sponsor-showcase-tile:focus-within, +.sponsor-showcase-tile:focus-visible, +.sponsor-showcase-tile[data-open="true"] { + z-index: 30; +} + +.sponsor-showcase-tile-logo { + width: 96px; + height: 64px; + background: #ffffff; + border-radius: 12px; + padding: 8px; + border: 2px solid var(--color-border); + margin-bottom: var(--spacing-sm); + display: flex; + align-items: center; + justify-content: center; +} + +.sponsor-showcase-tile-logo img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.sponsor-showcase-tile-name { + font-size: 0.9rem; + font-weight: 600; +} + +a.sponsor-showcase-tile-name:hover { + color: var(--color-orange); +} + +/* Floating popover with the sponsor's full description */ +.sponsor-showcase-popover { + position: absolute; + bottom: calc(100% + 12px); + left: 50%; + width: min(340px, 90vw); + max-width: calc(100vw - 2rem); + background: var(--color-dark-alt); + border: 1px solid var(--color-orange); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + padding: var(--spacing-sm); + text-align: left; + opacity: 0; + pointer-events: none; + transform: translateX(-50%) translateY(6px) scale(0.98); + transition: opacity var(--transition-normal), transform var(--transition-normal); + z-index: 40; +} + +/* Invisible bridge so the pointer doesn't lose :hover crossing the gap */ +.sponsor-showcase-popover::before { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: -12px; + height: 12px; +} + +.sponsor-showcase-popover::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + width: 10px; + height: 10px; + margin-top: -5px; + background: var(--color-dark-alt); + border-right: 1px solid var(--color-orange); + border-bottom: 1px solid var(--color-orange); + transform: translateX(-50%) rotate(45deg); +} + +@media (hover: hover) { + .sponsor-showcase-tile:hover .sponsor-showcase-popover { + opacity: 1; + pointer-events: auto; + transform: translateX(-50%) translateY(0) scale(1); + } +} + +.sponsor-showcase-tile:focus-within .sponsor-showcase-popover, +.sponsor-showcase-tile:focus-visible .sponsor-showcase-popover, +.sponsor-showcase-tile[data-open="true"] .sponsor-showcase-popover { + opacity: 1; + pointer-events: auto; + transform: translateX(-50%) translateY(0) scale(1); +} + +/* Flip below the tile when there isn't enough room above (set via JS) */ +.sponsor-showcase-tile[data-placement="below"] .sponsor-showcase-popover { + bottom: auto; + top: calc(100% + 12px); + transform: translateX(-50%) translateY(-6px) scale(0.98); +} + +.sponsor-showcase-tile[data-placement="below"] .sponsor-showcase-popover::before { + top: -12px; + bottom: auto; +} + +.sponsor-showcase-tile[data-placement="below"] .sponsor-showcase-popover::after { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: -5px; + border-right: none; + border-bottom: none; + border-left: 1px solid var(--color-orange); + border-top: 1px solid var(--color-orange); +} + +.sponsor-showcase-popover-head { + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 0.6rem; +} + +.sponsor-showcase-popover-logo { + width: 40px; + height: 40px; + background: #ffffff; + border-radius: 8px; + padding: 4px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.sponsor-showcase-popover-logo img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.sponsor-showcase-popover-name { + font-family: var(--font-ui); + font-weight: 600; + font-size: 0.95rem; + margin-bottom: 2px; +} + +.sponsor-showcase-popover-desc { + font-size: 0.85rem; + line-height: 1.55; + color: var(--color-text-secondary); + max-height: 240px; + overflow-y: auto; + margin: 0 0 0.6rem; +} + +.sponsor-showcase-popover-link { + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--color-orange); + font-weight: 600; + font-size: 0.85rem; +} + +.sponsor-showcase-popover-link:hover { + text-decoration: underline; +} + +@media (prefers-reduced-motion: reduce) { + .sponsor-showcase-popover { + transition: none; + } +} + +/* Empty state */ +.sponsor-showcase-empty { + max-width: 720px; + margin: 0 auto; + padding: var(--spacing-lg); + text-align: center; +} + +.sponsor-showcase-empty .grid { + margin: var(--spacing-md) 0 var(--spacing-lg); +} + /* =================================== Venue Page Specific Styles =================================== */ @@ -3200,4 +3660,92 @@ img { top: var(--spacing-xs); right: var(--spacing-xs); } -} \ No newline at end of file +} +/* =================================== + Sponsor marquee (home) + =================================== */ +.sponsor-marquee { + overflow: hidden; + width: 100%; + padding: var(--spacing-sm) 0; + -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); + mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); +} + +.sponsor-marquee-track { + display: flex; + width: max-content; + animation: sponsor-marquee 40s linear infinite; +} + +.sponsor-marquee:hover .sponsor-marquee-track { + animation-play-state: paused; +} + +.sponsor-marquee-group { + display: flex; + align-items: center; + gap: var(--spacing-lg); + padding-right: var(--spacing-lg); + flex-shrink: 0; +} + +.sponsor-marquee-logo { + height: 72px; + width: 150px; + background: #ffffff; + border-radius: 12px; + padding: 10px; + border: 2px solid var(--color-border); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.sponsor-marquee-logo img { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + object-fit: contain; +} + +@keyframes sponsor-marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } +} + + +@media (prefers-reduced-motion: reduce) { + .sponsor-marquee-track { + animation: none; + flex-wrap: wrap; + justify-content: center; + width: 100%; + } + + .sponsor-marquee-group[aria-hidden="true"] { + display: none; + } + + .sponsor-marquee { + mask-image: none; + -webkit-mask-image: none; + } +} + +@media (max-width: 768px) { + .sponsor-marquee-logo { + height: 56px; + width: 120px; + } + + .sponsor-marquee-group { + gap: var(--spacing-md); + } +} diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 21791a6..c028250 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -7,6 +7,7 @@ import useScrollAnimation from '../hooks/useScrollAnimation'; import LazyImage from '../components/LazyImage'; import TrackSection from '../components/TrackSection'; import KeynoteCard from '../components/KeynoteCard'; +import SponsorMarquee from '../components/SponsorMarquee'; import { sponsorshipDeckUrl } from '../data/sponsors'; import { speakers } from '../data/speakers'; import { keynoteSessions } from '../data/agenda'; @@ -117,6 +118,9 @@ const Home = () => { )} + {/* Sponsors marquee */} + + {/* Features Section */}
diff --git a/src/pages/Sponsors.jsx b/src/pages/Sponsors.jsx new file mode 100644 index 0000000..d5e2bab --- /dev/null +++ b/src/pages/Sponsors.jsx @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; +import useScrollAnimation from '../hooks/useScrollAnimation'; +import SponsorShowcase from '../components/SponsorShowcase'; + +const Sponsors = () => { + const { t } = useTranslation(); + useScrollAnimation(); + + return ( + <> + {/* Page Header */} +
+
+

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

+

{t('sponsors.subtitle')}

+
+
+ + + + ); +}; + +export default Sponsors;