Skip to content
Merged
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
23 changes: 18 additions & 5 deletions components/freelancer/freelancer-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
type EscrowEntry,
type FreelancerDashboardData,
} from '@/lib/freelancer-dashboard'
import { MilestoneModal } from '@/components/milestone-modal'
import { Plus } from 'lucide-react'
import { Button } from '@/components/ui/button'

function formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
Expand Down Expand Up @@ -153,11 +156,21 @@ export function FreelancerDashboard() {

return (
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<header className="mb-8">
<h1 className="text-3xl font-semibold text-foreground">Freelancer Dashboard</h1>
<p className="mt-2 text-sm text-muted-foreground">
Last updated: {formatDate(data.updatedAt)}. Data refreshes every 30 seconds.
</p>
<header className="mb-8 flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-semibold text-foreground">Freelancer Dashboard</h1>
<p className="mt-2 text-sm text-muted-foreground">
Last updated: {formatDate(data.updatedAt)}. Data refreshes every 30 seconds.
</p>
</div>
<MilestoneModal
trigger={
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">
<Plus className="mr-2 h-4 w-4" />
New Milestone
</Button>
}
/>
</header>

<section className="mb-8 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
Expand Down
208 changes: 208 additions & 0 deletions components/milestone-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"use client"

import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { format } from "date-fns"
import { CalendarIcon, Loader2 } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Calendar } from "@/components/ui/calendar"

const milestoneSchema = z.object({
title: z.string().min(2, {
message: "Title must be at least 2 characters.",
}),
description: z.string().min(10, {
message: "Description must be at least 10 characters.",
}),
amount: z.coerce.number().positive({
message: "Amount must be a positive number.",
}),
dueDate: z.date({
required_error: "A due date is required.",
}).refine((date) => date > new Date(), {
message: "Due date must be in the future.",
}),
})

type MilestoneFormValues = z.infer<typeof milestoneSchema>

interface MilestoneModalProps {
trigger?: React.ReactNode
projectId?: string
onSuccess?: (data: MilestoneFormValues) => void
}

export function MilestoneModal({ trigger, projectId, onSuccess }: MilestoneModalProps) {
const [open, setOpen] = React.useState(false)
const [isSubmitting, setIsSubmitting] = React.useState(false)

const form = useForm<MilestoneFormValues>({
resolver: zodResolver(milestoneSchema),
defaultValues: {
title: "",
description: "",
amount: 0,
},
})

async function onSubmit(data: MilestoneFormValues) {
setIsSubmitting(true)
try {
// Simulate API call
console.log("Submitting milestone:", { ...data, projectId })
await new Promise((resolve) => setTimeout(resolve, 1000))

onSuccess?.(data)
setOpen(false)
form.reset()
} catch (error) {
console.error("Failed to create milestone:", error)
} finally {
setIsSubmitting(false)
}
}

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{trigger || <Button variant="outline">Create Milestone</Button>}
</DialogTrigger>
<DialogContent className="sm:max-w-[500px] border-border/40 bg-card/95 backdrop-blur-md">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">Create Project Milestone</DialogTitle>
<DialogDescription>
Hold funds in escrow and release them once the work is completed.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 py-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Milestone Title</FormLabel>
<FormControl>
<Input placeholder="e.g. Initial Design Prototype" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Detail the deliverables for this milestone..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount (USD)</FormLabel>
<FormControl>
<Input type="number" step="0.01" placeholder="0.00" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dueDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="mb-1">Due Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={cn(
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date) =>
date < new Date(new Date().setHours(0, 0, 0, 0))
}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter className="pt-4">
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Milestone
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}
2 changes: 2 additions & 0 deletions components/ui/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export function ThemeToggle() {
useEffect(() => {
if (dark) {
document.documentElement.classList.add("dark");
// eslint-disable-next-line react-hooks/set-state-in-effect
setDark(true);
} else {
document.documentElement.classList.remove("dark");
}
Expand Down
50 changes: 27 additions & 23 deletions components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,60 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"

const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
},
}
)

function Button({
className,
variant,
size,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<'button'> &
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
const Comp = asChild ? Slot.Root : "button"

return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
Expand Down
Loading
Loading