From 3e3a3dfbc8a793461f63f48a38579a306535bc61 Mon Sep 17 00:00:00 2001 From: Oluwamorowa Date: Fri, 29 May 2026 16:27:09 +0100 Subject: [PATCH 1/5] fix(layout): add suppressHydrationWarning to body element for improved rendering --- app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ( - + Date: Fri, 29 May 2026 16:27:23 +0100 Subject: [PATCH 2/5] feat(profile): create Freelancer Profile Page with metadata and layout --- app/dashboard/profile/page.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/dashboard/profile/page.tsx 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 ( +
+ +
+ ) +} From 95c2b97db9821527e8e78e23ea12069ebb5f88e6 Mon Sep 17 00:00:00 2001 From: Oluwamorowa Date: Fri, 29 May 2026 16:27:42 +0100 Subject: [PATCH 3/5] feat(header): update profile link in dropdown menu for navigation --- components/dashboard/header.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/dashboard/header.tsx b/components/dashboard/header.tsx index 641f426..e387b82 100644 --- a/components/dashboard/header.tsx +++ b/components/dashboard/header.tsx @@ -1,5 +1,6 @@ 'use client' +import Link from 'next/link' import { Menu, Bell, User } from 'lucide-react' import { Button } from '@/components/ui/button' import { @@ -48,9 +49,11 @@ export function DashboardHeader({ onMenuClick }: DashboardHeaderProps) { - - - Profile + + + + Profile + Settings Logout From d88c6adb1a1115f72acb83a56b87e2509f4cdcae Mon Sep 17 00:00:00 2001 From: Oluwamorowa Date: Fri, 29 May 2026 16:27:52 +0100 Subject: [PATCH 4/5] feat(sidebar): add Profile navigation item to sidebar --- components/dashboard/sidebar.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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) { From e04d10a5a2a37d43a6afc5c90d3b596553737fc2 Mon Sep 17 00:00:00 2001 From: Oluwamorowa Date: Fri, 29 May 2026 16:28:02 +0100 Subject: [PATCH 5/5] feat(profile): implement Freelancer Profile Page with skills, reputation, and wallet details --- components/freelancer/freelancer-profile.tsx | 441 +++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 components/freelancer/freelancer-profile.tsx 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 ( +
+
+ {icon} +

{label}

+
+

{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 && ( +
+ + {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 ? ( +
    + {Array.from({ length: Math.min(metrics.jobsCompleted, 3) }, (_, i) => ( +
  • + +
    +

    + Project #{i + 1} +

    +

    Completed · Payout confirmed

    +
    + + Done + +
  • + ))} + {metrics.jobsCompleted > 3 && ( +

    + +{metrics.jobsCompleted - 3} more completed +

    + )} +
+ ) : ( +
+ +

+ 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)} +

+
+ )} +
+
+
+
+ ) +}