feat: add PartnerLogoMark component for dynamic partner logo rendering

This commit is contained in:
syntaxbullet
2026-07-01 11:39:45 +02:00
parent 1bc1f31940
commit 2fda7e8122
72 changed files with 99921 additions and 3421 deletions

View File

@@ -13,6 +13,8 @@ import type { SpaApplicationPhaseData, SpaApplicationPhaseItem } from '@/spa/app
import { useApplicationPhase, useCmsCollection, useCmsRoute } from '@/spa/cmsRoute'
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
import { homeContent } from '@/spa/homeContent'
import { netzwerkContent } from '@/spa/netzwerkContent'
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'
const FF = '"IBM Plex Sans", sans-serif';
@@ -84,6 +86,9 @@ type HomeCms = {
videoModal?: HomeVideoModalCms;
winners?: HomeSectionCms;
};
type NetzwerkPartnersCms = {
defaultLogoColor?: string | null;
};
type RenderedStatusPhase = Omit<SpaApplicationPhaseItem, 'accent' | 'bg' | 'cta' | 'membershipCta'> & {
accent: string;
bg: string;
@@ -121,16 +126,39 @@ function Lines({ text }: { text: string }) {
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
}
function partnerLogos(partners: unknown) {
return fallbackArray(partners, homeContent.hero.partners).map((partner) => {
const item = partner as { id?: string; name?: string; label?: React.ReactNode };
return item.label
? { id: item.id || String(item.name || ''), label: item.label }
: {
id: item.id || String(item.name || ''),
label: <span style={{ fontWeight: 700, fontSize: 10, letterSpacing: 1.5 }}>{item.name}</span>,
};
});
const isNetzwerkPage = (page: CmsRecord) =>
page.spaPath === '/netzwerk' || page.slug === 'netzwerk' || page.slug === 'network'
function partnerLogos(partners: PartnerLogoData[], defaultLogoColor: string) {
return partners.map((partner, index) => ({
id: String(partner.stableId || partner.id || partner.name || index),
label: (
<span
aria-label={fallbackText(partner.name, 'Partner')}
style={{
width: 224,
height: 54,
padding: '9px 18px',
background: 'rgba(255,255,255,0.92)',
border: '1px solid rgba(255,255,255,0.18)',
borderRadius: 6,
boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<PartnerLogoMark partner={partner} defaultLogoColor={defaultLogoColor} />
</span>
),
}));
}
function sortedPartners<T extends PartnerLogoData>(partners: T[]) {
return partners
.map((partner, index) => ({ partner, index }))
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
.map(({ partner }) => partner);
}
function HomeWinnerCard({
@@ -187,6 +215,20 @@ const Home: React.FC = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const isMobile = useIsMobile();
const home = (useCmsRoute()?.doc?.home || {}) as HomeCms;
const netzwerkPage = useCmsCollection('pages').find((page) => isNetzwerkPage(page as CmsRecord)) as
| ({ netzwerk?: { partners?: NetzwerkPartnersCms } } & CmsRecord)
| undefined;
const partnerSection = netzwerkPage?.netzwerk?.partners;
const defaultLogoColor = fallbackText(partnerSection?.defaultLogoColor, netzwerkContent.partners.defaultLogoColor);
const cmsPartners = useCmsCollection('partners') as PartnerLogoData[];
const partnerSource = cmsPartners.length ? cmsPartners : (netzwerkContent.partners.items as PartnerLogoData[]);
const networkPartners = sortedPartners(partnerSource)
.filter((partner) => partner.active !== false && partner.showOnHomepage !== false)
.map((partner) => ({
...partner,
id: partner.stableId || partner.id,
logoColor: fallbackText(partner.logoColor, defaultLogoColor),
}));
const hero = home.hero || {};
const quickCheck = home.quickCheck || {};
const intro = home.intro || {};
@@ -301,11 +343,11 @@ const Home: React.FC = () => {
<div className="px-8 py-5 flex items-center justify-between opacity-60 hover:opacity-100 transition-opacity duration-500">
{/* Left: label stays put */}
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 9, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.5)', whiteSpace: 'nowrap', flexShrink: 0 }}>{fallbackText(hero.partnersLabel, homeContent.hero.partnersLabel)}</span>
{/* Right: ticker fixed window, exactly 4 logos wide */}
{/* Right: ticker window */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexShrink: 0 }}>
<div style={{ width: 1, height: 20, background: 'rgba(255,255,255,0.2)' }} />
<div style={{ width: 560, overflow: 'hidden', flexShrink: 0 }}>
<PartnerTicker logos={partnerLogos(hero.partners)} />
<div style={{ width: 'min(820px, calc(100vw - 260px))', overflow: 'hidden', flexShrink: 0 }}>
<PartnerTicker logos={partnerLogos(networkPartners, defaultLogoColor)} />
</div>
</div>
</div>