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 frontend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Public app URL used for canonical links and callback generation.
NEXT_PUBLIC_APP_URL=http://localhost:3000

# Backend API that powers payment intent + claim verification requests.
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000

# Chain/network selector used by the reference claim UI.
NEXT_PUBLIC_CRYPTO_NETWORK=stellar-testnet

# Human support destination shown on error fallback states.
NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com
52 changes: 52 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Bridgelet Frontend

Reference Next.js UI for initiating and claiming crypto payments.

## Tech stack

- Next.js 16 (App Router)
- TypeScript 5 in strict mode
- Tailwind CSS 4

## Local setup

1. Install dependencies:

```bash
cd frontend
npm install
```

2. Configure environment variables:

```bash
cp .env.example .env.local
```

3. Start development server:

```bash
npm run dev
```

4. Open [http://localhost:3000](http://localhost:3000).

## Routes

- `/` homepage placeholder
- `/send` sender flow placeholder
- `/claim/[token]` recipient claim placeholder

## Quality checks

- Type-check only:

```bash
npm run typecheck
```

- Production build:

```bash
npm run build
```
28 changes: 28 additions & 0 deletions frontend/app/claim/[token]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PageShell } from '@/components/page-shell';
import { publicEnv } from '@/lib/env';

type ClaimPageProps = {
params: Promise<{ token: string }>;
};

export default async function ClaimPage({ params }: ClaimPageProps) {
const { token } = await params;

return (
<PageShell
title="Recipient Claim Flow"
description="Placeholder flow for recipients to claim a crypto payment with a secure token."
>
<dl className="space-y-4 text-sm text-slate-700">
<div>
<dt className="font-medium text-slate-900">Claim Token</dt>
<dd className="break-all">{token}</dd>
</div>
<div>
<dt className="font-medium text-slate-900">Support Contact</dt>
<dd>{publicEnv.NEXT_PUBLIC_SUPPORT_EMAIL}</dd>
</div>
</dl>
</PageShell>
);
}
9 changes: 9 additions & 0 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import "tailwindcss";

:root {
color-scheme: light;
}

body {
@apply min-h-screen bg-slate-50 text-slate-900 antialiased;
}
20 changes: 20 additions & 0 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import './globals.css';

export const metadata: Metadata = {
title: 'Bridgelet Payments',
description: 'Reference UI for sending and claiming crypto payments.'
};

type RootLayoutProps = {
children: ReactNode;
};

export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
29 changes: 29 additions & 0 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Link from 'next/link';
import { PageShell } from '@/components/page-shell';

export default function HomePage() {
return (
<PageShell
title="Bridgelet Payment Flows"
description="Reference placeholder UI for sender and recipient claim experiences."
>
<div className="space-y-4">
<p className="text-slate-700">Select a flow to continue.</p>
<nav className="flex flex-col gap-3 sm:flex-row">
<Link
href="/send"
className="rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-700"
>
Open Sender Flow
</Link>
<Link
href="/claim/example-token"
className="rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-900 transition hover:bg-slate-100"
>
Open Claim Flow
</Link>
</nav>
</div>
</PageShell>
);
}
22 changes: 22 additions & 0 deletions frontend/app/send/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PageShell } from '@/components/page-shell';
import { publicEnv } from '@/lib/env';

export default function SendPage() {
return (
<PageShell
title="Sender Flow"
description="Placeholder sender journey for creating and funding a payment claim."
>
<dl className="space-y-4 text-sm text-slate-700">
<div>
<dt className="font-medium text-slate-900">API Base URL</dt>
<dd>{publicEnv.NEXT_PUBLIC_API_BASE_URL}</dd>
</div>
<div>
<dt className="font-medium text-slate-900">Network</dt>
<dd>{publicEnv.NEXT_PUBLIC_CRYPTO_NETWORK}</dd>
</div>
</dl>
</PageShell>
);
}
19 changes: 19 additions & 0 deletions frontend/components/page-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ReactNode } from 'react';

type PageShellProps = {
title: string;
description: string;
children?: ReactNode;
};

export function PageShell({ title, description, children }: PageShellProps) {
return (
<main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col gap-6 px-6 py-16">
<header className="space-y-2">
<h1 className="text-3xl font-semibold tracking-tight text-slate-950">{title}</h1>
<p className="text-base text-slate-600">{description}</p>
</header>
<section className="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">{children}</section>
</main>
);
}
21 changes: 21 additions & 0 deletions frontend/lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type PublicEnv = {
NEXT_PUBLIC_APP_URL: string;
NEXT_PUBLIC_API_BASE_URL: string;
NEXT_PUBLIC_CRYPTO_NETWORK: string;
NEXT_PUBLIC_SUPPORT_EMAIL: string;
};

function readPublicEnv(name: keyof PublicEnv): string {
const value = process.env[name];
if (!value || value.trim().length === 0) {
return 'not-configured';
}
return value;
}

export const publicEnv: PublicEnv = {
NEXT_PUBLIC_APP_URL: readPublicEnv('NEXT_PUBLIC_APP_URL'),
NEXT_PUBLIC_API_BASE_URL: readPublicEnv('NEXT_PUBLIC_API_BASE_URL'),
NEXT_PUBLIC_CRYPTO_NETWORK: readPublicEnv('NEXT_PUBLIC_CRYPTO_NETWORK'),
NEXT_PUBLIC_SUPPORT_EMAIL: readPublicEnv('NEXT_PUBLIC_SUPPORT_EMAIL')
};
4 changes: 4 additions & 0 deletions frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited.
6 changes: 6 additions & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

module.exports = nextConfig;
27 changes: 27 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "bridgelet-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=20.10.0"
}
}
7 changes: 7 additions & 0 deletions frontend/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {}
}
};

export default config;
Empty file added frontend/public/.gitkeep
Empty file.
33 changes: 33 additions & 0 deletions frontend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"types": ["node"],
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}