feat: add 6-step job wizard with Preview, Billing, Payment steps and Stripe integration preparation

This commit is contained in:
Tiago Yamamoto 2025-12-23 23:00:17 -03:00
parent 42e9f81f48
commit 9f7d8e9ca5
6 changed files with 492 additions and 91 deletions

View file

@ -0,0 +1,75 @@
-- Migration: Create job_payments table
-- Description: Track payments for job postings
CREATE TABLE IF NOT EXISTS job_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID NOT NULL,
user_id UUID,
-- Stripe
stripe_session_id VARCHAR(255),
stripe_payment_intent VARCHAR(255),
stripe_customer_id VARCHAR(255),
-- Amount
amount DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
-- Status
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'failed', 'refunded', 'expired')),
-- Billing Info
billing_type VARCHAR(20) CHECK (billing_type IN ('company', 'individual')),
billing_name VARCHAR(255),
billing_email VARCHAR(255),
billing_phone VARCHAR(50),
billing_address TEXT,
billing_city VARCHAR(100),
billing_state VARCHAR(100),
billing_zip VARCHAR(20),
billing_country VARCHAR(2) DEFAULT 'BR',
-- Job Posting Details
duration_days INT DEFAULT 30,
is_featured BOOLEAN DEFAULT false,
featured_price DECIMAL(12,2) DEFAULT 0,
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
paid_at TIMESTAMP,
expires_at TIMESTAMP,
-- Foreign key (UUID to match jobs table)
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
);
-- Indexes
CREATE INDEX idx_job_payments_job_id ON job_payments(job_id);
CREATE INDEX idx_job_payments_status ON job_payments(status);
CREATE INDEX idx_job_payments_stripe_session ON job_payments(stripe_session_id);
CREATE INDEX idx_job_payments_user ON job_payments(user_id);
-- Comments
COMMENT ON TABLE job_payments IS 'Payment records for job postings';
COMMENT ON COLUMN job_payments.duration_days IS 'How many days the job posting is active';
COMMENT ON COLUMN job_payments.is_featured IS 'Whether this is a featured/sponsored listing';
-- Add job_posting_price table for pricing configuration
CREATE TABLE IF NOT EXISTS job_posting_prices (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
price DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
duration_days INT DEFAULT 30,
is_featured BOOLEAN DEFAULT false,
stripe_price_id VARCHAR(255),
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert default pricing
INSERT INTO job_posting_prices (name, description, price, currency, duration_days, is_featured, active)
VALUES
('Standard Job Posting', 'Your job will be visible for 30 days', 32.61, 'USD', 30, false, true),
('Featured Job Posting', 'Your job will be featured and visible for 30 days', 47.61, 'USD', 30, true, true);

View file

@ -109,6 +109,34 @@ erDiagram
int user_id FK
int job_id FK
}
job_payments {
uuid id PK
uuid job_id FK
uuid user_id FK
varchar stripe_session_id
varchar stripe_payment_intent
decimal amount
varchar currency
varchar status
varchar billing_type
varchar billing_name
varchar billing_email
int duration_days
timestamp created_at
timestamp paid_at
}
job_posting_prices {
int id PK
varchar name
decimal price
varchar currency
int duration_days
boolean is_featured
varchar stripe_price_id
boolean active
}
users ||--o{ user_companies : "belongs to"
companies ||--o{ user_companies : "has members"
@ -116,6 +144,12 @@ erDiagram
users ||--o{ jobs : "creates"
jobs ||--o{ applications : "receives"
users ||--o{ applications : "submits"
jobs ||--o{ job_payments : "has payment"
companies ||--o{ user_companies : "has members"
companies ||--o{ jobs : "posts"
users ||--o{ jobs : "creates"
jobs ||--o{ applications : "receives"
users ||--o{ applications : "submits"
regions ||--o{ cities : "contains"
regions ||--o{ companies : "located in"
cities ||--o{ companies : "located in"

View file

@ -24,7 +24,7 @@
"@radix-ui/react-navigation-menu": "1.2.3",
"@radix-ui/react-popover": "1.1.4",
"@radix-ui/react-progress": "1.1.1",
"@radix-ui/react-radio-group": "1.2.2",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-scroll-area": "1.2.2",
"@radix-ui/react-select": "2.1.4",
"@radix-ui/react-separator": "1.1.1",

View file

@ -26,7 +26,7 @@
"@radix-ui/react-navigation-menu": "1.2.3",
"@radix-ui/react-popover": "1.1.4",
"@radix-ui/react-progress": "1.1.1",
"@radix-ui/react-radio-group": "1.2.2",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-scroll-area": "1.2.2",
"@radix-ui/react-select": "2.1.4",
"@radix-ui/react-separator": "1.1.1",

View file

@ -8,7 +8,10 @@ import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { ArrowLeft, ArrowRight, Check, Loader2, Building2, DollarSign, FileText, Eye } from "lucide-react"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { ArrowLeft, ArrowRight, Check, Loader2, Building2, DollarSign, FileText, Eye, CreditCard, Receipt, MapPin, Clock, Briefcase } from "lucide-react"
import { jobsApi, adminCompaniesApi, type CreateJobPayload, type AdminCompany } from "@/lib/api"
import { toast } from "sonner"
import { cn } from "@/lib/utils"
@ -17,9 +20,14 @@ const STEPS = [
{ id: 1, title: "Job Details", icon: FileText },
{ id: 2, title: "Salary & Type", icon: DollarSign },
{ id: 3, title: "Company", icon: Building2 },
{ id: 4, title: "Review", icon: Eye },
{ id: 4, title: "Preview", icon: Eye },
{ id: 5, title: "Billing", icon: Receipt },
{ id: 6, title: "Payment", icon: CreditCard },
]
const JOB_POSTING_PRICE = 32.61
const FEATURED_PRICE = 15.00
export default function NewJobPage() {
const router = useRouter()
const [currentStep, setCurrentStep] = useState(1)
@ -28,16 +36,36 @@ export default function NewJobPage() {
const [loadingCompanies, setLoadingCompanies] = useState(true)
const [formData, setFormData] = useState({
// Job Details
title: "",
description: "",
companyId: "",
location: "",
employmentType: "",
// Salary
salaryMin: "",
salaryMax: "",
salaryType: "",
salaryType: "monthly",
currency: "BRL",
employmentType: "",
workingHours: "",
// Company
companyId: "",
// Options
isFeatured: false,
})
const [billingData, setBillingData] = useState({
type: "company" as "company" | "individual",
firstName: "",
lastName: "",
email: "",
phone: "",
companyName: "",
address: "",
addressLine2: "",
city: "",
state: "",
zip: "",
country: "BR",
})
useEffect(() => {
@ -56,31 +84,39 @@ export default function NewJobPage() {
loadCompanies()
}, [])
const updateField = (field: string, value: string) => {
const updateField = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const updateBilling = (field: string, value: string) => {
setBillingData(prev => ({ ...prev, [field]: value }))
}
const selectedCompany = companies.find(c => c.id === formData.companyId)
const totalPrice = JOB_POSTING_PRICE + (formData.isFeatured ? FEATURED_PRICE : 0)
const canProceed = () => {
switch (currentStep) {
case 1:
return formData.title.length >= 5 && formData.description.length >= 20
case 2:
return true // Salary is optional
return true
case 3:
return formData.companyId !== ""
case 4:
return true // Preview is always valid
case 5:
return billingData.firstName && billingData.lastName && billingData.email &&
(billingData.type === "individual" || billingData.companyName)
default:
return true
}
}
const handleSubmit = async () => {
if (!formData.companyId || !formData.title || !formData.description) {
toast.error("Please fill in all required fields")
return
}
const handlePayment = async () => {
setIsSubmitting(true)
try {
// First create the job as draft
const payload: CreateJobPayload = {
companyId: formData.companyId,
title: formData.title,
@ -92,23 +128,26 @@ export default function NewJobPage() {
salaryType: formData.salaryType as CreateJobPayload['salaryType'] || undefined,
currency: formData.currency as CreateJobPayload['currency'] || undefined,
workingHours: formData.workingHours || undefined,
status: "published",
status: "draft", // Will be published after payment
}
console.log("[DEBUG] Submitting job:", payload)
await jobsApi.create(payload)
toast.success("Job posted successfully!")
console.log("[DEBUG] Creating draft job:", payload)
const job = await jobsApi.create(payload)
// TODO: Create Stripe checkout session and redirect
// For now, simulate success
toast.success("Job created! Payment integration coming soon.")
router.push("/dashboard/jobs")
} catch (error) {
console.error("Failed to create job:", error)
toast.error("Failed to post job. Please try again.")
toast.error("Failed to create job. Please try again.")
} finally {
setIsSubmitting(false)
}
}
const nextStep = () => {
if (currentStep < 4 && canProceed()) {
if (currentStep < 6 && canProceed()) {
setCurrentStep(prev => prev + 1)
}
}
@ -119,28 +158,33 @@ export default function NewJobPage() {
}
}
const getCurrencySymbol = (currency: string) => {
const symbols: Record<string, string> = { BRL: "R$", USD: "$", EUR: "€", GBP: "£", JPY: "¥" }
return symbols[currency] || currency
}
return (
<div className="max-w-3xl mx-auto space-y-8">
<div className="max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="text-3xl font-bold">Post a new job</h1>
<p className="text-muted-foreground">Fill in the details for your job listing</p>
<h1 className="text-3xl font-bold">Post a job</h1>
<p className="text-muted-foreground">Create your job listing in a few steps</p>
</div>
</div>
{/* Progress Steps */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between overflow-x-auto pb-2">
{STEPS.map((step, index) => {
const Icon = step.icon
const isActive = currentStep === step.id
const isCompleted = currentStep > step.id
return (
<div key={step.id} className="flex items-center">
<div key={step.id} className="flex items-center flex-shrink-0">
<div className="flex flex-col items-center">
<div
className={cn(
@ -152,11 +196,11 @@ export default function NewJobPage() {
: "border-muted-foreground/30 text-muted-foreground"
)}
>
{isCompleted ? <Check className="h-5 w-5" /> : <Icon className="h-5 w-5" />}
{isCompleted ? <Check className="h-5 w-5" /> : <Icon className="h-4 w-4" />}
</div>
<span
className={cn(
"mt-2 text-xs font-medium",
"mt-2 text-xs font-medium whitespace-nowrap",
isActive ? "text-primary" : "text-muted-foreground"
)}
>
@ -166,7 +210,7 @@ export default function NewJobPage() {
{index < STEPS.length - 1 && (
<div
className={cn(
"h-0.5 w-16 mx-2 transition-colors",
"h-0.5 w-8 md:w-12 mx-1 transition-colors flex-shrink-0",
currentStep > step.id ? "bg-primary" : "bg-muted-foreground/30"
)}
/>
@ -182,9 +226,11 @@ export default function NewJobPage() {
<CardTitle>{STEPS[currentStep - 1].title}</CardTitle>
<CardDescription>
{currentStep === 1 && "Enter the basic information about this job"}
{currentStep === 2 && "Set the compensation details"}
{currentStep === 2 && "Set the compensation and employment details"}
{currentStep === 3 && "Select the company posting this job"}
{currentStep === 4 && "Review your job listing before publishing"}
{currentStep === 4 && "Preview how your job will appear"}
{currentStep === 5 && "Enter your billing information"}
{currentStep === 6 && "Complete your payment to publish"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
@ -233,9 +279,8 @@ export default function NewJobPage() {
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="salaryMin">Minimum Salary</Label>
<Label>Minimum Salary</Label>
<Input
id="salaryMin"
type="number"
placeholder="e.g. 5000"
value={formData.salaryMin}
@ -243,9 +288,8 @@ export default function NewJobPage() {
/>
</div>
<div className="space-y-2">
<Label htmlFor="salaryMax">Maximum Salary</Label>
<Label>Maximum Salary</Label>
<Input
id="salaryMax"
type="number"
placeholder="e.g. 10000"
value={formData.salaryMax}
@ -257,9 +301,7 @@ export default function NewJobPage() {
<div className="space-y-2">
<Label>Currency</Label>
<Select value={formData.currency} onValueChange={(v) => updateField("currency", v)}>
<SelectTrigger>
<SelectValue placeholder="Currency" />
</SelectTrigger>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="BRL">R$ (BRL)</SelectItem>
<SelectItem value="USD">$ (USD)</SelectItem>
@ -272,9 +314,7 @@ export default function NewJobPage() {
<div className="space-y-2">
<Label>Salary Period</Label>
<Select value={formData.salaryType} onValueChange={(v) => updateField("salaryType", v)}>
<SelectTrigger>
<SelectValue placeholder="Select period" />
</SelectTrigger>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="hourly">Per hour</SelectItem>
<SelectItem value="daily">Per day</SelectItem>
@ -287,9 +327,7 @@ export default function NewJobPage() {
<div className="space-y-2">
<Label>Contract Type</Label>
<Select value={formData.employmentType} onValueChange={(v) => updateField("employmentType", v)}>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
<SelectContent>
<SelectItem value="permanent">Permanent</SelectItem>
<SelectItem value="full-time">Full-time</SelectItem>
@ -298,15 +336,13 @@ export default function NewJobPage() {
<SelectItem value="temporary">Temporary</SelectItem>
<SelectItem value="training">Training</SelectItem>
<SelectItem value="voluntary">Voluntary</SelectItem>
<SelectItem value="dispatch">Dispatch</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="workingHours">Working Hours</Label>
<Label>Working Hours</Label>
<Input
id="workingHours"
placeholder="e.g. 9:00 - 18:00, Mon-Fri"
value={formData.workingHours}
onChange={(e) => updateField("workingHours", e.target.value)}
@ -326,9 +362,7 @@ export default function NewJobPage() {
</div>
) : (
<Select value={formData.companyId} onValueChange={(v) => updateField("companyId", v)}>
<SelectTrigger>
<SelectValue placeholder="Select a company" />
</SelectTrigger>
<SelectTrigger><SelectValue placeholder="Select a company" /></SelectTrigger>
<SelectContent>
{companies.length === 0 ? (
<SelectItem value="__none" disabled>No companies available</SelectItem>
@ -345,45 +379,259 @@ export default function NewJobPage() {
</div>
)}
{/* Step 4: Review */}
{/* Step 4: Preview */}
{currentStep === 4 && (
<div className="space-y-4">
<div className="space-y-6">
{/* Job Preview Card */}
<div className="border rounded-lg p-6 space-y-4">
<div>
<h2 className="text-2xl font-bold">{formData.title || "Job Title"}</h2>
<p className="text-muted-foreground">{selectedCompany?.name || "Company Name"}</p>
</div>
<div className="flex flex-wrap gap-4 text-sm">
{formData.location && (
<div className="flex items-center gap-1 text-muted-foreground">
<MapPin className="h-4 w-4" />
{formData.location}
</div>
)}
{(formData.salaryMin || formData.salaryMax) && (
<div className="flex items-center gap-1 text-muted-foreground">
<DollarSign className="h-4 w-4" />
{getCurrencySymbol(formData.currency)}{formData.salaryMin || "?"} - {formData.salaryMax || "?"} {formData.salaryType}
</div>
)}
{formData.employmentType && (
<Badge variant="secondary" className="capitalize">
{formData.employmentType.replace("-", " ")}
</Badge>
)}
{formData.workingHours && (
<div className="flex items-center gap-1 text-muted-foreground">
<Clock className="h-4 w-4" />
{formData.workingHours}
</div>
)}
</div>
<Separator />
<div>
<p className="text-sm whitespace-pre-wrap">{formData.description || "Job description will appear here..."}</p>
</div>
{selectedCompany && (
<>
<Separator />
<div className="flex items-center gap-3 p-3 bg-muted rounded-lg">
<Building2 className="h-10 w-10 text-muted-foreground" />
<div>
<p className="font-medium">{selectedCompany.name}</p>
<p className="text-sm text-muted-foreground">View company profile</p>
</div>
</div>
</>
)}
</div>
<div className="text-center space-y-2 p-4 bg-green-50 dark:bg-green-950 rounded-lg border border-green-200 dark:border-green-800">
<p className="text-green-700 dark:text-green-400 font-medium">Your job offer is ready to be posted!</p>
</div>
<div className="flex items-center gap-2 p-4 border rounded-lg">
<input
type="checkbox"
id="featured"
checked={formData.isFeatured}
onChange={(e) => updateField("isFeatured", e.target.checked)}
className="h-4 w-4"
/>
<Label htmlFor="featured" className="flex-1 cursor-pointer">
<span className="font-medium">Feature this job</span>
<span className="text-muted-foreground text-sm block">
Your job will be highlighted and appear at the top of search results (+${FEATURED_PRICE.toFixed(2)})
</span>
</Label>
</div>
</div>
)}
{/* Step 5: Billing */}
{currentStep === 5 && (
<div className="space-y-6">
<div className="space-y-3">
<Label>You are *</Label>
<RadioGroup
value={billingData.type}
onValueChange={(v) => updateBilling("type", v)}
className="space-y-2"
>
<div className="flex items-center space-x-2 p-3 border rounded-lg">
<RadioGroupItem value="company" id="company" />
<Label htmlFor="company" className="flex-1 cursor-pointer">Company</Label>
</div>
<div className="flex items-center space-x-2 p-3 border rounded-lg">
<RadioGroupItem value="individual" id="individual" />
<Label htmlFor="individual" className="flex-1 cursor-pointer">Individual</Label>
</div>
</RadioGroup>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Job Title</p>
<p className="font-medium">{formData.title || "-"}</p>
<div className="space-y-2">
<Label>First name *</Label>
<Input
value={billingData.firstName}
onChange={(e) => updateBilling("firstName", e.target.value)}
/>
</div>
<div>
<p className="text-sm text-muted-foreground">Company</p>
<p className="font-medium">
{companies.find(c => c.id === formData.companyId)?.name || "-"}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Location</p>
<p className="font-medium">{formData.location || "-"}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Employment Type</p>
<p className="font-medium">{formData.employmentType || "-"}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Salary</p>
<p className="font-medium">
{formData.salaryMin || formData.salaryMax
? `${formData.currency} ${formData.salaryMin || "?"} - ${formData.salaryMax || "?"} ${formData.salaryType ? `(${formData.salaryType})` : ""}`
: "-"}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Working Hours</p>
<p className="font-medium">{formData.workingHours || "-"}</p>
<div className="space-y-2">
<Label>Last name *</Label>
<Input
value={billingData.lastName}
onChange={(e) => updateBilling("lastName", e.target.value)}
/>
</div>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">Description</p>
<div className="p-3 bg-muted rounded-md text-sm whitespace-pre-wrap max-h-40 overflow-y-auto">
{formData.description || "-"}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Email *</Label>
<Input
type="email"
value={billingData.email}
onChange={(e) => updateBilling("email", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Phone</Label>
<Input
value={billingData.phone}
onChange={(e) => updateBilling("phone", e.target.value)}
/>
</div>
</div>
{billingData.type === "company" && (
<div className="space-y-2">
<Label>Company name *</Label>
<Input
value={billingData.companyName}
onChange={(e) => updateBilling("companyName", e.target.value)}
/>
</div>
)}
<div className="space-y-2">
<Label>Address *</Label>
<Input
placeholder="Address line 1"
value={billingData.address}
onChange={(e) => updateBilling("address", e.target.value)}
/>
<Input
placeholder="Address line 2 (optional)"
value={billingData.addressLine2}
onChange={(e) => updateBilling("addressLine2", e.target.value)}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label>City *</Label>
<Input
value={billingData.city}
onChange={(e) => updateBilling("city", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>State *</Label>
<Input
value={billingData.state}
onChange={(e) => updateBilling("state", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Zip *</Label>
<Input
value={billingData.zip}
onChange={(e) => updateBilling("zip", e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label>Country *</Label>
<Select value={billingData.country} onValueChange={(v) => updateBilling("country", v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="BR">Brazil</SelectItem>
<SelectItem value="US">United States</SelectItem>
<SelectItem value="GB">United Kingdom</SelectItem>
<SelectItem value="DE">Germany</SelectItem>
<SelectItem value="JP">Japan</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
{/* Step 6: Payment */}
{currentStep === 6 && (
<div className="space-y-6">
<div className="flex gap-2">
<Badge variant="outline" className="px-3 py-1">
<span className="text-blue-600 font-bold">VISA</span>
</Badge>
<Badge variant="outline" className="px-3 py-1 bg-red-50">
<span className="text-red-600 font-bold">MC</span>
</Badge>
<Badge variant="outline" className="px-3 py-1 bg-blue-50">
<span className="text-blue-800 font-bold">AMEX</span>
</Badge>
</div>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Your order</span>
<span>1 job posting</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Duration</span>
<span>30 days</span>
</div>
{formData.isFeatured && (
<div className="flex justify-between text-green-600">
<span>Featured upgrade</span>
<span>+${FEATURED_PRICE.toFixed(2)}</span>
</div>
)}
<Separator />
<div className="flex justify-between text-lg font-bold">
<span>Total amount due</span>
<span>${totalPrice.toFixed(2)}</span>
</div>
</div>
<div className="space-y-4 p-4 border rounded-lg">
<p className="text-center text-muted-foreground">
You will be redirected to Stripe to complete your payment securely.
</p>
</div>
<div className="space-y-2 text-sm text-muted-foreground">
<div className="flex items-start gap-2">
<Check className="h-4 w-4 text-green-600 mt-0.5" />
<span>Once you have paid, your job posting will be online within a few minutes!</span>
</div>
<div className="flex items-start gap-2">
<Check className="h-4 w-4 text-green-600 mt-0.5" />
<span>You will be able to modify the job description at any time</span>
</div>
<div className="flex items-start gap-2">
<Check className="h-4 w-4 text-green-600 mt-0.5" />
<span>Your job ad will be visible for 30 days</span>
</div>
</div>
</div>
@ -398,22 +646,22 @@ export default function NewJobPage() {
Back
</Button>
{currentStep < 4 ? (
{currentStep < 6 ? (
<Button onClick={nextStep} disabled={!canProceed()}>
Next
{currentStep === 4 ? `Pay to post - $${totalPrice.toFixed(2)}` : "Continue"}
<ArrowRight className="h-4 w-4 ml-2" />
</Button>
) : (
<Button onClick={handleSubmit} disabled={isSubmitting}>
<Button onClick={handlePayment} disabled={isSubmitting} size="lg" className="gap-2">
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Publishing...
<Loader2 className="h-4 w-4 animate-spin" />
Processing...
</>
) : (
<>
<Check className="h-4 w-4 mr-2" />
Publish Job
<CreditCard className="h-4 w-4" />
Pay ${totalPrice.toFixed(2)}
</>
)}
</Button>

View file

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }