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
29 changes: 29 additions & 0 deletions app/api/landing/network-pathways/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Local content endpoint for the landing page's Network Pathways cards.
// Serves the exact `NetworkPathwayCard[]` contract the platform backend will
// expose, so the client layers (service -> hook -> component) talk to a
// real HTTP endpoint. Point NEXT_PUBLIC_API_URL at the backend to override it.
import { NextResponse } from 'next/server';
import type { NetworkPathwayCard } from '@/types/networkPathways';

const payload: NetworkPathwayCard[] = [
{
id: 'logistics-enterprises',
icon: 'enterprise',
title: 'Logistics Enterprises',
description:
'Run your entire fleet through one escrow-backed command center — dispatch, exceptions and settlement in a single operational view.',
cta: { label: 'Talk to our team', href: '/contact' },
},
{
id: 'independent-carriers',
icon: 'carrier',
title: 'Independent Carriers',
description:
'Pick up jobs, get paid the moment delivery is confirmed, and build an on-chain reputation that travels with you across the network.',
cta: { label: 'Join as a carrier', href: '/dashboard' },
},
];

export async function GET() {
return NextResponse.json(payload);
}
22 changes: 22 additions & 0 deletions app/api/mobile/escrow-summary/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Local content endpoint for the mobile Trustless Escrow Widget.
// Serves the exact `MobileEscrowSummary` contract the platform backend will
// expose, so the client layers (service -> hook -> component) talk to a
// real HTTP endpoint. Point NEXT_PUBLIC_API_URL at the backend to override it.
import { NextResponse } from 'next/server';
import type { MobileEscrowSummary } from '@/types/mobileEscrow';

const payload: MobileEscrowSummary = {
status: 'locked',
statusLabel: 'ESCROW LOCKED',
amount: 42500,
currency: 'USDC',
metrics: [
{ id: 'metric-signatures', label: 'Signatures', value: '2 / 3' },
{ id: 'metric-network', label: 'Network', value: 'Stellar Mainnet' },
{ id: 'metric-eta', label: 'Release ETA', value: '~4s after confirmation' },
],
};

export async function GET() {
return NextResponse.json(payload);
}
34 changes: 34 additions & 0 deletions app/api/mobile/features/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Local content endpoint for the mobile Kinetic Features Stack.
// Serves the exact `MobileFeatureCard[]` contract the platform backend will
// expose, so the client layers (service -> hook -> component) talk to a
// real HTTP endpoint. Point NEXT_PUBLIC_API_URL at the backend to override it.
import { NextResponse } from 'next/server';
import type { MobileFeatureCard } from '@/types/mobileFeatures';

const payload: MobileFeatureCard[] = [
{
id: 'spatial-anchoring',
icon: '📍',
title: 'Spatial Anchoring',
description:
'Every shipment is pinned to a live location trail, so pickup, transit and delivery are all verifiable against real coordinates.',
},
{
id: 'smart-contracts',
icon: '📜',
title: 'Smart Contracts',
description:
'Escrow terms are enforced by code, not a promise — funds release automatically the moment delivery conditions are met.',
},
{
id: 'immutable-audit',
icon: '🔒',
title: 'Immutable Audit',
description:
'Every status change and payout is written to the ledger permanently, giving disputes a record no one can quietly edit.',
},
];

export async function GET() {
return NextResponse.json(payload);
}
19 changes: 19 additions & 0 deletions app/api/mobile/hero-content/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Local content endpoint for the mobile hero section.
// Serves the exact `MobileHeroContent` contract the platform backend will
// expose, so the client layers (service -> hook -> component) talk to a
// real HTTP endpoint. Point NEXT_PUBLIC_API_URL at the backend to override it.
import { NextResponse } from 'next/server';
import type { MobileHeroContent } from '@/types/mobileHero';

const payload: MobileHeroContent = {
networkBadge: 'MAINNET V4.0 ACTIVE',
headline: 'Deliver Anything. Pay Only When It Arrives.',
subheadline:
'SwiftChain protects your deliveries using blockchain escrow — funds stay locked until delivery is confirmed.',
primaryCta: { label: 'Secure Your Shipment', href: '/dashboard' },
secondaryCta: { label: 'See How It Works', href: '#value-props' },
};

export async function GET() {
return NextResponse.json(payload);
}
130 changes: 130 additions & 0 deletions components/landing/NetworkPathways.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client';

import { useNetworkPathways } from '@/hooks/useNetworkPathways';
import type { NetworkPathwayIcon } from '@/types/networkPathways';

/**
* Custom thin-stroke line icons for the pathway cards. Kept as small,
* hand-drawn glyphs (not an icon library import) since each represents a
* distinct concept — a multi-bay warehouse for enterprises, a route line
* with a truck for independent carriers.
*/
function EnterpriseIcon() {
return (
<svg
viewBox="0 0 40 40"
fill="none"
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
className="h-8 w-8"
aria-hidden="true"
>
<path d="M6 34V14l8-6 8 6v20" />
<path d="M22 34V20l8-4 4 2v16" />
<path d="M11 34v-6h6v6" />
<path d="M18 22h0M18 27h0M11 22h0M11 27h0" strokeWidth="2.5" />
</svg>
);
}

function CarrierIcon() {
return (
<svg
viewBox="0 0 40 40"
fill="none"
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
className="h-8 w-8"
aria-hidden="true"
>
<path d="M4 26h16V15H4v11z" />
<path d="M20 19h6l6 5v2h-2" />
<circle cx="11" cy="30" r="3" />
<circle cx="27" cy="30" r="3" />
<path d="M14 30h10" />
</svg>
);
}

const ICONS: Record<NetworkPathwayIcon, () => React.JSX.Element> = {
enterprise: EnterpriseIcon,
carrier: CarrierIcon,
};

/**
* NetworkPathways — "Architected for Scale" split card layout showing the
* two ways an organization joins the network (Logistics Enterprises,
* Independent Carriers). Cards sit side by side from the md breakpoint
* (768px) up and stack vertically below it.
*/
export function NetworkPathways() {
const { cards, isLoading, error } = useNetworkPathways();

if (error) {
return (
<div
role="alert"
className="mx-auto max-w-2xl rounded-lg border border-red-500/30 bg-red-500/10 p-6 text-center text-sm text-red-400"
>
{error}
</div>
);
}

return (
<div className="mx-auto max-w-5xl px-6" aria-label="Network pathways">
<div className="text-center">
<p className="text-xs font-medium uppercase tracking-widest text-blue-400 sm:text-sm">
Architected for Scale
</p>
<h2 className="mt-3 text-balance text-3xl font-extrabold text-white sm:text-4xl">
Built for every kind of operator
</h2>
</div>

<div className="mt-10 grid grid-cols-1 gap-6 md:grid-cols-2">
{isLoading &&
Array.from({ length: 2 }).map((_, index) => (
<div
key={index}
className="h-56 animate-pulse rounded-2xl border border-white/10 bg-white/5"
/>
))}

{!isLoading &&
cards.map((card) => {
const Icon = ICONS[card.icon];
return (
<div
key={card.id}
className="group rounded-2xl border border-white/10 bg-white/5 p-8 backdrop-blur-sm transition hover:-translate-y-1 hover:border-blue-500/40 hover:bg-white/[0.08]"
>
<div className="inline-flex h-14 w-14 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-blue-400 transition group-hover:border-blue-500/40 group-hover:text-blue-300">
<Icon />
</div>

<h3 className="mt-5 text-xl font-semibold text-white">{card.title}</h3>
<p className="mt-3 text-sm leading-relaxed text-gray-300">
{card.description}
</p>

<a
href={card.cta.href}
className="mt-6 inline-flex items-center gap-1.5 text-sm font-semibold text-blue-400 transition group-hover:gap-2.5 group-hover:text-blue-300"
>
{card.cta.label}
<span aria-hidden="true">→</span>
</a>
</div>
);
})}
</div>
</div>
);
}

export default NetworkPathways;
97 changes: 97 additions & 0 deletions components/landing/__tests__/NetworkPathways.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { render, screen } from '@testing-library/react';
import { NetworkPathways } from '@/components/landing/NetworkPathways';
import { useNetworkPathways } from '@/hooks/useNetworkPathways';

jest.mock('@/hooks/useNetworkPathways');
const mockUseNetworkPathways = useNetworkPathways as jest.Mock;

const baseCards = [
{
id: 'logistics-enterprises',
icon: 'enterprise',
title: 'Logistics Enterprises',
description: 'Run your entire fleet through one escrow-backed command center.',
cta: { label: 'Talk to our team', href: '/contact' },
},
{
id: 'independent-carriers',
icon: 'carrier',
title: 'Independent Carriers',
description: 'Pick up jobs, get paid the moment delivery is confirmed.',
cta: { label: 'Join as a carrier', href: '/dashboard' },
},
];

describe('NetworkPathways', () => {
it('renders both pathway cards once loaded', () => {
mockUseNetworkPathways.mockReturnValue({
cards: baseCards,
isLoading: false,
error: null,
refetch: jest.fn(),
});

render(<NetworkPathways />);

expect(screen.getByText('Logistics Enterprises')).toBeInTheDocument();
expect(screen.getByText('Independent Carriers')).toBeInTheDocument();
expect(screen.getByRole('link', { name: /Talk to our team/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /Join as a carrier/i })).toBeInTheDocument();
});

it('stacks cards in a single column below md and two columns from md up', () => {
mockUseNetworkPathways.mockReturnValue({
cards: baseCards,
isLoading: false,
error: null,
refetch: jest.fn(),
});

const { container } = render(<NetworkPathways />);
const grid = container.querySelector('.grid');

expect(grid?.className).toContain('grid-cols-1');
expect(grid?.className).toContain('md:grid-cols-2');
});

it('applies a hover state class to each card', () => {
mockUseNetworkPathways.mockReturnValue({
cards: baseCards,
isLoading: false,
error: null,
refetch: jest.fn(),
});

render(<NetworkPathways />);

const card = screen.getByText('Logistics Enterprises').closest('div.group');
expect(card?.className).toContain('hover:');
});

it('shows a loading skeleton while data is being fetched', () => {
mockUseNetworkPathways.mockReturnValue({
cards: [],
isLoading: true,
error: null,
refetch: jest.fn(),
});

const { container } = render(<NetworkPathways />);

expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
expect(screen.queryByText('Logistics Enterprises')).not.toBeInTheDocument();
});

it('renders an error message when the fetch fails', () => {
mockUseNetworkPathways.mockReturnValue({
cards: [],
isLoading: false,
error: 'Failed to load network pathways',
refetch: jest.fn(),
});

render(<NetworkPathways />);

expect(screen.getByText('Failed to load network pathways')).toBeInTheDocument();
});
});
Loading
Loading