Skip to content
Open
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
216 changes: 79 additions & 137 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,147 +1,80 @@
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'
import { LandingPage } from './pages/LandingPage'
import { PricingPage } from './pages/PricingPage'
import { VerifyPage } from './pages/VerifyPage'
import { ProtectedRoute } from './components/ProtectedRoute'
import { useAuth } from './contexts/AuthContext'
import { Button } from './components/Button'
import { Input } from './components/Input'
import { Card } from './components/Card'
import { useToast } from './components/ToastProvider'
import { useTranslation } from 'react-i18next'
import { useState } from 'react'

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 LoginPage() {
const { t } = useTranslation()
const { login } = useAuth()
const { showToast } = useToast()
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('demo@sigra.io')
const [password, setPassword] = useState('password')

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>
)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
await login({ email, password })
showToast('success', t('auth.loginSuccess'))
} catch (error) {
showToast('error', t('auth.invalidCredentials'))
} finally {
setLoading(false)
}
}

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>
<div className="min-h-screen flex items-center justify-center p-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
<Card className="w-full max-w-md">
<h1 className="heading-2 mb-6">{t('auth.loginTitle')}</h1>
<p className="text-muted mb-6">{t('auth.loginSubtitle')}</p>
<form onSubmit={handleSubmit} className="space-y-4">
<Input
label={t('auth.email')}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<Input
label={t('auth.password')}
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<Button type="submit" loading={loading} className="w-full">
{t('auth.loginButton')}
</Button>
</form>
</Card>
</div>
)
}

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>
function DashboardPage() {
const { t } = useTranslation()
const { user, logout } = useAuth()

<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>
return (
<div className="min-h-screen p-8 max-w-6xl mx-auto" style={{ backgroundColor: 'var(--bg-primary)' }}>
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="heading-1 mb-2">{t('dashboard.title')}</h1>
<p className="text-muted">{t('dashboard.welcome', { name: user?.firstName })}</p>
</div>
<Button variant="ghost" onClick={logout}>
{t('nav.logout')}
</Button>
</div>
</div>
)
Expand All @@ -150,9 +83,18 @@ function LayoutDemo() {
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="/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