diff --git a/frontend/src/components/LoadingSpinner.css b/frontend/src/components/LoadingSpinner.css
index 13fe9829..c3faf8c8 100644
--- a/frontend/src/components/LoadingSpinner.css
+++ b/frontend/src/components/LoadingSpinner.css
@@ -35,17 +35,4 @@
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
-}
-
-/* Visually hidden text for screen readers */
-.visually-hidden {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
}
\ No newline at end of file
diff --git a/frontend/src/components/Skeleton.css b/frontend/src/components/Skeleton.css
index 4223577b..7f1c02a6 100644
--- a/frontend/src/components/Skeleton.css
+++ b/frontend/src/components/Skeleton.css
@@ -26,17 +26,4 @@
100% {
background-position: 200% 0;
}
-}
-
-/* Visually hidden text for screen readers */
-.visually-hidden {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
}
\ No newline at end of file
diff --git a/frontend/src/components/__tests__/LandingPage.dataDriven.test.tsx b/frontend/src/components/__tests__/LandingPage.dataDriven.test.tsx
new file mode 100644
index 00000000..cf48206e
--- /dev/null
+++ b/frontend/src/components/__tests__/LandingPage.dataDriven.test.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import LandingPage from '../LandingPage';
+import { api } from '../../lib/api/client';
+
+describe('LandingPage data-driven sections', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(api, 'getStatistics')
+ .mockResolvedValue({ totalMarkets: 128, totalVolume: 45000, activeUsers: 512 });
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('renders one feature-card article per feature entry with translated copy', () => {
+ render(
);
+ const cards = document.querySelectorAll('.feature-card');
+ expect(cards).toHaveLength(3);
+ expect(screen.getByRole('heading', { name: 'Fully Decentralized' })).toBeInTheDocument();
+ expect(screen.getByRole('heading', { name: 'Secure & Audited' })).toBeInTheDocument();
+ expect(screen.getByRole('heading', { name: 'Lightning Fast' })).toBeInTheDocument();
+ });
+
+ it('renders the how-it-works steps as an ordered list with one item per step', () => {
+ render(
);
+ const steps = document.querySelectorAll('.steps-list > li');
+ expect(steps).toHaveLength(4);
+ expect(screen.getByRole('heading', { name: 'Create a Market' })).toBeInTheDocument();
+ expect(screen.getByRole('heading', { name: 'Claim Winnings' })).toBeInTheDocument();
+ });
+
+ it('renders footer columns, including link lists, from the data array', () => {
+ render(
);
+ const columns = document.querySelectorAll('.footer-section');
+ expect(columns).toHaveLength(3);
+
+ expect(screen.getByRole('link', { name: 'Documentation' })).toHaveAttribute('href', '/docs');
+ expect(screen.getByRole('link', { name: 'GitHub' })).toHaveAttribute('href', '/github');
+ expect(screen.getByRole('link', { name: 'Discord' })).toHaveAttribute('href', '/discord');
+ expect(screen.getByRole('link', { name: 'Privacy Policy' })).toHaveAttribute('href', '/privacy');
+ expect(screen.getByRole('link', { name: 'Terms of Service' })).toHaveAttribute('href', '/terms');
+ });
+});
diff --git a/frontend/src/components/landing/FeatureCard.tsx b/frontend/src/components/landing/FeatureCard.tsx
new file mode 100644
index 00000000..33849519
--- /dev/null
+++ b/frontend/src/components/landing/FeatureCard.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+export interface FeatureCardProps {
+ icon: string;
+ title: string;
+ description: string;
+}
+
+export const FeatureCard: React.FC
= ({ icon, title, description }) => (
+
+
+ {title}
+ {description}
+
+);
diff --git a/frontend/src/components/landing/FooterColumn.tsx b/frontend/src/components/landing/FooterColumn.tsx
new file mode 100644
index 00000000..1b726c62
--- /dev/null
+++ b/frontend/src/components/landing/FooterColumn.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+
+export interface FooterColumnLink {
+ href: string;
+ label: string;
+}
+
+export interface FooterColumnProps {
+ heading: string;
+ headingLevel?: 'h2' | 'h3';
+ tagline?: string;
+ links?: FooterColumnLink[];
+}
+
+export const FooterColumn: React.FC = ({
+ heading,
+ headingLevel = 'h3',
+ tagline,
+ links,
+}) => {
+ const Heading = headingLevel;
+ return (
+
+
{heading}
+ {tagline &&
{tagline}
}
+ {links && (
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/landing/Step.tsx b/frontend/src/components/landing/Step.tsx
new file mode 100644
index 00000000..33e009b0
--- /dev/null
+++ b/frontend/src/components/landing/Step.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+
+export interface StepProps {
+ title: string;
+ description: string;
+}
+
+export const Step: React.FC = ({ title, description }) => (
+
+ {title}
+ {description}
+
+);
diff --git a/frontend/src/lib/__tests__/i18n.test.ts b/frontend/src/lib/__tests__/i18n.test.ts
index 8f0776b8..c594f16b 100644
--- a/frontend/src/lib/__tests__/i18n.test.ts
+++ b/frontend/src/lib/__tests__/i18n.test.ts
@@ -44,6 +44,16 @@ describe('i18n', () => {
const result = i18n.t('features.decentralized.title');
expect(result).toBe('Fully Decentralized');
});
+
+ it('should return default value when a key resolves to a nested object rather than a string', () => {
+ const result = i18n.t('features', 'default');
+ expect(result).toBe('default');
+ });
+
+ it('should return the key when a key resolves to a nested object and no default is provided', () => {
+ const result = i18n.t('features');
+ expect(result).toBe('features');
+ });
});
describe('loadLocaleFromStorage', () => {
diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts
index 92eefea6..99b463f4 100644
--- a/frontend/src/lib/i18n.ts
+++ b/frontend/src/lib/i18n.ts
@@ -114,7 +114,7 @@ class I18n {
t(key: string, defaultValue?: string): string {
const keys = key.split('.');
- let value: any = translations[this.currentLocale];
+ let value: Translations | string = translations[this.currentLocale];
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
diff --git a/frontend/src/styles/__tests__/accessibility.css.test.ts b/frontend/src/styles/__tests__/accessibility.css.test.ts
new file mode 100644
index 00000000..a2f66550
--- /dev/null
+++ b/frontend/src/styles/__tests__/accessibility.css.test.ts
@@ -0,0 +1,25 @@
+import fs from 'fs';
+import path from 'path';
+
+const SRC_DIR = path.resolve(__dirname, '../../');
+
+function findCssFiles(dir: string): string[] {
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ return findCssFiles(fullPath);
+ }
+ return entry.name.endsWith('.css') ? [fullPath] : [];
+ });
+}
+
+describe('.visually-hidden utility class', () => {
+ it('is defined exactly once across the stylesheet tree', () => {
+ const cssFiles = findCssFiles(SRC_DIR);
+ const definitions = cssFiles.filter((file) =>
+ fs.readFileSync(file, 'utf-8').match(/\.visually-hidden\s*\{/)
+ );
+
+ expect(definitions).toEqual([path.resolve(SRC_DIR, 'styles/accessibility.css')]);
+ });
+});