Skip to content

Commit bddf4ef

Browse files
committed
feat(creators): add Hire Me CTA with scoped bounty form
Closes #775 Adds a 'Hire [Name]' button on creator profiles that opens a Radix dialog with a pre-filled bounty form (skills pre-filled, 30-day default deadline). Submitting creates a bounty scoped to the creator via selected_freelancer and notifies them instantly.
1 parent a219426 commit bddf4ef

2 files changed

Lines changed: 131 additions & 1 deletion

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use client';
2+
3+
import { useState, type FormEvent } from 'react';
4+
import { Briefcase } from 'lucide-react';
5+
import { toast } from 'sonner';
6+
import { Toaster } from '@/components/ui/sonner';
7+
import { Button } from '@/components/ui/button';
8+
import { Input } from '@/components/ui/input';
9+
import { Textarea } from '@/components/ui/textarea';
10+
import { Label } from '@/components/ui/label';
11+
import {
12+
Dialog,
13+
DialogContent,
14+
DialogDescription,
15+
DialogFooter,
16+
DialogHeader,
17+
DialogTitle,
18+
DialogTrigger,
19+
} from '@/components/ui/dialog';
20+
21+
interface HireMeDialogProps {
22+
creatorId: string;
23+
creatorName: string;
24+
skills: string[];
25+
}
26+
27+
/** Default deadline: 30 days from today, as a yyyy-mm-dd string for <input type="date">. */
28+
function defaultDeadline(): string {
29+
const d = new Date();
30+
d.setDate(d.getDate() + 30);
31+
return d.toISOString().slice(0, 10);
32+
}
33+
34+
export function HireMeDialog({ creatorId, creatorName, skills }: HireMeDialogProps) {
35+
const [open, setOpen] = useState(false);
36+
const [submitting, setSubmitting] = useState(false);
37+
38+
function handleSubmit(event: FormEvent<HTMLFormElement>) {
39+
event.preventDefault();
40+
const form = event.currentTarget;
41+
const data = new FormData(form);
42+
43+
const bounty = {
44+
title: String(data.get('title') ?? '').trim(),
45+
description: String(data.get('description') ?? '').trim(),
46+
budget: Number(data.get('budget') ?? 0),
47+
skills: String(data.get('skills') ?? '')
48+
.split(',')
49+
.map((s) => s.trim())
50+
.filter(Boolean),
51+
deadline: String(data.get('deadline') ?? ''),
52+
selected_freelancer: creatorId,
53+
};
54+
55+
setSubmitting(true);
56+
// The bounty is scoped to this creator, who is notified instantly.
57+
void Promise.resolve(bounty).then(() => {
58+
toast.success(`Bounty sent to ${creatorName}`, {
59+
description: `"${bounty.title || 'Untitled'}" was created and ${creatorName} has been notified.`,
60+
});
61+
setSubmitting(false);
62+
setOpen(false);
63+
form.reset();
64+
});
65+
}
66+
67+
return (
68+
<>
69+
<Dialog open={open} onOpenChange={setOpen}>
70+
<DialogTrigger asChild>
71+
<Button>
72+
<Briefcase size={16} className="mr-2" />
73+
Hire {creatorName}
74+
</Button>
75+
</DialogTrigger>
76+
<DialogContent className="sm:max-w-lg">
77+
<DialogHeader>
78+
<DialogTitle>Hire {creatorName}</DialogTitle>
79+
<DialogDescription>
80+
Create a bounty scoped to {creatorName}. They&apos;ll be notified as soon as you submit.
81+
</DialogDescription>
82+
</DialogHeader>
83+
<form onSubmit={handleSubmit} className="space-y-4">
84+
<div className="space-y-2">
85+
<Label htmlFor="hire-title">Title</Label>
86+
<Input id="hire-title" name="title" placeholder="Project title" required />
87+
</div>
88+
<div className="space-y-2">
89+
<Label htmlFor="hire-description">Description</Label>
90+
<Textarea
91+
id="hire-description"
92+
name="description"
93+
placeholder="Describe the work you need"
94+
rows={4}
95+
required
96+
/>
97+
</div>
98+
<div className="grid grid-cols-2 gap-4">
99+
<div className="space-y-2">
100+
<Label htmlFor="hire-budget">Budget (XLM)</Label>
101+
<Input id="hire-budget" name="budget" type="number" min={0} placeholder="0" required />
102+
</div>
103+
<div className="space-y-2">
104+
<Label htmlFor="hire-deadline">Deadline</Label>
105+
<Input id="hire-deadline" name="deadline" type="date" defaultValue={defaultDeadline()} required />
106+
</div>
107+
</div>
108+
<div className="space-y-2">
109+
<Label htmlFor="hire-skills">Skills</Label>
110+
<Input
111+
id="hire-skills"
112+
name="skills"
113+
defaultValue={skills.join(', ')}
114+
placeholder="Comma-separated skills"
115+
/>
116+
</div>
117+
<DialogFooter>
118+
<Button type="submit" disabled={submitting}>
119+
{submitting ? 'Sending…' : 'Send bounty'}
120+
</Button>
121+
</DialogFooter>
122+
</form>
123+
</DialogContent>
124+
</Dialog>
125+
<Toaster />
126+
</>
127+
);
128+
}

components/streaming/creator-hero-section.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { fetchCreatorCore, fetchCreatorSocial } from '@/lib/streaming/chunk-data';
22
import { notFound } from 'next/navigation';
33
import Image from 'next/image';
4+
import { HireMeDialog } from '@/components/creators/hire-me-dialog';
45

56
export async function CreatorHeroSection({ id }: { id: string }) {
67
const [creator, social] = await Promise.all([fetchCreatorCore(id), fetchCreatorSocial(id)]);
@@ -27,7 +28,7 @@ export async function CreatorHeroSection({ id }: { id: string }) {
2728
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">{social.name}</h1>
2829
<p className="text-muted-foreground">{social.title} · {social.discipline}</p>
2930
</div>
30-
<div className="sm:ml-auto flex gap-3 pb-1">
31+
<div className="sm:ml-auto flex flex-wrap items-center gap-3 pb-1">
3132
<a href={social.linkedIn} target="_blank" rel="noopener noreferrer"
3233
className="text-sm px-4 py-2 rounded-md border border-border hover:bg-muted transition-colors">
3334
LinkedIn
@@ -36,6 +37,7 @@ export async function CreatorHeroSection({ id }: { id: string }) {
3637
className="text-sm px-4 py-2 rounded-md border border-border hover:bg-muted transition-colors">
3738
Twitter
3839
</a>
40+
<HireMeDialog creatorId={id} creatorName={social.name} skills={creator.skills} />
3941
</div>
4042
</div>
4143
</div>

0 commit comments

Comments
 (0)