Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/visual-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script>
// Apply the saved (or OS-preferred) theme before first paint to avoid a flash of the wrong theme.
(function () {
Expand All @@ -28,9 +24,18 @@
: 'dark';
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
document.documentElement.classList.add(theme);
if (theme === 'dark') {
document.documentElement.style.backgroundColor = '#071018';
} else {
document.documentElement.style.backgroundColor = '#f3f6fb';
}
} catch (e) {}
})();
</script>
</head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@stellar/freighter-api@6.0.1/build/index.min.js"></script>
<script type="module" src="/src/main.jsx"></script>
</body>
Expand Down
58 changes: 58 additions & 0 deletions frontend/src/components/ClaimButton.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<button
type="button"
className={`btn btn-claim btn-claim--${status}`}
disabled={isDisabled}
onClick={onClick}
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
padding: '8px 16px',
borderRadius: '8px',
fontWeight: 600,
fontSize: '0.9rem',
cursor: isDisabled ? 'not-allowed' : 'pointer',
border: 'none',
background: isClaimed
? 'rgba(161, 177, 199, 0.2)'
: isLoading
? 'var(--accent-soft, #4c8dff33)'
: 'var(--accent, #4c8dff)',
color: isClaimed ? 'var(--text-muted, #a1b1c7)' : '#ffffff',
transition: 'all 0.2s ease',
}}
>
{isLoading ? (
<>
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
<span>Claiming...</span>
</>
) : isClaimed ? (
<>
<span aria-hidden="true">✓</span>
<span>Claimed</span>
</>
) : (
<>
<span aria-hidden="true">🎁</span>
<span>Claim {points > 0 ? `${points} pts` : 'Reward'}</span>
</>
)}
</button>
);
}

export default ClaimButton;
54 changes: 54 additions & 0 deletions frontend/src/components/RewardBadge.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<span
className={`reward-badge reward-badge--${variant}`}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
borderRadius: '9999px',
fontWeight: 600,
...sizeStyles[size],
...variantStyles[variant],
}}
>
<span aria-hidden="true">★</span>
<span>
{points} {label}
</span>
</span>
);
}

export default RewardBadge;
30 changes: 24 additions & 6 deletions frontend/src/stories/CampaignCard.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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' } },
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/stories/ClaimButton.stories.jsx
Original file line number Diff line number Diff line change
@@ -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 },
};
37 changes: 37 additions & 0 deletions frontend/src/stories/RewardBadge.stories.jsx
Original file line number Diff line number Diff line change
@@ -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' },
};
28 changes: 28 additions & 0 deletions frontend/tests/visual/pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
Loading