Skip to content
172 changes: 22 additions & 150 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,158 +1,30 @@
import { Routes, Route, Navigate } from 'react-router-dom'
import { PublicHeader } from './components/PublicHeader'
import { PublicFooter } from './components/PublicFooter'
import { AppShell } from './components/AppShell'
import { ThemeToggle } from './components/ThemeToggle'
import { Link } from 'react-router-dom'

function PublicLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen flex flex-col">
<PublicHeader />
<main className="flex-1">{children}</main>
<PublicFooter />
</div>
)
}

function LandingDemo() {
return (
<PublicLayout>
<div className="max-w-4xl mx-auto px-4 py-20 text-center">
<h1 className="heading-1 mb-6">
Every Signature, <span className="text-primary">Forever Verifiable</span>
</h1>
<p className="text-xl text-muted mb-8 max-w-2xl mx-auto">
Blockchain-anchored e-signatures with independent verification. No vendor lock-in, no expiration.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<Link to="/register" className="btn-primary text-lg px-8 py-4">
Get Started Free
</Link>
<Link to="/verify" className="btn-secondary text-lg px-8 py-4">
Verify Document
</Link>
</div>

<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16">
<div className="card">
<h3 className="heading-3 mb-3">Upload</h3>
<p className="text-muted">Upload any PDF document to get started</p>
</div>
<div className="card">
<h3 className="heading-3 mb-3">Sign</h3>
<p className="text-muted">Add signers and collect e-signatures</p>
</div>
<div className="card">
<h3 className="heading-3 mb-3">Anchor</h3>
<p className="text-muted">Proof anchored on Ethereum L2</p>
</div>
</div>
</div>
</PublicLayout>
)
}

function DashboardDemo() {
return (
<AppShell title="Dashboard">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="card">
<p className="text-sm text-muted mb-2">Total Documents</p>
<p className="text-3xl font-bold">12</p>
</div>
<div className="card">
<p className="text-sm text-muted mb-2">Pending Signatures</p>
<p className="text-3xl font-bold text-warning">3</p>
</div>
<div className="card">
<p className="text-sm text-muted mb-2">Completed</p>
<p className="text-3xl font-bold text-accent">8</p>
</div>
<div className="card">
<p className="text-sm text-muted mb-2">Anchored On-Chain</p>
<p className="text-3xl font-bold text-primary">8</p>
</div>
</div>

<div className="card">
<h2 className="heading-2 mb-6">Recent Activity</h2>
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center justify-between p-4 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div>
<p className="font-semibold">NDA Agreement #{i}</p>
<p className="text-sm text-muted">Created 2 days ago</p>
</div>
<span className="badge badge-success">Completed</span>
</div>
))}
</div>
</div>
</div>
</AppShell>
)
}

function LayoutDemo() {
return (
<div className="min-h-screen p-8">
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="heading-1 mb-2">Layout Components Demo</h1>
<p className="text-muted">Step 2: Public and authenticated layouts</p>
</div>
<ThemeToggle />
</div>

<div className="space-y-6">
<section className="card">
<h2 className="heading-2 mb-4">Public Layout</h2>
<p className="text-muted mb-4">
Used for landing page, pricing, verify, login, and register pages.
Includes PublicHeader (sticky navigation) and PublicFooter (links & social).
</p>
<Link to="/demo/public" className="btn-primary">
View Public Layout →
</Link>
</section>

<section className="card">
<h2 className="heading-2 mb-4">Authenticated Layout</h2>
<p className="text-muted mb-4">
Used for dashboard, documents, envelopes, and settings.
Includes AppShell (flex layout), Sidebar (responsive navigation), and Topbar (header with menu).
</p>
<Link to="/demo/authenticated" className="btn-primary">
View Authenticated Layout →
</Link>
</section>

<section className="card">
<h2 className="heading-3 mb-4">Component Features</h2>
<ul className="space-y-2 text-muted">
<li>✓ Responsive sidebar (mobile drawer + desktop fixed)</li>
<li>✓ Sticky headers with backdrop blur</li>
<li>✓ Theme-aware colors and borders</li>
<li>✓ Active route highlighting in sidebar</li>
<li>✓ Accessible navigation with ARIA labels</li>
<li>✓ Smooth transitions and animations</li>
</ul>
</section>
</div>
</div>
</div>
)
}
import { LandingPage } from './pages/LandingPage'
import { PricingPage } from './pages/PricingPage'
import { VerifyPage } from './pages/VerifyPage'
import { LoginPage } from './pages/LoginPage'
import { RegisterPage } from './pages/RegisterPage'
import { ForgotPasswordPage } from './pages/ForgotPasswordPage'
import { DashboardPage } from './pages/DashboardPage'
import { ProtectedRoute } from './components/ProtectedRoute'

export default function App() {
return (
<Routes>
<Route path="/" element={<LayoutDemo />} />
<Route path="/demo/public" element={<LandingDemo />} />
<Route path="/demo/authenticated" element={<DashboardDemo />} />
<Route path="/" element={<LandingPage />} />
<Route path="/pricing" element={<PricingPage />} />
<Route path="/verify" element={<VerifyPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route
path="/app/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)
Expand Down
148 changes: 148 additions & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import axios, { AxiosInstance, AxiosError } from 'axios'
import { User, Document, Envelope, LoginRequest, RegisterRequest, AuthResponse } from './types'
import { mockUser, mockDocuments, mockEnvelopes } from './mockData'

class ApiClient {
private client: AxiosInstance
private useMock: boolean = true

constructor() {
this.client = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})

this.client.interceptors.request.use((config) => {
const token = localStorage.getItem('auth-token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})

this.client.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth-token')
localStorage.removeItem('refresh-token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
}

private delay(ms: number = 500): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}

async login(data: LoginRequest): Promise<AuthResponse> {
if (this.useMock) {
await this.delay()
if (data.email === 'demo@sigra.io' && data.password === 'password') {
return {
user: mockUser,
token: 'mock-jwt-token-' + Date.now(),
refreshToken: 'mock-refresh-token-' + Date.now(),
}
}
throw new Error('Invalid email or password')
}
const response = await this.client.post<AuthResponse>('/auth/login', data)
return response.data
}

async register(data: RegisterRequest): Promise<AuthResponse> {
if (this.useMock) {
await this.delay()
return {
user: {
...mockUser,
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
company: data.company,
},
token: 'mock-jwt-token-' + Date.now(),
refreshToken: 'mock-refresh-token-' + Date.now(),
}
}
const response = await this.client.post<AuthResponse>('/auth/register', data)
return response.data
}

async getCurrentUser(): Promise<User> {
if (this.useMock) {
await this.delay(300)
return mockUser
}
const response = await this.client.get<User>('/auth/me')
return response.data
}

async getDocuments(): Promise<Document[]> {
if (this.useMock) {
await this.delay()
return mockDocuments
}
const response = await this.client.get<Document[]>('/documents')
return response.data
}

async getDocument(id: string): Promise<Document> {
if (this.useMock) {
await this.delay()
const doc = mockDocuments.find(d => d.id === id)
if (!doc) throw new Error('Document not found')
return doc
}
const response = await this.client.get<Document>(`/documents/${id}`)
return response.data
}

async getEnvelopes(): Promise<Envelope[]> {
if (this.useMock) {
await this.delay()
return mockEnvelopes
}
const response = await this.client.get<Envelope[]>('/envelopes')
return response.data
}

async getEnvelope(id: string): Promise<Envelope> {
if (this.useMock) {
await this.delay()
const env = mockEnvelopes.find(e => e.id === id)
if (!env) throw new Error('Envelope not found')
return env
}
const response = await this.client.get<Envelope>(`/envelopes/${id}`)
return response.data
}

async verifyDocument(hash: string): Promise<{ verified: boolean; envelope?: Envelope }> {
if (this.useMock) {
await this.delay(800)
const env = mockEnvelopes.find(e =>
mockDocuments.find(d => d.id === e.documentId)?.hash === hash
)
return { verified: !!env, envelope: env }
}
const response = await this.client.post('/verify', { hash })
return response.data
}

async logout(): Promise<void> {
if (this.useMock) {
await this.delay(200)
return
}
await this.client.post('/auth/logout')
}
}

export const apiClient = new ApiClient()
Loading