diff --git a/public/feed.xml b/public/feed.xml index ab4d0bd..8b96570 100644 --- a/public/feed.xml +++ b/public/feed.xml @@ -5,7 +5,7 @@ https://usewraith.xyz/blog Notes on stealth payments, private infrastructure, and the Wraith ecosystem. en-us - Thu, 30 Jul 2026 09:40:05 GMT + Thu, 30 Jul 2026 11:32:01 GMT diff --git a/src/App.tsx b/src/App.tsx index 55f46a3..f7cf24a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,7 @@ const Privacy = lazy(() => import('./pages/Privacy')); const UseCases = lazy(() => import('./pages/UseCases')); const Stellar = lazy(() => import('./pages/Stellar')); const Roadmap = lazy(() => import('./pages/Roadmap')); +const Grants = lazy(() => import('./pages/Grants')); const CaseStudies = lazy(() => import('./pages/CaseStudies')); const Careers = lazy(() => import('./pages/Careers')); const About = lazy(() => import('./pages/About')); @@ -94,6 +95,14 @@ export default function App() { } /> + + + + } + /> { + it('renders the grants page for the /grants route', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + expect( + await screen.findByRole('heading', { level: 1, name: /build private payments/i }), + ).toBeInTheDocument(); + }); + + it('renders the current wave section with name and budget', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + const currentWave = waveData.currentWave; + if (currentWave) { + expect( + await screen.findByRole('heading', { level: 2, name: currentWave.name }), + ).toBeInTheDocument(); + expect(screen.getByText(currentWave.budget)).toBeInTheDocument(); + expect(screen.getAllByText(currentWave.fundingSource).length).toBeGreaterThanOrEqual(1); + } + }); + + it('renders the apply button with correct URL when wave is open', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + const currentWave = waveData.currentWave; + if (currentWave?.status === 'open') { + const applyLink = await screen.findByRole('link', { name: /apply on drips/i }); + expect(applyLink).toHaveAttribute('href', currentWave.applyUrl); + expect(applyLink).toHaveAttribute('target', '_blank'); + } + }); + + it('renders eligibility and review criteria lists', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + const currentWave = waveData.currentWave; + if (currentWave) { + await screen.findByRole('heading', { level: 3, name: /eligibility/i }); + expect( + screen.getByRole('heading', { level: 3, name: /review criteria/i }), + ).toBeInTheDocument(); + + for (const item of currentWave.eligibility) { + expect(screen.getByText(item)).toBeInTheDocument(); + } + + for (const item of currentWave.reviewCriteria) { + expect(screen.getByText(item)).toBeInTheDocument(); + } + } + }); + + it('renders past waves section when past waves exist', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + const pastWaves = waveData.pastWaves; + if (pastWaves.length > 0) { + expect(await screen.findByText(/past waves/i)).toBeInTheDocument(); + + for (const wave of pastWaves) { + expect(screen.getByRole('heading', { level: 2, name: wave.name })).toBeInTheDocument(); + expect(screen.getByText((content) => content.startsWith(wave.budget))).toBeInTheDocument(); + } + } + }); + + it('renders past wave recipients when present', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + const pastWaves = waveData.pastWaves; + if (pastWaves.length > 0) { + for (const wave of pastWaves) { + if (wave.recipients && wave.recipients.length > 0) { + await screen.findByText(/recipients/i); + for (const r of wave.recipients) { + expect(screen.getByText(r.name)).toBeInTheDocument(); + expect(screen.getByText(r.project)).toBeInTheDocument(); + expect(screen.getByText(r.grantAmount)).toBeInTheDocument(); + } + } + } + } + }); + + it('renders the FAQ section with all entries', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + expect( + await screen.findByRole('heading', { level: 2, name: /frequently asked questions/i }), + ).toBeInTheDocument(); + + const faqEntries = waveData.faq; + for (const entry of faqEntries) { + expect(screen.getByText(entry.question)).toBeInTheDocument(); + } + }); + + it('toggles FAQ answers open and closed', async () => { + window.history.replaceState({}, '', '/grants'); + const user = userEvent.setup(); + + render(); + + const faqEntries = waveData.faq; + if (faqEntries.length > 0) { + const firstQuestion = faqEntries[0]; + const toggleButton = await screen.findByRole('button', { + name: new RegExp(firstQuestion?.question ?? ''), + }); + + await user.click(toggleButton); + expect(screen.getByText(firstQuestion?.answer ?? '')).toBeInTheDocument(); + + await user.click(toggleButton); + expect(screen.queryByText(firstQuestion?.answer ?? '')).not.toBeInTheDocument(); + } + }); + + it('shows Drips as a link in the hero section', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + const dripsLinks = await screen.findAllByRole('link', { name: /drips/i }); + const dripsLink = dripsLinks.find( + (l) => l.getAttribute('href') === 'https://www.drips.network', + ); + expect(dripsLink).toBeDefined(); + expect(dripsLink).toHaveAttribute('target', '_blank'); + }); + + it('renders the layout header with brand link back home', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + const brandLink = await screen.findByRole('link', { name: /^wraith$/i }); + expect(brandLink).toHaveAttribute('href', 'https://usewraith.xyz'); + }); + + it('has no axe violations on the grants page', async () => { + const { axe } = await import('vitest-axe'); + window.history.replaceState({}, '', '/grants'); + + const { container } = render(); + + await screen.findByRole('heading', { level: 1, name: /build private payments/i }); + + const results = await axe(container); + + expect(results.violations).toEqual([]); + }); +}); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 4d4ed3a..c26c188 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -110,6 +110,12 @@ export default function Header() { > {t('header.nav.useCases')} + + {t('header.nav.grants')} + {t('header.nav.useCases')} + + {t('header.nav.grants')} + (null); + + return ( +
+ {/* Hero */} +
+ Grants +
+

+ Build private payments. Get funded. +

+

+ Wraith runs a grant program supported by{' '} + + Drips + {' '} + recurring revenue and external ecosystem funding. Each wave funds teams building stealth + address infrastructure, SDK integrations, and privacy-preserving payment tooling. +

+
+
+ + {/* Current wave */} + {currentWave && ( +
+ Current wave +
+
+
+

+ {currentWave.name} +

+

+ {currentWave.description} +

+
+
+ + Budget + + + {currentWave.budget} + + + {currentWave.fundingSource} + +
+
+ +
+ + {currentWave.status === 'open' ? 'Open for applications' : currentWave.status} + + + {currentWave.rewardRange} per grant + +
+ +
+

+ {currentWave.howToApply} +

+ + Apply on Drips + +
+
+ + {/* Eligibility */} +
+
+

+ Eligibility +

+
    + {currentWave.eligibility.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+

+ Review criteria +

+
    + {currentWave.reviewCriteria.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+
+ )} + + {/* Past waves */} + {pastWaves.length > 0 && ( +
+ Past waves +
+ {pastWaves.map((wave) => ( +
+
+
+

+ {wave.name} +

+ + {wave.budget} · {wave.fundingSource} + +
+ + Closed + +
+ + {wave.highlights && wave.highlights.length > 0 && ( +
    + {wave.highlights.map((highlight) => ( +
  • + + {highlight} +
  • + ))} +
+ )} + + {wave.recipients && wave.recipients.length > 0 && ( +
+ + Recipients + +
+ {wave.recipients.map((r) => ( +
+ {r.name} + {r.project} + + {r.grantAmount} + +
+ ))} +
+
+ )} +
+ ))} +
+
+ )} + + {/* FAQ */} + {faqEntries.length > 0 && ( +
+ FAQ +

+ Frequently asked questions +

+
+ {faqEntries.map((entry) => { + const isOpen = openFaqId === entry.id; + return ( +
+
+

+ +

+
+ {isOpen && ( +
+

+ {entry.answer} +

+
+ )} +
+ ); + })} +
{' '} +
+ )} +
+ ); +}