diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index 4ca29f09..8e37525e 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -47,6 +47,17 @@ jobs: run: npm run test:visual continue-on-error: true + - name: Publish to Chromatic + if: env.CHROMATIC_PROJECT_TOKEN != '' + uses: chromaui/action@latest + with: + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + workingDir: frontend + buildScriptName: build-storybook + onlyChanged: true + exitZeroOnChanges: true + continue-on-error: true + - name: Upload test report if: always() uses: actions/upload-artifact@v4 diff --git a/frontend/index.html b/frontend/index.html index 22eb7d03..9cb6ced6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -11,10 +11,6 @@ - -
- + + + diff --git a/frontend/src/components/ClaimButton.jsx b/frontend/src/components/ClaimButton.jsx new file mode 100644 index 00000000..ee67a956 --- /dev/null +++ b/frontend/src/components/ClaimButton.jsx @@ -0,0 +1,58 @@ +import React from 'react'; + +export function ClaimButton({ + status = 'ready', // 'ready' | 'loading' | 'claimed' | 'disabled' + points = 100, + onClick, +}) { + const isClaimed = status === 'claimed'; + const isLoading = status === 'loading'; + const isDisabled = status === 'disabled' || isClaimed || isLoading; + + return ( + + ); +} + +export default ClaimButton; diff --git a/frontend/src/components/RewardBadge.jsx b/frontend/src/components/RewardBadge.jsx new file mode 100644 index 00000000..a9bf76be --- /dev/null +++ b/frontend/src/components/RewardBadge.jsx @@ -0,0 +1,54 @@ +import React from 'react'; + +export function RewardBadge({ points = 50, label = 'pts', variant = 'default', size = 'medium' }) { + const sizeStyles = { + small: { fontSize: '0.75rem', padding: '2px 8px' }, + medium: { fontSize: '0.875rem', padding: '4px 10px' }, + large: { fontSize: '1rem', padding: '6px 14px' }, + }; + + const variantStyles = { + default: { + background: 'rgba(76, 141, 255, 0.16)', + color: '#4c8dff', + border: '1px solid rgba(76, 141, 255, 0.32)', + }, + success: { + background: 'rgba(51, 195, 127, 0.16)', + color: '#33c37f', + border: '1px solid rgba(51, 195, 127, 0.32)', + }, + warning: { + background: 'rgba(245, 158, 11, 0.16)', + color: '#f59e0b', + border: '1px solid rgba(245, 158, 11, 0.32)', + }, + claimed: { + background: 'rgba(161, 177, 199, 0.16)', + color: '#a1b1c7', + border: '1px solid rgba(161, 177, 199, 0.32)', + }, + }; + + return ( + + + + {points} {label} + + + ); +} + +export default RewardBadge; diff --git a/frontend/src/stories/CampaignCard.stories.jsx b/frontend/src/stories/CampaignCard.stories.jsx index ef5494eb..3663620e 100644 --- a/frontend/src/stories/CampaignCard.stories.jsx +++ b/frontend/src/stories/CampaignCard.stories.jsx @@ -26,14 +26,34 @@ const baseCampaign = { imageUrl: null, }; +export const Loading = { args: { campaign: null, loading: true } }; + export const Active = { args: { campaign: baseCampaign } }; -export const Featured = { - args: { campaign: { ...baseCampaign, featured: true, name: 'Featured Airdrop Campaign' } }, +export const Ended = { + args: { + campaign: { + ...baseCampaign, + active: false, + status: 'ended', + name: 'Ended Campaign', + }, + }, +}; + +export const Claimed = { + args: { + campaign: { + ...baseCampaign, + active: false, + status: 'claimed', + name: 'Claimed Rewards Campaign', + }, + }, }; -export const Inactive = { - args: { campaign: { ...baseCampaign, active: false, status: 'ended', name: 'Ended Campaign' } }, +export const Featured = { + args: { campaign: { ...baseCampaign, featured: true, name: 'Featured Airdrop Campaign' } }, }; export const Upcoming = { @@ -56,8 +76,6 @@ export const WithImage = { }, }; -export const LoadingSkeleton = { args: { campaign: baseCampaign, loading: true } }; - export const DarkMode = { args: { campaign: { ...baseCampaign, featured: true } }, parameters: { backgrounds: { default: 'dark' } }, diff --git a/frontend/src/stories/ClaimButton.stories.jsx b/frontend/src/stories/ClaimButton.stories.jsx new file mode 100644 index 00000000..a78c36c6 --- /dev/null +++ b/frontend/src/stories/ClaimButton.stories.jsx @@ -0,0 +1,30 @@ +import ClaimButton from '../components/ClaimButton.jsx'; + +export default { + title: 'Components/ClaimButton', + component: ClaimButton, + tags: ['autodocs'], + argTypes: { + status: { + control: 'select', + options: ['ready', 'loading', 'claimed', 'disabled'], + }, + points: { control: 'number' }, + }, +}; + +export const ReadyToClaim = { + args: { status: 'ready', points: 150 }, +}; + +export const Loading = { + args: { status: 'loading', points: 150 }, +}; + +export const Claimed = { + args: { status: 'claimed', points: 150 }, +}; + +export const Disabled = { + args: { status: 'disabled', points: 0 }, +}; diff --git a/frontend/src/stories/RewardBadge.stories.jsx b/frontend/src/stories/RewardBadge.stories.jsx new file mode 100644 index 00000000..c11f16df --- /dev/null +++ b/frontend/src/stories/RewardBadge.stories.jsx @@ -0,0 +1,37 @@ +import RewardBadge from '../components/RewardBadge.jsx'; + +export default { + title: 'Components/RewardBadge', + component: RewardBadge, + tags: ['autodocs'], + argTypes: { + points: { control: 'number' }, + label: { control: 'text' }, + variant: { control: 'select', options: ['default', 'success', 'warning', 'claimed'] }, + size: { control: 'select', options: ['small', 'medium', 'large'] }, + }, +}; + +export const Default = { + args: { points: 50, label: 'pts', variant: 'default', size: 'medium' }, +}; + +export const Success = { + args: { points: 100, label: 'pts', variant: 'success', size: 'medium' }, +}; + +export const Warning = { + args: { points: 25, label: 'pts', variant: 'warning', size: 'medium' }, +}; + +export const Claimed = { + args: { points: 200, label: 'pts', variant: 'claimed', size: 'medium' }, +}; + +export const Small = { + args: { points: 10, label: 'pts', variant: 'default', size: 'small' }, +}; + +export const Large = { + args: { points: 500, label: 'pts', variant: 'success', size: 'large' }, +}; diff --git a/frontend/tests/visual/pages.spec.ts b/frontend/tests/visual/pages.spec.ts index b245af09..2938ae79 100644 --- a/frontend/tests/visual/pages.spec.ts +++ b/frontend/tests/visual/pages.spec.ts @@ -146,4 +146,32 @@ test.describe('Primary Screens Visual Regression', () => { maxDiffPixelRatio: 0.02, }); }); + + test('Initial load in dark mode - no flash', async ({ page }) => { + // Set stored theme to dark prior to navigation + await page.addInitScript(() => { + window.localStorage.setItem('trivela-theme', 'dark'); + }); + await page.goto(`${APP_URL}/`); + await page.waitForLoadState('domcontentloaded'); + + // Verify document has dark theme applied before or right at render + const themeApplied = await page.evaluate(() => { + const el = document.documentElement; + return ( + el.dataset.theme === 'dark' || + el.classList.contains('dark') || + el.style.colorScheme === 'dark' + ); + }); + expect(themeApplied).toBeTruthy(); + + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(300); + await expect(page).toHaveScreenshot('initial-dark-mode.png', { + fullPage: true, + animations: 'disabled', + maxDiffPixelRatio: 0.02, + }); + }); });