From 7cc9988b3204d341e3a6ec66755c033ef2101f1f Mon Sep 17 00:00:00 2001 From: odarome132 Date: Mon, 27 Jul 2026 12:58:40 +0000 Subject: [PATCH 1/2] feat: add /grants page with wave data, FAQ, and tests --- src/App.tsx | 2 + src/__tests__/grants.test.tsx | 174 ++++++++++++++++++++++ src/components/Header.tsx | 13 ++ src/data/wave.json | 82 +++++++++++ src/i18n/en.json | 3 +- src/i18n/es.json | 3 +- src/pages/Grants.tsx | 262 ++++++++++++++++++++++++++++++++++ 7 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/grants.test.tsx create mode 100644 src/data/wave.json create mode 100644 src/pages/Grants.tsx diff --git a/src/App.tsx b/src/App.tsx index 11cbb9c..cc6c572 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,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 NotFound = lazy(() => import('./pages/NotFound')); @@ -69,6 +70,7 @@ export default function App() { } /> } /> } /> + } /> {/* Wrap Stellar with Layout */} { + 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('links back to the home page', async () => { + window.history.replaceState({}, '', '/grants'); + + render(); + + const backLinks = await screen.findAllByRole('link', { name: /back home/i }); + expect(backLinks.length).toBeGreaterThanOrEqual(1); + }); + + 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 2f8f05a..7ea17e3 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -132,6 +132,12 @@ export default function Header() { > {t('header.nav.useCases')} + + {t('header.nav.grants')} + {t('header.nav.useCases')} + + {t('header.nav.grants')} + (null); + + return ( +
+
+ + Wraith + + WRAITH PROTOCOL + + + + Back home + +
+ +
+ {/* 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} +

+
+ )} +
+ ); + })} +
+
+ )} +
+ +
+
+ ); +} From 6aba87ddb5baeddf980644df148f7ef059dec587 Mon Sep 17 00:00:00 2001 From: odarome132 Date: Thu, 30 Jul 2026 11:32:51 +0000 Subject: [PATCH 2/2] fix(grants): wire /grants route, fix Layout integration and a11y - Add missing /grants Route in App.tsx (Grants was imported but not wired) - Remove duplicate header/main/footer from Grants.tsx (Layout provides them) - Preserve container styling with wrapper div - Fix grants test to verify Layout header brand link - Resolve axe violations from duplicate landmarks --- public/feed.xml | 2 +- public/sitemap.xml | 14 +- src/App.tsx | 8 + src/__tests__/grants.test.tsx | 6 +- src/pages/Grants.tsx | 423 ++++++++++++++++------------------ 5 files changed, 220 insertions(+), 233 deletions(-) diff --git a/public/feed.xml b/public/feed.xml index e988933..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 - Mon, 27 Jul 2026 17:10:48 GMT + Thu, 30 Jul 2026 11:32:01 GMT diff --git a/public/sitemap.xml b/public/sitemap.xml index 4b90d66..6566f1c 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -2,43 +2,43 @@ https://usewraith.xyz - 2026-07-26 + 2026-07-30 daily 1.0 https://usewraith.xyz/blog - 2026-07-26 + 2026-07-30 daily 0.8 https://usewraith.xyz/compare - 2026-07-26 + 2026-07-30 daily 0.8 https://usewraith.xyz/faq - 2026-07-26 + 2026-07-30 daily 0.8 https://usewraith.xyz/privacy - 2026-07-26 + 2026-07-30 daily 0.8 https://usewraith.xyz/stellar - 2026-07-26 + 2026-07-30 daily 0.8 https://usewraith.xyz/use-cases - 2026-07-26 + 2026-07-30 daily 0.8 diff --git a/src/App.tsx b/src/App.tsx index 132a646..01e239e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -93,6 +93,14 @@ export default function App() { } /> + + + + } + /> { expect(dripsLink).toHaveAttribute('target', '_blank'); }); - it('links back to the home page', async () => { + it('renders the layout header with brand link back home', async () => { window.history.replaceState({}, '', '/grants'); render(); - const backLinks = await screen.findAllByRole('link', { name: /back home/i }); - expect(backLinks.length).toBeGreaterThanOrEqual(1); + 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 () => { diff --git a/src/pages/Grants.tsx b/src/pages/Grants.tsx index a98f37f..6131c96 100644 --- a/src/pages/Grants.tsx +++ b/src/pages/Grants.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import Footer from '../components/Footer'; import waveData from '../data/wave.json'; type Wave = (typeof waveData)['currentWave']; @@ -16,247 +15,227 @@ export default function Grants() { const [openFaqId, setOpenFaqId] = useState(null); return ( -
-
- - Wraith - - WRAITH PROTOCOL - - - - Back home - -
+
+ {/* 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. +

+
+
-
- {/* Hero */} -
- Grants -
-

- Build private payments. Get funded. -

-

- Wraith runs a grant program supported by{' '} + {/* 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} +

- Drips - {' '} - recurring revenue and external ecosystem funding. Each wave funds teams building - stealth address infrastructure, SDK integrations, and privacy-preserving payment - tooling. -

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

+ Eligibility +

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

+ Review criteria +

+
    + {currentWave.reviewCriteria.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+ )} - {/* Current wave */} - {currentWave && ( -
- Current wave -
-
-
-

- {currentWave.name} -

-

- {currentWave.description} -

-
-
- - Budget - - - {currentWave.budget} - - - {currentWave.fundingSource} + {/* Past waves */} + {pastWaves.length > 0 && ( +
+ Past waves +
+ {pastWaves.map((wave) => ( +
+
+
+

+ {wave.name} +

+ + {wave.budget} · {wave.fundingSource} + +
+ + Closed
-
-
- - {currentWave.status === 'open' ? 'Open for applications' : currentWave.status} - - - {currentWave.rewardRange} per grant - -
- -
-

- {currentWave.howToApply} -

- - Apply on Drips - -
-
+ {wave.highlights && wave.highlights.length > 0 && ( +
    + {wave.highlights.map((highlight) => ( +
  • + + {highlight} +
  • + ))} +
+ )} - {/* Eligibility */} -
-
-

- Eligibility -

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

- Review criteria -

-
    - {currentWave.reviewCriteria.map((item) => ( -
  • - - {item} -
  • - ))} -
+ {wave.recipients && wave.recipients.length > 0 && ( +
+ + Recipients + +
+ {wave.recipients.map((r) => ( +
+ {r.name} + {r.project} + + {r.grantAmount} + +
+ ))} +
+
+ )}
-
-
- )} + ))} +
+
+ )} - {/* Past waves */} - {pastWaves.length > 0 && ( -
- Past waves -
- {pastWaves.map((wave) => ( + {/* FAQ */} + {faqEntries.length > 0 && ( +
+ FAQ +

+ Frequently asked questions +

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

- {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} - -
- ))} -
+ {isOpen && ( +
+

+ {entry.answer} +

)}
- ))} -
-
- )} - - {/* FAQ */} - {faqEntries.length > 0 && ( -
- FAQ -

- Frequently asked questions -

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

- -

-
- {isOpen && ( -
-

- {entry.answer} -

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