From 5fe46a8231a51243fdb76634c439107dc57e359f Mon Sep 17 00:00:00 2001 From: legend4tech Date: Wed, 25 Feb 2026 20:25:03 +0100 Subject: [PATCH] feat: Create Client Dashboard UI --- app/dashboard/disputes/page.tsx | 227 + app/dashboard/layout.tsx | 25 + app/dashboard/page.tsx | 104 + app/dashboard/projects/[id]/page.tsx | 259 + app/dashboard/projects/page.tsx | 263 + components/dashboard/approval-dialog.tsx | 140 + .../dashboard/create-project-dialog.tsx | 198 + components/dashboard/dispute-form.tsx | 238 + components/dashboard/header.tsx | 63 + components/dashboard/milestones-list.tsx | 133 + components/dashboard/project-card.tsx | 183 + components/dashboard/sidebar.tsx | 105 + components/dashboard/timeline-activity.tsx | 101 + components/ui/badge.tsx | 48 + components/ui/card.tsx | 92 + components/ui/checkbox.tsx | 32 + components/ui/dialog.tsx | 158 + components/ui/dropdown-menu.tsx | 257 + components/ui/input.tsx | 21 + components/ui/label.tsx | 24 + components/ui/progress.tsx | 31 + components/ui/select.tsx | 190 + components/ui/tabs.tsx | 91 + components/ui/textarea.tsx | 18 + package.json | 1 + pnpm-lock.yaml | 7521 +++++++++++++---- 26 files changed, 9002 insertions(+), 1521 deletions(-) create mode 100644 app/dashboard/disputes/page.tsx create mode 100644 app/dashboard/layout.tsx create mode 100644 app/dashboard/page.tsx create mode 100644 app/dashboard/projects/[id]/page.tsx create mode 100644 app/dashboard/projects/page.tsx create mode 100644 components/dashboard/approval-dialog.tsx create mode 100644 components/dashboard/create-project-dialog.tsx create mode 100644 components/dashboard/dispute-form.tsx create mode 100644 components/dashboard/header.tsx create mode 100644 components/dashboard/milestones-list.tsx create mode 100644 components/dashboard/project-card.tsx create mode 100644 components/dashboard/sidebar.tsx create mode 100644 components/dashboard/timeline-activity.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx diff --git a/app/dashboard/disputes/page.tsx b/app/dashboard/disputes/page.tsx new file mode 100644 index 0000000..cac9fd0 --- /dev/null +++ b/app/dashboard/disputes/page.tsx @@ -0,0 +1,227 @@ +"use client"; + +import { useState } from "react"; +import { + AlertCircle, + Plus, + MessageSquare, + Clock, + CheckCircle2, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { DisputeForm } from "@/components/dashboard/dispute-form"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +interface Dispute { + id: string; + projectTitle: string; + reason: string; + status: "open" | "in-review" | "resolved"; + createdDate: string; + messages: number; + evidence: string[]; +} + +const mockDisputes: Dispute[] = [ + { + id: "1", + projectTitle: "Logo Design - Revision Required", + reason: + "Deliverables do not match the agreed-upon specifications. Colors and fonts are different from requirements.", + status: "in-review", + createdDate: "2024-02-20", + messages: 5, + evidence: ["design_spec.pdf", "screenshot_comparison.png"], + }, +]; + +const statusConfig = { + open: { + color: "bg-amber-500/20", + text: "Open", + textColor: "text-amber-500", + icon: AlertCircle, + }, + "in-review": { + color: "bg-secondary/20", + text: "In Review", + textColor: "text-secondary", + icon: Clock, + }, + resolved: { + color: "bg-accent/20", + text: "Resolved", + textColor: "text-accent", + icon: CheckCircle2, + }, +}; + +export default function DisputesPage() { + const [showDisputeForm, setShowDisputeForm] = useState(false); + const [selectedDispute, setSelectedDispute] = useState(null); + + return ( +
+
+ {/* Header */} +
+
+

Disputes

+

+ Manage disputes and resolutions +

+
+ +
+ + {/* Stats */} +
+
+

Total Disputes

+

1

+
+
+

In Review

+

1

+
+
+

Resolved

+

0

+
+
+ + {/* Disputes List */} + {mockDisputes.length > 0 ? ( +
+ {mockDisputes.map((dispute) => { + const config = statusConfig[dispute.status]; + const Icon = config.icon; + + return ( +
setSelectedDispute(dispute)} + > +
+ {/* Header */} +
+
+
+ +

+ {dispute.projectTitle} +

+ + {config.text} + +
+

+ {dispute.reason} +

+
+
+ + {/* Details */} +
+
+

+ Created +

+

+ {new Date(dispute.createdDate).toLocaleDateString()} +

+
+
+

+ Messages +

+
+ +

{dispute.messages}

+
+
+
+

+ Evidence +

+

+ {dispute.evidence.length} files +

+
+
+ + {/* Evidence */} + {dispute.evidence.length > 0 && ( +
+

+ Evidence: +

+
+ {dispute.evidence.map((file, idx) => ( +
+ 📎 {file} +
+ ))} +
+
+ )} + + {/* Action */} +
+ +
+
+
+ ); + })} +
+ ) : ( +
+
+ +
+

No Disputes

+

+ You don't have any active disputes. +

+ +
+ )} +
+ + {/* Dialogs */} + +
+ ); +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx new file mode 100644 index 0000000..3b134e4 --- /dev/null +++ b/app/dashboard/layout.tsx @@ -0,0 +1,25 @@ +'use client' + +import { useState } from 'react' +import { Sidebar } from '@/components/dashboard/sidebar' +import { DashboardHeader } from '@/components/dashboard/header' + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode +}) { + const [sidebarOpen, setSidebarOpen] = useState(true) + + return ( +
+ +
+ setSidebarOpen(!sidebarOpen)} /> +
+ {children} +
+
+
+ ) +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..ab29a00 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { useState } from "react"; +import { Project, ProjectCard } from "@/components/dashboard/project-card"; +import { CreateProjectDialog } from "@/components/dashboard/create-project-dialog"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; + +// Mock data - replace with real data from API +const mockProjects: Project[] = [ + { + id: "1", + title: "Website Redesign", + description: "Complete redesign of company website", + status: "in-progress", + budget: 5000, + progress: 65, + milestonesCount: 4, + completedMilestones: 2, + deadline: "2024-03-15", + }, + { + id: "2", + title: "Mobile App Development", + description: "iOS and Android app development", + status: "pending-approval", + budget: 15000, + progress: 100, + milestonesCount: 5, + completedMilestones: 5, + deadline: "2024-03-30", + }, + { + id: "3", + title: "Logo Design", + description: "Brand identity and logo design", + status: "pending", + budget: 2000, + progress: 0, + milestonesCount: 2, + completedMilestones: 0, + deadline: "2024-02-28", + }, +]; + +export default function DashboardPage() { + const [showCreateDialog, setShowCreateDialog] = useState(false); + + return ( +
+
+ {/* Header */} +
+
+

Projects

+

+ Manage your projects and milestones +

+
+ +
+ + {/* Stats */} +
+
+

+ Active Projects +

+

3

+
+
+

+ Pending Approval +

+

1

+
+
+

Total Escrow

+

$22,000

+
+
+ + {/* Projects Grid */} +
+ {mockProjects.map((project) => ( + + ))} +
+
+ + +
+ ); +} diff --git a/app/dashboard/projects/[id]/page.tsx b/app/dashboard/projects/[id]/page.tsx new file mode 100644 index 0000000..2a427bd --- /dev/null +++ b/app/dashboard/projects/[id]/page.tsx @@ -0,0 +1,259 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { + ArrowLeft, + CheckCircle2, + Clock, + AlertCircle, + Download, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Card } from "@/components/ui/card"; +import { MilestonesList } from "@/components/dashboard/milestones-list"; +import { ApprovalDialog } from "@/components/dashboard/approval-dialog"; +import { TimelineActivity } from "@/components/dashboard/timeline-activity"; + +interface ProjectDetail { + id: string; + title: string; + description: string; + status: "pending" | "in-progress" | "pending-approval" | "completed"; + budget: number; + progress: number; + milestonesCount: number; + completedMilestones: number; + deadline: string; + freelancer: { + name: string; + avatar: string; + rating: number; + reviews: number; + }; + escrowAmount: number; + releaseAmount: number; + files: Array<{ + name: string; + size: string; + uploadedDate: string; + }>; +} + +// Mock project detail data +const mockProjectDetail: ProjectDetail = { + id: "1", + title: "Website Redesign", + description: "Complete redesign of company website with modern UI/UX", + status: "in-progress", + budget: 5000, + progress: 65, + milestonesCount: 4, + completedMilestones: 2, + deadline: "2024-03-15", + freelancer: { + name: "Alex Johnson", + avatar: "", + rating: 4.8, + reviews: 42, + }, + escrowAmount: 5000, + releaseAmount: 2500, + files: [ + { + name: "Website_Mockups_v2.xd", + size: "45 MB", + uploadedDate: "2024-02-20", + }, + { + name: "Design_Specifications.pdf", + size: "2.3 MB", + uploadedDate: "2024-02-18", + }, + { + name: "Component_Library.figma", + size: "52 MB", + uploadedDate: "2024-02-15", + }, + ], +}; + +export default function ProjectDetailPage() { + const params = useParams(); + const [showApprovalDialog, setShowApprovalDialog] = useState(false); + const project = mockProjectDetail; + const [now] = useState(() => Date.now()); + const daysLeft = Math.ceil( + (new Date(project.deadline).getTime() - now) / (1000 * 60 * 60 * 24), + ); + const isOverdue = daysLeft < 0; + + return ( +
+
+ {/* Header */} +
+
+ + + +
+

{project.title}

+

+ {project.description} +

+
+
+ + {project.status.replace("-", " ")} + +
+ + {/* Quick Stats */} +
+ +

+ Budget +

+

+ ${project.budget.toLocaleString()} +

+
+ +

+ Progress +

+

+ {project.progress}% +

+
+ +

+ In Escrow +

+

+ ${project.escrowAmount.toLocaleString()} +

+
+ +

+ Deadline +

+

+ {isOverdue ? `${Math.abs(daysLeft)}d ago` : `${daysLeft}d left`} +

+
+
+ + {/* Main Content */} + + + Milestones + Files & Deliverables + Activity + + + +
+
+

Project Milestones

+

+ {project.completedMilestones} of {project.milestonesCount}{" "} + completed +

+
+ {project.status === "pending-approval" && ( + + )} +
+ +
+ + +
+

Uploaded Files

+
+ {project.files.map((file, idx) => ( +
+
+
+ + {file.name.split(".").pop()?.toUpperCase()} + +
+
+

{file.name}

+

+ {file.size} •{" "} + {new Date(file.uploadedDate).toLocaleDateString()} +

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

Activity Log

+ +
+
+
+ + {/* Freelancer Info */} + +

Freelancer

+
+
+
+
+

+ {project.freelancer.name} +

+
+ + ★ {project.freelancer.rating} + + + ({project.freelancer.reviews} reviews) + +
+
+
+ +
+ +
+ + {/* Approval Dialog */} + +
+ ); +} diff --git a/app/dashboard/projects/page.tsx b/app/dashboard/projects/page.tsx new file mode 100644 index 0000000..1bb16fb --- /dev/null +++ b/app/dashboard/projects/page.tsx @@ -0,0 +1,263 @@ +"use client"; + +import { useMemo, useState } from "react"; +import Link from "next/link"; +import { Search, Filter, ChevronRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface Project { + id: string; + title: string; + status: "pending" | "in-progress" | "pending-approval" | "completed"; + budget: number; + progress: number; + milestonesCount: number; + completedMilestones: number; + deadline: string; +} + +const mockProjects: Project[] = [ + { + id: "1", + title: "Website Redesign", + status: "in-progress", + budget: 5000, + progress: 65, + milestonesCount: 4, + completedMilestones: 2, + deadline: "2024-03-15", + }, + { + id: "2", + title: "Mobile App Development", + status: "pending-approval", + budget: 15000, + progress: 100, + milestonesCount: 5, + completedMilestones: 5, + deadline: "2024-03-30", + }, + { + id: "3", + title: "Logo Design", + status: "pending", + budget: 2000, + progress: 0, + milestonesCount: 2, + completedMilestones: 0, + deadline: "2024-02-28", + }, + { + id: "4", + title: "Content Writing", + status: "completed", + budget: 3000, + progress: 100, + milestonesCount: 3, + completedMilestones: 3, + deadline: "2024-02-10", + }, +]; + +const statusConfig = { + pending: { + color: "bg-muted", + text: "Pending", + textColor: "text-muted-foreground", + }, + "in-progress": { + color: "bg-secondary/20", + text: "In Progress", + textColor: "text-secondary", + }, + "pending-approval": { + color: "bg-amber-500/20", + text: "Pending Approval", + textColor: "text-amber-500", + }, + completed: { + color: "bg-accent/20", + text: "Completed", + textColor: "text-accent", + }, +}; + +export default function ProjectsPage() { + const [searchTerm, setSearchTerm] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [now] = useState(() => Date.now()); + + const filteredProjects = mockProjects.filter((project) => { + const matchesSearch = project.title + .toLowerCase() + .includes(searchTerm.toLowerCase()); + const matchesStatus = + statusFilter === "all" || project.status === statusFilter; + return matchesSearch && matchesStatus; + }); + + return ( +
+
+ {/* Header */} +
+

All Projects

+

+ View and manage all your projects +

+
+ + {/* Filters */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10 border-border/40" + /> +
+ +
+ + {/* Projects Table */} +
+
+ + + + + + + + + + + + + + {filteredProjects.map((project) => { + const config = statusConfig[project.status]; + const daysLeft = Math.ceil( + (new Date(project.deadline).getTime() - now) / + (1000 * 60 * 60 * 24), + ); + const isOverdue = daysLeft < 0; + + return ( + + + + + + + + + + ); + })} + +
ProjectStatus + Progress + Budget + Milestones + + Deadline + Action
+

{project.title}

+
+ + {config.text} + + +
+
+
+
+ + {project.progress}% + +
+
+

+ ${project.budget.toLocaleString()} +

+
+

+ {project.completedMilestones}/ + {project.milestonesCount} +

+
+

+ {isOverdue + ? `${Math.abs(daysLeft)}d ago` + : `${daysLeft}d left`} +

+
+ + + +
+
+
+ + {/* Empty State */} + {filteredProjects.length === 0 && ( +
+

+ No projects found matching your criteria +

+ +
+ )} +
+
+ ); +} diff --git a/components/dashboard/approval-dialog.tsx b/components/dashboard/approval-dialog.tsx new file mode 100644 index 0000000..9e048e2 --- /dev/null +++ b/components/dashboard/approval-dialog.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useState } from "react"; +import { CheckCircle2, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; + +interface ApprovalDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + projectTitle: string; + amount: number; +} + +export function ApprovalDialog({ + open, + onOpenChange, + projectTitle, + amount, +}: ApprovalDialogProps) { + const [agreeToTerms, setAgreeToTerms] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + const handleApprove = async () => { + if (!agreeToTerms) return; + + setIsProcessing(true); + // Simulate API call + setTimeout(() => { + setIsProcessing(false); + onOpenChange(false); + setAgreeToTerms(false); + }, 2000); + }; + + return ( + + + + + + Approve & Release Payment + + + Review the milestone completion before approving payment + + + +
+ {/* Project Details */} +
+

+ Project +

+

{projectTitle}

+
+ + {/* Amount */} +
+

Payment Amount

+

+ ${amount.toLocaleString()} +

+

+ This amount will be released from escrow to the freelancer +

+
+ + {/* Confirmation */} +
+
+ +
+

+ Please verify: +

+
    +
  • ✓ All deliverables have been completed
  • +
  • ✓ Quality meets your expectations
  • +
  • ✓ All requirements are satisfied
  • +
+
+
+ + {/* Agreement */} +
+ +
+
+
+ + + + + +
+
+ ); +} diff --git a/components/dashboard/create-project-dialog.tsx b/components/dashboard/create-project-dialog.tsx new file mode 100644 index 0000000..5f9ff26 --- /dev/null +++ b/components/dashboard/create-project-dialog.tsx @@ -0,0 +1,198 @@ +'use client' + +import { useState } from 'react' +import { Plus } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +interface CreateProjectDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogProps) { + const [isLoading, setIsLoading] = useState(false) + const [formData, setFormData] = useState({ + title: '', + description: '', + budget: '', + deadline: '', + milestones: '3', + }) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + + // Simulate API call + setTimeout(() => { + setIsLoading(false) + onOpenChange(false) + setFormData({ + title: '', + description: '', + budget: '', + deadline: '', + milestones: '3', + }) + }, 1500) + } + + return ( + + + + + + Create New Project + + + Define your project requirements and budget to get started + + + +
+ {/* Title */} +
+ + + setFormData({ ...formData, title: e.target.value }) + } + required + className="border-border/40" + /> +
+ + {/* Description */} +
+ +