mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-08 14:15:00 +03:00
fd7b3559bc
Monthly sponsor placements need to change without cutting a panel release. Panels now read 3X/sponsors.json from the MHSanaei/sponsors repo (GitHub Pages on sponsors.sanaei.dev) and show active sponsors in four slots: an overview banner, a rotating sidebar card (max three), the login page and a new Sponsors page that also lists open placements. An entry shows only while enable is not false and until is in the future; links must be https and logos are png/webp/jpg by name only. The list is cached for an hour and the last good copy survives upstream failures; logos are proxied through /sponsors/logo/:name with failures cached, so CSP stays 'self' and admin browsers never reach a third party. Admins can hide a slot for 24h. Under XUI_DEBUG the panel reads a sibling ../sponsors/3X checkout so edits can be previewed before push.
61 lines
1.5 KiB
TypeScript
61 lines
1.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
import { useSponsorsQuery } from '@/api/queries/useSponsorsQuery';
|
|
import {
|
|
dismissSponsor,
|
|
isSponsorDismissed,
|
|
sponsorsForSlot,
|
|
type SponsorSlot as Slot,
|
|
} 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) {
|
|
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];
|
|
|
|
return (
|
|
<div className={className}>
|
|
<SponsorCard
|
|
sponsor={sponsor}
|
|
variant={variant}
|
|
iconOnly={iconOnly}
|
|
onClose={() => {
|
|
dismissSponsor(sponsor.id, slot);
|
|
setDismissTick((n) => n + 1);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|