diff --git a/app/dashboard/profile/page.tsx b/app/dashboard/profile/page.tsx
new file mode 100644
index 0000000..c6968b1
--- /dev/null
+++ b/app/dashboard/profile/page.tsx
@@ -0,0 +1,15 @@
+import type { Metadata } from 'next'
+import { FreelancerProfile } from '@/components/freelancer/freelancer-profile'
+
+export const metadata: Metadata = {
+ title: 'Profile | TaskChain',
+ description: 'View your freelancer profile, reputation score, skills, and completed projects.',
+}
+
+export default function ProfilePage() {
+ return (
+
+
+
+ )
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 1f17a3e..44ef6a3 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -35,7 +35,7 @@ export default function RootLayout({
}>) {
return (
-
+
-
-
- Profile
+
+
+
+ Profile
+
Settings
Logout
diff --git a/components/dashboard/sidebar.tsx b/components/dashboard/sidebar.tsx
index 3506a2f..f0f28bd 100644
--- a/components/dashboard/sidebar.tsx
+++ b/components/dashboard/sidebar.tsx
@@ -2,12 +2,13 @@
import Link from 'next/link'
import { usePathname } from 'next/navigation'
-import {
- LayoutDashboard,
- FileText,
- AlertCircle,
+import {
+ LayoutDashboard,
+ FileText,
+ AlertCircle,
LogOut,
ChevronRight,
+ UserCircle,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
@@ -33,6 +34,11 @@ const navItems = [
href: '/dashboard/disputes',
icon: AlertCircle,
},
+ {
+ label: 'Profile',
+ href: '/dashboard/profile',
+ icon: UserCircle,
+ },
]
export function Sidebar({ open, onOpenChange }: SidebarProps) {
diff --git a/components/freelancer/freelancer-profile.tsx b/components/freelancer/freelancer-profile.tsx
new file mode 100644
index 0000000..fdcd533
--- /dev/null
+++ b/components/freelancer/freelancer-profile.tsx
@@ -0,0 +1,441 @@
+'use client'
+
+import { useCallback, useEffect, useState } from 'react'
+import {
+ AlertCircle,
+ CheckCircle2,
+ Clock,
+ Copy,
+ ExternalLink,
+ Shield,
+ Star,
+ TrendingUp,
+ Wallet,
+} from 'lucide-react'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Progress } from '@/components/ui/progress'
+import type { FreelancerReputationPayload } from '@/lib/reputation'
+
+const MOCK_SKILLS = [
+ 'Next.js',
+ 'TypeScript',
+ 'React',
+ 'Node.js',
+ 'Stellar',
+ 'Solidity',
+ 'Tailwind CSS',
+ 'REST APIs',
+ 'Web3',
+ 'PostgreSQL',
+]
+
+const MOCK_BIO =
+ 'Full-stack developer specializing in blockchain-powered applications and decentralized work platforms. Passionate about building transparent, secure systems that empower freelancers and clients worldwide.'
+
+const MOCK_TITLE = 'Full-Stack & Web3 Developer'
+
+const FALLBACK_REPUTATION: FreelancerReputationPayload = {
+ userId: 0,
+ reputationScore: null,
+ computedAt: new Date().toISOString(),
+ metrics: {
+ completionRate: null,
+ disputeRate: null,
+ totalVolume: 0,
+ onTimeDeliveryPct: null,
+ jobsStarted: 0,
+ jobsCompleted: 0,
+ jobsWithDispute: 0,
+ completedWithDeadline: 0,
+ onTimeDeliveries: 0,
+ },
+}
+
+function pct(value: number | null): string {
+ if (value === null) return '—'
+ return `${Math.round(value * 100)}%`
+}
+
+function formatVolume(value: number): string {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ maximumFractionDigits: 0,
+ }).format(value)
+}
+
+function truncateWallet(address: string): string {
+ if (address.length <= 12) return address
+ return `${address.slice(0, 6)}…${address.slice(-4)}`
+}
+
+function reputationColor(score: number | null): string {
+ if (score === null) return 'text-muted-foreground'
+ if (score >= 90) return 'text-emerald-400'
+ if (score >= 70) return 'text-primary'
+ if (score >= 40) return 'text-amber-400'
+ return 'text-destructive'
+}
+
+function reputationLabel(score: number | null): string {
+ if (score === null) return 'Unrated'
+ if (score >= 90) return 'Excellent'
+ if (score >= 70) return 'Good'
+ if (score >= 40) return 'Fair'
+ return 'Needs Improvement'
+}
+
+function StatCard({
+ icon,
+ label,
+ value,
+}: {
+ icon: React.ReactNode
+ label: string
+ value: string
+}) {
+ return (
+
+
+ {value}
+
+ )
+}
+
+function SkillBadge({ skill }: { skill: string }) {
+ return (
+
+ {skill}
+
+ )
+}
+
+function MetricRow({
+ label,
+ value,
+ barValue,
+}: {
+ label: string
+ value: string
+ barValue: number
+}) {
+ return (
+
+
+ {label}
+ {value}
+
+
+
+ )
+}
+
+export function FreelancerProfile() {
+ const [reputation, setReputation] = useState(FALLBACK_REPUTATION)
+ const [walletAddress, setWalletAddress] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [copied, setCopied] = useState(false)
+
+ const load = useCallback(async () => {
+ try {
+ const [repRes, meRes] = await Promise.all([
+ fetch('/api/freelancer/reputation', { credentials: 'include', cache: 'no-store' }),
+ fetch('/api/auth/me', { credentials: 'include', cache: 'no-store' }),
+ ])
+
+ if (repRes.ok) {
+ const data = (await repRes.json()) as FreelancerReputationPayload
+ setReputation(data)
+ }
+
+ if (meRes.ok) {
+ const me = (await meRes.json()) as { walletAddress: string }
+ setWalletAddress(me.walletAddress ?? '')
+ }
+
+ setError(null)
+ } catch {
+ setError('Unable to load profile data. Showing cached information.')
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ void load()
+ }, [load])
+
+ async function copyWallet() {
+ if (!walletAddress) return
+ await navigator.clipboard.writeText(walletAddress)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ const { metrics, reputationScore } = reputation
+ const displayName = walletAddress
+ ? `Freelancer ${walletAddress.slice(0, 4).toUpperCase()}`
+ : 'Freelancer'
+
+ if (loading) {
+ return (
+
+ Loading profile…
+
+ )
+ }
+
+ return (
+
+ {error && (
+
+ )}
+
+ {/* Hero */}
+
+
+ {/* Avatar */}
+
+
+
+ {displayName.charAt(0)}
+
+
+
+
+
+
+ Verified Freelancer
+
+
+
+ {/* Info */}
+
+
+
+
+ {displayName}
+
+
{MOCK_TITLE}
+
+
+ {/* Reputation Score */}
+
+
+
+ {reputationScore !== null ? Math.round(reputationScore) : '—'}
+
+
+ {reputationLabel(reputationScore)}
+
+
+
+
+
+ {MOCK_BIO}
+
+
+ {/* Wallet */}
+
+
+
+
+ {walletAddress ? truncateWallet(walletAddress) : 'Not connected'}
+
+ {walletAddress && (
+
+ )}
+
+ {walletAddress && (
+
+
+ View on Explorer
+
+ )}
+
+
+
+
+
+ {/* Stats */}
+
+ }
+ label="Completed Projects"
+ value={String(metrics.jobsCompleted)}
+ />
+ }
+ label="Success Rate"
+ value={pct(metrics.completionRate)}
+ />
+ }
+ label="On-Time Delivery"
+ value={pct(metrics.onTimeDeliveryPct)}
+ />
+ }
+ label="Total Volume"
+ value={formatVolume(metrics.totalVolume)}
+ />
+
+
+
+ {/* Left column: Skills + Projects */}
+
+ {/* Skills */}
+
+ Skills
+
+ Technologies and areas of expertise.
+
+
+ {MOCK_SKILLS.map((skill) => (
+
+ ))}
+
+
+
+ {/* Completed Projects */}
+
+ Completed Projects
+
+ {metrics.jobsCompleted > 0
+ ? `${metrics.jobsCompleted} project${metrics.jobsCompleted !== 1 ? 's' : ''} delivered successfully.`
+ : 'No completed projects yet.'}
+
+ {metrics.jobsCompleted > 0 ? (
+
+ ) : (
+
+
+
+ Completed projects will appear here once work is delivered.
+
+
+ )}
+
+
+
+ {/* Right column: Reputation Breakdown */}
+
+
+ Reputation Breakdown
+
+ Performance metrics across all contracts.
+
+
+
+
+
+
+
+
+
+ Jobs Started
+ {metrics.jobsStarted}
+
+
+ Jobs Completed
+ {metrics.jobsCompleted}
+
+
+ Disputes
+ {metrics.jobsWithDispute}
+
+
+
+ {reputationScore !== null && (
+
+
Overall Score
+
+ {Math.round(reputationScore)}
+ /100
+
+
+ {reputationLabel(reputationScore)}
+
+
+ )}
+
+
+
+
+ )
+}