Skip to content

Commit 358a80d

Browse files
committed
feat: add freelancer dashboard UI with earnings and escrow summary
1 parent 7b72b98 commit 358a80d

8 files changed

Lines changed: 5630 additions & 374 deletions

File tree

app/freelancer/dashboard/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Metadata } from 'next'
2+
import { FreelancerDashboard } from '@/components/freelancer/freelancer-dashboard'
3+
4+
export const metadata: Metadata = {
5+
title: 'Freelancer Dashboard | TaskChain',
6+
description:
7+
'Track active and completed contracts, earnings, and escrow status in one place.',
8+
}
9+
10+
export default function FreelancerDashboardPage() {
11+
return (
12+
<main className="min-h-screen bg-background">
13+
<FreelancerDashboard />
14+
</main>
15+
)
16+
}

components/benefits.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function Benefits() {
1111
Built for Everyone
1212
</h2>
1313
<p className="mx-auto max-w-2xl text-lg text-muted-foreground text-balance">
14-
Whether you're hiring or looking for work, TaskChain has you covered.
14+
Whether you&apos;re hiring or looking for work, TaskChain has you covered.
1515
</p>
1616
</div>
1717

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
'use client'
2+
3+
import type { ReactNode } from 'react'
4+
import { useCallback, useEffect, useMemo, useState } from 'react'
5+
import { AlertCircle, Banknote, BriefcaseBusiness, ShieldCheck } from 'lucide-react'
6+
import {
7+
getFreelancerDashboardData,
8+
type ContractItem,
9+
type EscrowEntry,
10+
type FreelancerDashboardData,
11+
} from '@/lib/freelancer-dashboard'
12+
13+
function formatCurrency(value: number): string {
14+
return new Intl.NumberFormat('en-US', {
15+
style: 'currency',
16+
currency: 'USD',
17+
maximumFractionDigits: 0,
18+
}).format(value)
19+
}
20+
21+
function formatDate(value: string): string {
22+
return new Intl.DateTimeFormat('en-US', {
23+
month: 'short',
24+
day: 'numeric',
25+
year: 'numeric',
26+
}).format(new Date(value))
27+
}
28+
29+
function Card({
30+
title,
31+
icon,
32+
value,
33+
helper,
34+
}: {
35+
title: string
36+
icon: ReactNode
37+
value: string
38+
helper: string
39+
}) {
40+
return (
41+
<article className="rounded-xl border border-border/70 bg-card/60 p-5 shadow-lg shadow-black/10 backdrop-blur">
42+
<div className="mb-3 flex items-center justify-between">
43+
<p className="text-sm text-muted-foreground">{title}</p>
44+
<span className="text-muted-foreground">{icon}</span>
45+
</div>
46+
<p className="text-2xl font-semibold text-foreground">{value}</p>
47+
<p className="mt-1 text-xs text-muted-foreground">{helper}</p>
48+
</article>
49+
)
50+
}
51+
52+
function ContractRow({
53+
contract,
54+
completed = false,
55+
}: {
56+
contract: ContractItem
57+
completed?: boolean
58+
}) {
59+
return (
60+
<li className="rounded-xl border border-border/60 bg-background/40 p-4">
61+
<div className="flex flex-wrap items-center justify-between gap-2">
62+
<p className="font-medium text-foreground">{contract.title}</p>
63+
<span className="rounded-full bg-secondary/30 px-3 py-1 text-xs text-secondary-foreground">
64+
{formatCurrency(contract.amountUsd)}
65+
</span>
66+
</div>
67+
<div className="mt-2 grid gap-2 text-sm text-muted-foreground md:grid-cols-3">
68+
<p>Client: {contract.clientName}</p>
69+
<p>Deadline: {formatDate(contract.deadline)}</p>
70+
<p>Terms: {contract.paymentTerms}</p>
71+
</div>
72+
{completed ? (
73+
<p className="mt-3 text-xs text-emerald-300">
74+
Status: {contract.payoutConfirmed ? 'Payout Confirmed' : 'Awaiting Payout Confirmation'}
75+
</p>
76+
) : null}
77+
</li>
78+
)
79+
}
80+
81+
function EscrowRow({ escrow }: { escrow: EscrowEntry }) {
82+
const statusClass =
83+
escrow.status === 'released'
84+
? 'bg-emerald-500/20 text-emerald-300'
85+
: escrow.status === 'releasing'
86+
? 'bg-amber-500/20 text-amber-300'
87+
: 'bg-sky-500/20 text-sky-300'
88+
89+
return (
90+
<li className="rounded-xl border border-border/60 bg-background/40 p-4">
91+
<div className="flex flex-wrap items-center justify-between gap-2">
92+
<p className="font-medium text-foreground">{escrow.contractTitle}</p>
93+
<span className="text-sm font-medium text-foreground">{formatCurrency(escrow.amountUsd)}</span>
94+
</div>
95+
<p className="mt-2 text-sm text-muted-foreground">{escrow.releaseCondition}</p>
96+
<span className={`mt-3 inline-flex rounded-full px-3 py-1 text-xs ${statusClass}`}>
97+
{escrow.status}
98+
</span>
99+
</li>
100+
)
101+
}
102+
103+
export function FreelancerDashboard() {
104+
const [data, setData] = useState<FreelancerDashboardData | null>(null)
105+
const [error, setError] = useState<string | null>(null)
106+
const [loading, setLoading] = useState(true)
107+
108+
const loadData = useCallback(async () => {
109+
try {
110+
const response = await getFreelancerDashboardData()
111+
setData(response)
112+
setError(null)
113+
} catch {
114+
setError('Unable to load freelancer dashboard data at this time.')
115+
} finally {
116+
setLoading(false)
117+
}
118+
}, [])
119+
120+
useEffect(() => {
121+
void loadData()
122+
123+
const interval = setInterval(() => {
124+
void loadData()
125+
}, 30000)
126+
127+
return () => clearInterval(interval)
128+
}, [loadData])
129+
130+
const totalEscrow = useMemo(() => {
131+
if (!data) return 0
132+
return data.escrow.reduce((sum, entry) => sum + entry.amountUsd, 0)
133+
}, [data])
134+
135+
if (loading) {
136+
return (
137+
<div className="flex min-h-[40vh] items-center justify-center text-sm text-muted-foreground">
138+
Loading dashboard...
139+
</div>
140+
)
141+
}
142+
143+
if (!data || error) {
144+
return (
145+
<div className="mx-auto max-w-4xl rounded-xl border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive-foreground">
146+
<div className="flex items-center gap-2">
147+
<AlertCircle className="size-4" />
148+
<p>{error ?? 'No dashboard data available.'}</p>
149+
</div>
150+
</div>
151+
)
152+
}
153+
154+
return (
155+
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
156+
<header className="mb-8">
157+
<h1 className="text-3xl font-semibold text-foreground">Freelancer Dashboard</h1>
158+
<p className="mt-2 text-sm text-muted-foreground">
159+
Last updated: {formatDate(data.updatedAt)}. Data refreshes every 30 seconds.
160+
</p>
161+
</header>
162+
163+
<section className="mb-8 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
164+
<Card
165+
title="Total Earnings"
166+
icon={<Banknote className="size-4" />}
167+
value={formatCurrency(data.earnings.totalEarningsUsd)}
168+
helper="Confirmed lifetime payouts"
169+
/>
170+
<Card
171+
title="Pending Payments"
172+
icon={<BriefcaseBusiness className="size-4" />}
173+
value={formatCurrency(data.earnings.pendingPaymentsUsd)}
174+
helper="Awaiting release from milestones"
175+
/>
176+
<Card
177+
title="Withdrawals"
178+
icon={<Banknote className="size-4" />}
179+
value={formatCurrency(data.earnings.withdrawalsUsd)}
180+
helper="Transferred to wallet or bank"
181+
/>
182+
<Card
183+
title="In Escrow"
184+
icon={<ShieldCheck className="size-4" />}
185+
value={formatCurrency(totalEscrow)}
186+
helper="Protected until release conditions are met"
187+
/>
188+
</section>
189+
190+
<section className="grid gap-6 lg:grid-cols-2">
191+
<article className="rounded-2xl border border-border/70 bg-card/50 p-5">
192+
<h2 className="text-lg font-semibold text-foreground">Active Contracts</h2>
193+
<p className="mb-4 mt-1 text-sm text-muted-foreground">
194+
Ongoing work with deadlines and payment terms.
195+
</p>
196+
<ul className="space-y-3">
197+
{data.activeContracts.length > 0 ? (
198+
data.activeContracts.map((contract) => (
199+
<ContractRow key={contract.id} contract={contract} />
200+
))
201+
) : (
202+
<li className="rounded-xl border border-border/60 bg-background/30 p-4 text-sm text-muted-foreground">
203+
No active contracts right now.
204+
</li>
205+
)}
206+
</ul>
207+
</article>
208+
209+
<article className="rounded-2xl border border-border/70 bg-card/50 p-5">
210+
<h2 className="text-lg font-semibold text-foreground">Completed Contracts</h2>
211+
<p className="mb-4 mt-1 text-sm text-muted-foreground">
212+
Finished contracts and payout confirmations.
213+
</p>
214+
<ul className="space-y-3">
215+
{data.completedContracts.length > 0 ? (
216+
data.completedContracts.map((contract) => (
217+
<ContractRow key={contract.id} contract={contract} completed />
218+
))
219+
) : (
220+
<li className="rounded-xl border border-border/60 bg-background/30 p-4 text-sm text-muted-foreground">
221+
No completed contracts available yet.
222+
</li>
223+
)}
224+
</ul>
225+
</article>
226+
</section>
227+
228+
<section className="mt-6 rounded-2xl border border-border/70 bg-card/50 p-5">
229+
<h2 className="text-lg font-semibold text-foreground">Escrow Status</h2>
230+
<p className="mb-4 mt-1 text-sm text-muted-foreground">
231+
Funds currently held and their release conditions.
232+
</p>
233+
<ul className="space-y-3">
234+
{data.escrow.length > 0 ? (
235+
data.escrow.map((entry) => <EscrowRow key={entry.id} escrow={entry} />)
236+
) : (
237+
<li className="rounded-xl border border-border/60 bg-background/30 p-4 text-sm text-muted-foreground">
238+
No funds are currently in escrow.
239+
</li>
240+
)}
241+
</ul>
242+
</section>
243+
</div>
244+
)
245+
}

components/testimonials.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function Testimonials() {
5050
</div>
5151

5252
<p className="text-muted-foreground leading-relaxed">
53-
"{testimonial.content}"
53+
&ldquo;{testimonial.content}&rdquo;
5454
</p>
5555

5656
<div className="flex items-center gap-3 pt-4 border-t border-border/40">

eslint.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig, globalIgnores } from 'eslint/config'
2+
import nextVitals from 'eslint-config-next/core-web-vitals'
3+
4+
export default defineConfig([
5+
...nextVitals,
6+
globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']),
7+
])

0 commit comments

Comments
 (0)