mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-15 02:15:01 +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.
74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
import { expect, within } from 'storybook/test';
|
|
|
|
import type { Sponsor } from '@/generated/types';
|
|
import SponsorCard from './SponsorCard';
|
|
|
|
const sponsor: Sponsor = {
|
|
id: 'acme-2026-10',
|
|
name: 'Acme VPS',
|
|
slots: ['dashboard', 'sidebar', '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.' },
|
|
link: 'https://acme.example/?utm_source=3x-ui',
|
|
};
|
|
|
|
const meta = {
|
|
title: 'Sponsor/SponsorCard',
|
|
component: SponsorCard,
|
|
tags: ['autodocs'],
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component:
|
|
'Paid sponsor placement fed by the project sponsors.json. Always labelled as a sponsor; links open in a new tab with rel="sponsored".',
|
|
},
|
|
},
|
|
},
|
|
argTypes: {
|
|
sponsor: { description: 'Sponsor entry from GET /sponsors.' },
|
|
variant: {
|
|
description: 'banner (dashboard), compact (sidebar/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 },
|
|
} satisfies Meta<typeof SponsorCard>;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
export const Banner: Story = {
|
|
args: { variant: 'banner', onClose: () => {} },
|
|
play: async ({ canvasElement }) => {
|
|
const canvas = within(canvasElement);
|
|
await expect(canvas.getByText('Sponsor')).toBeInTheDocument();
|
|
await expect(canvas.getByRole('link')).toHaveAttribute('rel', 'noopener noreferrer sponsored');
|
|
},
|
|
};
|
|
|
|
export const Compact: Story = {
|
|
args: { variant: 'compact', onClose: () => {} },
|
|
render: (args) => (
|
|
<div style={{ width: 204 }}>
|
|
<SponsorCard {...args} />
|
|
</div>
|
|
),
|
|
};
|
|
|
|
export const Card: Story = {
|
|
args: { variant: 'card' },
|
|
render: (args) => (
|
|
<div style={{ width: 320 }}>
|
|
<SponsorCard {...args} />
|
|
</div>
|
|
),
|
|
};
|
|
|
|
export const IconOnly: Story = {
|
|
args: { iconOnly: true },
|
|
};
|