feat(network): add responsive sponsor hierarchy
This commit is contained in:
@@ -201,10 +201,13 @@ export const netzwerkFields: Field[] = [
|
||||
text('detailLabel', 'Detail label', netzwerkContent.partners.detailLabel),
|
||||
{
|
||||
name: 'tiers',
|
||||
label: 'Tiers',
|
||||
label: 'Tier headings and supporting copy',
|
||||
type: 'array',
|
||||
dbName: 'network_tiers',
|
||||
defaultValue: netzwerkContent.partners.tiers,
|
||||
admin: {
|
||||
description: 'One copy row per sponsor tier. Partner membership and order come from each Partner record.',
|
||||
},
|
||||
fields: [numberField('tier', 'Tier'), text('label', 'Label'), text('note', 'Note')],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
|
||||
|
||||
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
|
||||
|
||||
const previousDescription =
|
||||
'Partner in drei Kategorien: Hauptsponsoren, Medienpartner und weitere Sponsoren. Klicken für Details.'
|
||||
const interimDescription =
|
||||
'Ein starkes Netzwerk aus Unternehmen und Medien begleitet den Bayerischen Mittelstandspreis. Klicken für Details.'
|
||||
const sponsorDescription =
|
||||
'Ein starkes Netzwerk aus Unternehmen und Medien begleitet den Bayerischen Mittelstandspreis. Die gestaffelten Bänder machen die Gewichtung auf jeder Bildschirmgröße klar.'
|
||||
|
||||
const tierCopy = [
|
||||
{ tier: 1, previousLabel: 'Hauptsponsoren', label: 'Große Darstellung' },
|
||||
{ tier: 2, previousLabel: 'Medienpartner', label: 'Mittlere Darstellung' },
|
||||
{ tier: 3, previousLabel: 'Weitere Sponsoren', label: 'Kompakte Darstellung' },
|
||||
] as const
|
||||
|
||||
async function updateTierLabels(
|
||||
db: MigrateUpArgs['db'],
|
||||
tableName: 'network_tiers' | '_network_tiers_v',
|
||||
direction: 'up' | 'down',
|
||||
) {
|
||||
for (const copy of tierCopy) {
|
||||
const from = direction === 'up' ? copy.previousLabel : copy.label
|
||||
const to = direction === 'up' ? copy.label : copy.previousLabel
|
||||
await db.run(
|
||||
sql.raw(
|
||||
`UPDATE ${tableName} SET label = ${literal(to)} WHERE tier = ${copy.tier} AND label = ${literal(from)};`,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
|
||||
await db.run(
|
||||
sql.raw(
|
||||
`UPDATE pages SET netzwerk_partners_description = ${literal(sponsorDescription)} WHERE netzwerk_partners_description IN (${literal(previousDescription)}, ${literal(interimDescription)});`,
|
||||
),
|
||||
)
|
||||
await db.run(
|
||||
sql.raw(
|
||||
`UPDATE _pages_v SET version_netzwerk_partners_description = ${literal(sponsorDescription)} WHERE version_netzwerk_partners_description IN (${literal(previousDescription)}, ${literal(interimDescription)});`,
|
||||
),
|
||||
)
|
||||
await updateTierLabels(db, 'network_tiers', 'up')
|
||||
await updateTierLabels(db, '_network_tiers_v', 'up')
|
||||
await db.run(sql.raw(`UPDATE network_tiers SET note = '' WHERE tier = 1 AND label = 'Große Darstellung' AND note = 'Premium';`))
|
||||
await db.run(sql.raw(`UPDATE _network_tiers_v SET note = '' WHERE tier = 1 AND label = 'Große Darstellung' AND note = 'Premium';`))
|
||||
}
|
||||
|
||||
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
|
||||
await db.run(
|
||||
sql.raw(
|
||||
`UPDATE pages SET netzwerk_partners_description = ${literal(previousDescription)} WHERE netzwerk_partners_description = ${literal(sponsorDescription)};`,
|
||||
),
|
||||
)
|
||||
await db.run(
|
||||
sql.raw(
|
||||
`UPDATE _pages_v SET version_netzwerk_partners_description = ${literal(previousDescription)} WHERE version_netzwerk_partners_description = ${literal(sponsorDescription)};`,
|
||||
),
|
||||
)
|
||||
await updateTierLabels(db, 'network_tiers', 'down')
|
||||
await updateTierLabels(db, '_network_tiers_v', 'down')
|
||||
await db.run(sql.raw(`UPDATE network_tiers SET note = 'Premium' WHERE tier = 1 AND label = 'Hauptsponsoren' AND note = '';`))
|
||||
await db.run(sql.raw(`UPDATE _network_tiers_v SET note = 'Premium' WHERE tier = 1 AND label = 'Hauptsponsoren' AND note = '';`))
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import * as migration_20260731_143000_remove_press_record_fallbacks from './2026
|
||||
import * as migration_20260731_160000_focused_payload_cleanup from './20260731_160000_focused_payload_cleanup';
|
||||
import * as migration_20260731_170000_netzwerk_greeting_video from './20260731_170000_netzwerk_greeting_video';
|
||||
import * as migration_20260731_180000_press_contact_image from './20260731_180000_press_contact_image';
|
||||
import * as migration_20260731_190000_responsive_sponsor_hierarchy from './20260731_190000_responsive_sponsor_hierarchy';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
@@ -126,4 +127,9 @@ export const migrations = [
|
||||
down: migration_20260731_180000_press_contact_image.down,
|
||||
name: '20260731_180000_press_contact_image',
|
||||
},
|
||||
{
|
||||
up: migration_20260731_190000_responsive_sponsor_hierarchy.up,
|
||||
down: migration_20260731_190000_responsive_sponsor_hierarchy.down,
|
||||
name: '20260731_190000_responsive_sponsor_hierarchy',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1337,6 +1337,9 @@ export interface Page {
|
||||
heading?: string | null;
|
||||
description?: string | null;
|
||||
detailLabel?: string | null;
|
||||
/**
|
||||
* One copy row per sponsor tier. Partner membership and order come from each Partner record.
|
||||
*/
|
||||
tiers?:
|
||||
| {
|
||||
tier?: number | null;
|
||||
|
||||
@@ -135,7 +135,7 @@ async function main() {
|
||||
const sponsoringImage = await mediaByFilename(payload, netzwerkContent.sponsoring.imageFilename)
|
||||
const { imageFilename: _heroFilename, ...heroContent } = netzwerkContent.hero
|
||||
const { imageFilename: _sponsoringFilename, ...sponsoringContent } = netzwerkContent.sponsoring
|
||||
const { items: _partnerItems, ...partnerSectionContent } = netzwerkContent.partners
|
||||
const { items: _partnerItems, tiers: partnerTiers, ...partnerSectionContent } = netzwerkContent.partners
|
||||
const existingGreetingVideo = page.netzwerk?.patronage?.greetingVideo || null
|
||||
const [patronPeople, juryMembers] = await Promise.all([
|
||||
itemsWithImageUploads(payload, netzwerkContent.patronage.people),
|
||||
@@ -169,6 +169,7 @@ async function main() {
|
||||
},
|
||||
partners: {
|
||||
...partnerSectionContent,
|
||||
tiers: partnerTiers.map(({ tier, label, note }) => ({ tier, label, note })),
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
|
||||
@@ -124,7 +124,7 @@ export const netzwerkContent = {
|
||||
partners: {
|
||||
eyebrow: 'Netzwerkqualität',
|
||||
heading: 'PARTNER & SPONSOREN',
|
||||
description: 'Ein starkes Netzwerk aus Unternehmen und Medien begleitet den Bayerischen Mittelstandspreis. Klicken für Details.',
|
||||
description: 'Ein starkes Netzwerk aus Unternehmen und Medien begleitet den Bayerischen Mittelstandspreis. Die gestaffelten Bänder machen die Gewichtung auf jeder Bildschirmgröße klar.',
|
||||
detailLabel: 'Details',
|
||||
tiers: [
|
||||
{ tier: 1, label: 'Große Darstellung', note: '' },
|
||||
|
||||
@@ -10,6 +10,15 @@ import { selectJuryDocs, sortJuryMembersQuoteFirst } from '@/spa/juryMembers';
|
||||
import { netzwerkContent } from '@/spa/netzwerkContent';
|
||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo';
|
||||
import {
|
||||
groupSponsorsByTier,
|
||||
sponsorCardBasis,
|
||||
sponsorColumns,
|
||||
sponsorTierNumbers,
|
||||
sponsorViewport,
|
||||
type SponsorTierNumber,
|
||||
type SponsorViewport,
|
||||
} from '@/spa/sponsorHierarchy';
|
||||
|
||||
const FF = '"IBM Plex Sans", sans-serif';
|
||||
const FB = '"Inter", sans-serif';
|
||||
@@ -51,17 +60,7 @@ type PartnerModalCopy = typeof netzwerkContent.partners.modal
|
||||
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||
const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
|
||||
const managedImage = (upload: unknown, fallback: unknown) => mediaUrl(upload, fallbackText(fallback, ''))
|
||||
const deprecatedPartnerDescription = 'Partner in drei Kategorien: Hauptsponsoren, Medienpartner und weitere Sponsoren. Klicken für Details.'
|
||||
const visiblePartnerDescription = (value: unknown) => {
|
||||
const text = fallbackText(value, netzwerkContent.partners.description)
|
||||
return text.trim() === deprecatedPartnerDescription ? netzwerkContent.partners.description : text
|
||||
}
|
||||
const juryImageFallback = '/images/preistraeger-placeholder.svg'
|
||||
const sortedPartners = <T extends PartnerLogoData>(partners: T[]) =>
|
||||
partners
|
||||
.map((partner, index) => ({ partner, index }))
|
||||
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
|
||||
.map(({ partner }) => partner)
|
||||
const normalizeJuryMember = (member: CmsRouteDoc): JuryMember => {
|
||||
const organization = fallbackText(member.organization, '')
|
||||
const rawRole = fallbackText(member.role, '')
|
||||
@@ -225,27 +224,46 @@ const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member
|
||||
|
||||
// ── SponsorCell (tiered) ──────────────────────────────────────────────────────
|
||||
|
||||
const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; detailLabel: string; onClick: () => void }> = ({ partner, variant, idx, cols, detailLabel, onClick }) => {
|
||||
const SponsorCell: React.FC<{
|
||||
partner: Partner
|
||||
variant: 'lg' | 'md' | 'sm'
|
||||
columns: number
|
||||
detailLabel: string
|
||||
gap: number
|
||||
viewport: SponsorViewport
|
||||
onClick: () => void
|
||||
}> = ({ partner, variant, columns, detailLabel, gap, viewport, onClick }) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isMobileCell = useIsMobile();
|
||||
const lg = variant === 'lg';
|
||||
const sm = variant === 'sm';
|
||||
const lastCol = (idx % cols) === cols - 1;
|
||||
const logoHeight = isMobileCell ? (lg ? 76 : sm ? 54 : 62) : lg ? 92 : sm ? 62 : 76;
|
||||
const mobileCell = viewport === 'mobile' || viewport === 'narrow-mobile';
|
||||
const logoHeight = mobileCell ? (lg ? 96 : sm ? 58 : 76) : lg ? 116 : sm ? 72 : 92;
|
||||
const basis = sponsorCardBasis(columns, gap);
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${detailLabel}: ${partner.name}`}
|
||||
data-testid={`sponsor-card-${partner.id}`}
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
padding: isMobileCell ? '26px 20px' : lg ? '44px 38px 40px' : sm ? '30px 28px' : '36px 32px',
|
||||
borderRight: !lastCol && !isMobileCell ? '1px solid rgba(3,9,58,0.08)' : 'none',
|
||||
borderBottom: '1px solid rgba(3,9,58,0.08)',
|
||||
appearance: 'none',
|
||||
boxSizing: 'border-box',
|
||||
width: basis,
|
||||
flex: `0 0 ${basis}`,
|
||||
minWidth: 0,
|
||||
padding: mobileCell ? (sm ? '22px 16px' : '30px 24px') : lg ? '48px 42px 44px' : sm ? '30px 26px' : '38px 34px',
|
||||
border: '1px solid rgba(3,9,58,0.1)',
|
||||
borderTop: `${lg ? 4 : sm ? 2 : 3}px solid ${GOLD}`,
|
||||
display: 'flex', flexDirection: 'column', gap: lg ? 18 : 16,
|
||||
cursor: 'pointer',
|
||||
background: hovered ? '#E8E5E4' : 'transparent',
|
||||
background: hovered ? '#E8E5E4' : 'rgba(255,255,255,0.92)',
|
||||
transition: 'background 0.15s',
|
||||
position: 'relative',
|
||||
textAlign: 'left',
|
||||
color: 'inherit',
|
||||
font: 'inherit',
|
||||
}}
|
||||
>
|
||||
<div style={{ height: logoHeight, display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', minWidth: 0 }}>
|
||||
@@ -257,7 +275,7 @@ const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx
|
||||
<div style={{ fontFamily: FF, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.12em', color: GOLD, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{detailLabel} <ArrowRight size={10} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -452,6 +470,8 @@ function PartnerModal({ partner, copy, onClose }: { partner: Partner; copy: Part
|
||||
|
||||
const Netzwerk: React.FC = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const isTabletOrMobile = useIsMobile(1200);
|
||||
const isNarrowMobile = useIsMobile(360);
|
||||
const [selectedPartner, setSelectedPartner] = useState<Partner | null>(null);
|
||||
const [showGreetingVideo, setShowGreetingVideo] = useState(false);
|
||||
const greetingVideoRef = useRef<HTMLVideoElement>(null);
|
||||
@@ -474,13 +494,20 @@ const Netzwerk: React.FC = () => {
|
||||
.filter((member) => member.active !== false && member.name);
|
||||
const juryMembers = sortJuryMembersQuoteFirst(normalizedJuryMembers);
|
||||
const expertiseRows = fallbackArray<ExpertiseRow>(expertise.rows, netzwerkContent.expertise.rows);
|
||||
const partners = sortedPartners(cmsPartners)
|
||||
const partners = cmsPartners
|
||||
.filter((partner) => partner.active !== false)
|
||||
.map((partner) => ({
|
||||
...partner,
|
||||
id: partner.stableId || partner.id,
|
||||
}));
|
||||
const partnerTiers = fallbackArray<PartnerTier>(partnerSection.tiers, netzwerkContent.partners.tiers);
|
||||
const configuredPartnerTiers = fallbackArray<PartnerTier>(partnerSection.tiers, netzwerkContent.partners.tiers);
|
||||
const partnerTiers = sponsorTierNumbers.map((tier) => ({
|
||||
...(netzwerkContent.partners.tiers.find((entry) => entry.tier === tier) as PartnerTier),
|
||||
...(configuredPartnerTiers.find((entry) => Number(entry.tier) === tier) || {}),
|
||||
tier,
|
||||
}));
|
||||
const sponsorsByTier = groupSponsorsByTier(partners);
|
||||
const sponsorLayoutViewport = sponsorViewport({ isMobile, isNarrowMobile, isTabletOrMobile });
|
||||
const mobileStats = fallbackArray<StatItem>(juryIntro.stats, netzwerkContent.juryIntro.stats);
|
||||
const desktopStats = fallbackArray<StatItem>(juryIntro.desktopStats, netzwerkContent.juryIntro.desktopStats);
|
||||
const sponsorBenefits = fallbackArray<BenefitItem>(sponsoring.benefits, netzwerkContent.sponsoring.benefits);
|
||||
@@ -741,23 +768,61 @@ const Netzwerk: React.FC = () => {
|
||||
<h2 className="type-section-title text-role-heading" style={{ textTransform: 'uppercase', margin: 0 }}><Lines text={fallbackText(partnerSection.heading, netzwerkContent.partners.heading)} /></h2>
|
||||
</div>
|
||||
<p className="type-body text-role-secondary" style={{ margin: 0 }}>
|
||||
{visiblePartnerDescription(partnerSection.description)}
|
||||
{fallbackText(partnerSection.description, netzwerkContent.partners.description)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tiered grid – continuous, ranks marked by label bars */}
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
{/* Independent tier bands keep rank legible without mixing tiers to fill a row. */}
|
||||
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '16px 20px 52px' : '24px 48px 80px' }}>
|
||||
{partnerTiers.map(t => {
|
||||
const tier = Number(t.tier) as 1 | 2 | 3;
|
||||
const tier = Number(t.tier) as SponsorTierNumber;
|
||||
const variant = tier === 1 ? 'lg' as const : tier === 2 ? 'md' as const : 'sm' as const;
|
||||
const cols = isMobile ? (tier === 1 ? 1 : 2) : (tier === 3 ? 4 : 2);
|
||||
const items = partners.filter(p => p.tier === tier);
|
||||
const columns = sponsorColumns(tier, sponsorLayoutViewport);
|
||||
const gap = isMobile ? 12 : tier === 3 ? 16 : 20;
|
||||
const items = sponsorsByTier[tier];
|
||||
const label = fallbackText(t.label, netzwerkContent.partners.tiers[tier - 1]?.label || '');
|
||||
const note = fallbackText(t.note, '');
|
||||
if (!items.length) return null;
|
||||
return (
|
||||
<div key={t.tier} style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)` }}>
|
||||
{items.map((p, i) => (
|
||||
<SponsorCell key={p.id} partner={p} variant={variant} idx={i} cols={cols} detailLabel={fallbackText(partnerSection.detailLabel, netzwerkContent.partners.detailLabel)} onClick={() => setSelectedPartner(p)} />
|
||||
))}
|
||||
</div>
|
||||
<section
|
||||
key={tier}
|
||||
data-testid={`sponsor-tier-${tier}`}
|
||||
data-tier={tier}
|
||||
data-columns={columns}
|
||||
data-viewport={sponsorLayoutViewport}
|
||||
style={{
|
||||
maxWidth: 1240,
|
||||
margin: '0 auto',
|
||||
padding: isMobile ? (tier === 1 ? '34px 0 14px' : '42px 0 14px') : (tier === 1 ? '50px 0 18px' : '62px 0 18px'),
|
||||
borderBottom: tier === 3 ? 'none' : '1px solid rgba(3,9,58,0.08)',
|
||||
}}
|
||||
>
|
||||
<header style={{ maxWidth: tier === 1 ? 720 : 620, margin: '0 auto', textAlign: 'center', padding: isMobile ? '0 8px 24px' : '0 16px 32px' }}>
|
||||
<h3 style={{ fontFamily: FF, fontSize: isMobile ? (tier === 1 ? 25 : 21) : (tier === 1 ? 34 : tier === 2 ? 28 : 23), fontWeight: 800, color: NAVY, textTransform: 'uppercase', letterSpacing: tier === 3 ? '0.08em' : '0.04em', lineHeight: 1.1, margin: 0 }}>
|
||||
{label}
|
||||
</h3>
|
||||
{note ? (
|
||||
<p style={{ fontFamily: FB, fontSize: isMobile ? 14 : 16, color: 'rgba(16,24,40,0.58)', lineHeight: 1.55, margin: '12px 0 0' }}>{note}</p>
|
||||
) : null}
|
||||
</header>
|
||||
<div
|
||||
data-testid={`sponsor-tier-${tier}-cards`}
|
||||
style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'stretch', gap }}
|
||||
>
|
||||
{items.map((partner) => (
|
||||
<SponsorCell
|
||||
key={partner.id}
|
||||
partner={partner}
|
||||
variant={variant}
|
||||
columns={columns}
|
||||
detailLabel={fallbackText(partnerSection.detailLabel, netzwerkContent.partners.detailLabel)}
|
||||
gap={gap}
|
||||
viewport={sponsorLayoutViewport}
|
||||
onClick={() => setSelectedPartner(partner)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
59
src/spa/sponsorHierarchy.ts
Normal file
59
src/spa/sponsorHierarchy.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export const sponsorTierNumbers = [1, 2, 3] as const
|
||||
|
||||
export type SponsorTierNumber = (typeof sponsorTierNumbers)[number]
|
||||
export type SponsorViewport = 'desktop' | 'tablet' | 'mobile' | 'narrow-mobile'
|
||||
|
||||
export type SponsorRecord = {
|
||||
sortOrder?: number | null
|
||||
tier: SponsorTierNumber
|
||||
}
|
||||
|
||||
export function groupSponsorsByTier<T extends SponsorRecord>(partners: T[]): Record<SponsorTierNumber, T[]> {
|
||||
const grouped: Record<SponsorTierNumber, Array<{ index: number; partner: T }>> = {
|
||||
1: [],
|
||||
2: [],
|
||||
3: [],
|
||||
}
|
||||
|
||||
partners.forEach((partner, index) => {
|
||||
if (sponsorTierNumbers.includes(partner.tier)) grouped[partner.tier].push({ index, partner })
|
||||
})
|
||||
|
||||
return {
|
||||
1: grouped[1]
|
||||
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
|
||||
.map(({ partner }) => partner),
|
||||
2: grouped[2]
|
||||
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
|
||||
.map(({ partner }) => partner),
|
||||
3: grouped[3]
|
||||
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
|
||||
.map(({ partner }) => partner),
|
||||
}
|
||||
}
|
||||
|
||||
export function sponsorColumns(tier: SponsorTierNumber, viewport: SponsorViewport): number {
|
||||
if (viewport === 'desktop') return tier === 1 ? 2 : tier === 2 ? 3 : 4
|
||||
if (viewport === 'tablet') return tier === 3 ? 3 : 2
|
||||
if (viewport === 'narrow-mobile') return 1
|
||||
return tier === 3 ? 2 : 1
|
||||
}
|
||||
|
||||
export function sponsorViewport({
|
||||
isMobile,
|
||||
isNarrowMobile,
|
||||
isTabletOrMobile,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
isNarrowMobile: boolean
|
||||
isTabletOrMobile: boolean
|
||||
}): SponsorViewport {
|
||||
if (isNarrowMobile) return 'narrow-mobile'
|
||||
if (isMobile) return 'mobile'
|
||||
if (isTabletOrMobile) return 'tablet'
|
||||
return 'desktop'
|
||||
}
|
||||
|
||||
export function sponsorCardBasis(columns: number, gap: number): string {
|
||||
return `calc((100% - ${(columns - 1) * gap}px) / ${columns})`
|
||||
}
|
||||
Reference in New Issue
Block a user