feat(sponsors): drop the sidebar slot and simplify the Sponsors page

The sidebar no longer carries a sponsor card. The backend stops serving
the "sidebar" slot, so a sponsors.json entry that still lists it keeps
only its other slots and one listing nothing else is dropped. Its
rotation, three-sponsor cap and collapsed-rail logo rendering go with it.

The Sponsors page now lists only real sponsors: the "Your brand here"
card shows only as the empty state instead of sitting in the grid and
repeating the header's Become a sponsor button, and the Available
placements section and its slot-occupancy helper are removed. Cards are
larger, two per row on wide screens. Unused i18n keys are dropped from
every locale.
This commit is contained in:
MHSanaei
2026-10-13 23:32:56 +02:00
parent 319e693276
commit 6f1dd0d757
25 changed files with 68 additions and 545 deletions
+24 -15
View File
@@ -184,28 +184,37 @@
.sponsor-card-card .sponsor-main {
flex-direction: column;
align-items: flex-start;
padding: 16px;
gap: 16px;
padding: 24px;
}
.sponsor-card-card .sponsor-logo {
width: 56px;
height: 56px;
width: 72px;
height: 72px;
border-radius: 12px;
}
.sponsor-card-card .sponsor-logo-fallback {
font-size: 28px;
}
.sponsor-card-card .sponsor-body {
gap: 6px;
}
.sponsor-card-card .sponsor-title {
font-size: 17px;
line-height: 1.4;
}
.sponsor-card-card .sponsor-text {
font-size: 14px;
line-height: 1.6;
}
.sponsor-card-card .sponsor-visit {
margin-top: auto;
}
.sponsor-card-icon {
justify-content: center;
padding: 6px;
border-color: transparent;
background: transparent;
}
.sponsor-card-icon .sponsor-logo {
width: 32px;
height: 32px;
font-size: 14px;
}
@media (max-width: 576px) {
@@ -7,7 +7,7 @@ import SponsorCard from './SponsorCard';
const sponsor: Sponsor = {
id: 'acme-2026-10',
name: 'Acme VPS',
slots: ['dashboard', 'sidebar', 'page', 'login'],
slots: ['dashboard', 'page', 'login'],
until: '2099-01-01T00:00:00Z',
title: { en: 'Acme VPS — fast NVMe servers', fa: 'سرورهای سریع Acme' },
text: { en: 'Deploy 3X-UI in 60 seconds. 20% off for panel users.' },
@@ -57,9 +57,8 @@ const meta = {
argTypes: {
sponsor: { description: 'Sponsor entry from GET /sponsors.' },
variant: {
description: 'banner (dashboard), compact (sidebar/login) or card (Sponsors page).',
description: 'banner (dashboard), compact (login) or card (Sponsors page).',
},
iconOnly: { description: 'Logo-only rendering for the collapsed sidebar rail.' },
onClose: { description: 'When set, shows a close button (temporary dismiss).' },
},
args: { sponsor },
@@ -81,7 +80,7 @@ export const Banner: Story = {
export const Compact: Story = {
args: { variant: 'compact', onClose: () => {} },
render: (args) => (
<div style={{ width: 204 }}>
<div style={{ width: 334 }}>
<SponsorCard {...args} />
</div>
),
@@ -108,23 +107,6 @@ export const BannerLongCopy: Story = {
},
};
export const SidebarLongCopy: Story = {
args: { variant: 'compact', sponsor: longSponsor, onClose: () => {} },
render: (args) => (
<div style={{ width: 204 }}>
<SponsorCard {...args} />
</div>
),
play: async ({ canvasElement }) => {
await expectNotClipped(canvasElement, '.sponsor-title');
await expectInsideCard(canvasElement, '.sponsor-text');
await expect(part(canvasElement, '.sponsor-main')).toHaveAttribute(
'title',
`${longSponsor.title.en}\n${longSponsor.text.en}`,
);
},
};
export const LoginLongCopy: Story = {
args: { variant: 'compact', sponsor: longSponsor, onClose: () => {} },
render: (args) => (
@@ -135,6 +117,10 @@ export const LoginLongCopy: Story = {
play: async ({ canvasElement }) => {
await expectNotClipped(canvasElement, '.sponsor-title');
await expectNotClipped(canvasElement, '.sponsor-text');
await expect(part(canvasElement, '.sponsor-main')).toHaveAttribute(
'title',
`${longSponsor.title.en}\n${longSponsor.text.en}`,
);
},
};
@@ -151,7 +137,3 @@ export const PageLongCopy: Story = {
await expectNotClipped(canvasElement, '.sponsor-title');
},
};
export const IconOnly: Story = {
args: { iconOnly: true },
};
@@ -12,7 +12,6 @@ export type SponsorCardVariant = 'banner' | 'compact' | 'card';
export interface SponsorCardProps {
sponsor: Sponsor;
variant?: SponsorCardVariant;
iconOnly?: boolean;
onClose?: () => void;
}
@@ -36,33 +35,13 @@ function SponsorLogo({ sponsor }: { sponsor: Sponsor }) {
);
}
export default function SponsorCard({
sponsor,
variant = 'banner',
iconOnly = false,
onClose,
}: SponsorCardProps) {
export default function SponsorCard({ sponsor, variant = 'banner', onClose }: SponsorCardProps) {
const { t, i18n } = useTranslation();
const lang = i18n.resolvedLanguage || i18n.language || 'en';
const title = pickLocale(sponsor.title, lang) || sponsor.name;
const text = pickLocale(sponsor.text, lang);
const tag = t('pages.sponsors.tag');
if (iconOnly) {
return (
<a
className="sponsor-card sponsor-card-icon"
href={sponsor.link}
target="_blank"
rel="noopener noreferrer sponsored"
title={`${tag} · ${title}`}
aria-label={`${tag}: ${title}`}
>
<SponsorLogo sponsor={sponsor} />
</a>
);
}
return (
<div className={`sponsor-card sponsor-card-${variant}`}>
<a
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useSponsorsQuery } from '@/api/queries/useSponsorsQuery';
import {
@@ -9,47 +9,29 @@ import {
} from '@/lib/sponsors';
import SponsorCard, { type SponsorCardVariant } from './SponsorCard';
const ROTATE_MS = 30_000;
interface SponsorSlotProps {
slot: Slot;
variant?: SponsorCardVariant;
iconOnly?: boolean;
rotate?: boolean;
className?: string;
}
export default function SponsorSlot({
slot,
variant = 'banner',
iconOnly,
rotate,
className,
}: SponsorSlotProps) {
export default function SponsorSlot({ slot, variant = 'banner', className }: SponsorSlotProps) {
const { data } = useSponsorsQuery();
const [, setDismissTick] = useState(0);
const [index, setIndex] = useState(0);
// Re-filtered each render so a close (dismissTick bump) re-reads localStorage.
const visible = sponsorsForSlot(data.sponsors, slot).filter(
(s) => !isSponsorDismissed(s.id, slot),
);
useEffect(() => {
if (!rotate || visible.length < 2) return;
const timer = window.setInterval(() => setIndex((i) => i + 1), ROTATE_MS);
return () => window.clearInterval(timer);
}, [rotate, visible.length]);
if (visible.length === 0) return null;
const sponsor = visible[(rotate ? index : 0) % visible.length];
const sponsor = visible[0];
return (
<div className={className}>
<SponsorCard
sponsor={sponsor}
variant={variant}
iconOnly={iconOnly}
onClose={() => {
dismissSponsor(sponsor.id, slot);
setDismissTick((n) => n + 1);