Skip to content

Commit aef38a7

Browse files
authored
Merge pull request #850 from ndii-dev/feat/issue-828-skills-taxonomy
feat(frontend): add skills taxonomy and auto-suggest combobox for bounty tags
2 parents b8d00b1 + 922d2d0 commit aef38a7

12 files changed

Lines changed: 1036 additions & 31 deletions

File tree

app/api/skills/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { searchSkills } from '@/lib/skills-taxonomy'
3+
4+
export const runtime = 'edge'
5+
6+
export async function GET(request: NextRequest) {
7+
const { searchParams } = new URL(request.url)
8+
const q = searchParams.get('q') ?? ''
9+
const limit = Math.min(parseInt(searchParams.get('limit') ?? '8', 10), 20)
10+
11+
const skills = searchSkills(q, limit)
12+
return NextResponse.json({ skills, total: skills.length, query: q })
13+
}

app/api/testimonials/[id]/route.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { getStore } from '../route';
3+
4+
export async function PATCH(
5+
request: NextRequest,
6+
{ params }: { params: { id: string } },
7+
) {
8+
const { id } = params;
9+
const body = await request.json().catch(() => null);
10+
11+
if (!body || typeof body.featured !== 'boolean') {
12+
return NextResponse.json(
13+
{ error: '`featured` (boolean) is required in the request body' },
14+
{ status: 400 },
15+
);
16+
}
17+
18+
const store = getStore();
19+
const testimonial = store.find((t) => t.id === id);
20+
21+
if (!testimonial) {
22+
return NextResponse.json({ error: 'Testimonial not found' }, { status: 404 });
23+
}
24+
25+
testimonial.featured = body.featured;
26+
27+
return NextResponse.json({ testimonial });
28+
}

app/api/testimonials/route.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import type { Testimonial } from '@/components/testimonials';
3+
4+
// In-memory store (mirrors component data; replace with Prisma in production)
5+
const store: Testimonial[] = [
6+
{
7+
id: '1',
8+
clientId: 'client-001',
9+
creatorId: 'creator-001',
10+
bountyId: 'bounty-101',
11+
author: 'Sarah Chen',
12+
role: 'Product Manager, TechStartup',
13+
quote:
14+
'Stellar connected us with incredible designers who transformed our product. The process was seamless and professional.',
15+
rating: 5,
16+
featured: true,
17+
createdAt: '2024-11-12T10:00:00Z',
18+
bountyTitle: 'Dashboard UI Redesign',
19+
creatorProfile: { name: 'Alex Rivera', slug: 'alex-rivera' },
20+
},
21+
{
22+
id: '2',
23+
clientId: 'client-002',
24+
creatorId: 'creator-002',
25+
bountyId: 'bounty-102',
26+
author: 'Marcus Johnson',
27+
role: 'UI/UX Designer',
28+
quote:
29+
'As a freelancer, this platform gave me access to high-quality projects and clients who truly value creative work.',
30+
rating: 5,
31+
featured: true,
32+
createdAt: '2024-12-01T14:30:00Z',
33+
bountyTitle: 'Mobile App Prototype',
34+
creatorProfile: { name: 'Jordan Kim', slug: 'jordan-kim' },
35+
videoUrl: 'https://assets.stellar.app/testimonials/marcus-johnson.mp4',
36+
},
37+
{
38+
id: '3',
39+
clientId: 'client-003',
40+
creatorId: 'creator-003',
41+
bountyId: 'bounty-103',
42+
author: 'Emily Rodriguez',
43+
role: 'Marketing Director, GrowthCo',
44+
quote:
45+
'The bounty system is revolutionary. We found the perfect content creator for our campaign in days, not weeks.',
46+
rating: 5,
47+
featured: true,
48+
createdAt: '2025-01-08T09:15:00Z',
49+
bountyTitle: 'Brand Campaign Content',
50+
creatorProfile: { name: 'Sam Okafor', slug: 'sam-okafor' },
51+
},
52+
];
53+
54+
export function getStore(): Testimonial[] {
55+
return store;
56+
}
57+
58+
export async function GET(request: NextRequest) {
59+
const { searchParams } = new URL(request.url);
60+
const featuredParam = searchParams.get('featured');
61+
const limit = Math.min(parseInt(searchParams.get('limit') ?? '10', 10), 50);
62+
63+
let results = [...store];
64+
65+
if (featuredParam === 'true') {
66+
results = results.filter((t) => t.featured);
67+
} else if (featuredParam === 'false') {
68+
results = results.filter((t) => !t.featured);
69+
}
70+
71+
results = results.slice(0, limit);
72+
73+
return NextResponse.json({ testimonials: results, total: results.length });
74+
}

components/testimonials.tsx

Lines changed: 133 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,171 @@
11
'use client';
22

3+
import Link from 'next/link';
34
import { Star } from 'lucide-react';
45

5-
interface Testimonial {
6+
export interface Testimonial {
67
id: string;
7-
text: string;
8+
clientId: string;
9+
creatorId: string;
10+
bountyId?: string;
11+
/** Display name of the client/author */
812
author: string;
913
role: string;
14+
quote: string;
1015
rating: number;
16+
featured: boolean;
17+
createdAt: string;
18+
creatorProfile?: { name: string; avatar?: string; slug: string };
19+
bountyTitle?: string;
20+
/** S3 URL for a video testimonial */
21+
videoUrl?: string;
1122
}
1223

1324
const testimonials: Testimonial[] = [
1425
{
1526
id: '1',
16-
text: 'Stellar connected us with incredible designers who transformed our product. The process was seamless and professional.',
27+
clientId: 'client-001',
28+
creatorId: 'creator-001',
29+
bountyId: 'bounty-101',
1730
author: 'Sarah Chen',
1831
role: 'Product Manager, TechStartup',
32+
quote:
33+
'Stellar connected us with incredible designers who transformed our product. The process was seamless and professional.',
1934
rating: 5,
35+
featured: true,
36+
createdAt: '2024-11-12T10:00:00Z',
37+
bountyTitle: 'Dashboard UI Redesign',
38+
creatorProfile: { name: 'Alex Rivera', slug: 'alex-rivera' },
2039
},
2140
{
2241
id: '2',
23-
text: 'As a freelancer, this platform gave me access to high-quality projects and clients who truly value creative work.',
42+
clientId: 'client-002',
43+
creatorId: 'creator-002',
44+
bountyId: 'bounty-102',
2445
author: 'Marcus Johnson',
2546
role: 'UI/UX Designer',
47+
quote:
48+
'As a freelancer, this platform gave me access to high-quality projects and clients who truly value creative work.',
2649
rating: 5,
50+
featured: true,
51+
createdAt: '2024-12-01T14:30:00Z',
52+
bountyTitle: 'Mobile App Prototype',
53+
creatorProfile: { name: 'Jordan Kim', slug: 'jordan-kim' },
54+
videoUrl: 'https://assets.stellar.app/testimonials/marcus-johnson.mp4',
2755
},
2856
{
2957
id: '3',
30-
text: 'The bounty system is revolutionary. We found the perfect content creator for our campaign in days, not weeks.',
58+
clientId: 'client-003',
59+
creatorId: 'creator-003',
60+
bountyId: 'bounty-103',
3161
author: 'Emily Rodriguez',
3262
role: 'Marketing Director, GrowthCo',
63+
quote:
64+
'The bounty system is revolutionary. We found the perfect content creator for our campaign in days, not weeks.',
3365
rating: 5,
66+
featured: true,
67+
createdAt: '2025-01-08T09:15:00Z',
68+
bountyTitle: 'Brand Campaign Content',
69+
creatorProfile: { name: 'Sam Okafor', slug: 'sam-okafor' },
3470
},
3571
];
3672

37-
export function TestimonialsSection() {
73+
/** Initials avatar fallback */
74+
function InitialsAvatar({ name }: { name: string }) {
75+
const initials = name
76+
.split(' ')
77+
.map((w) => w[0])
78+
.slice(0, 2)
79+
.join('')
80+
.toUpperCase();
81+
return (
82+
<span className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-accent/20 text-accent font-semibold text-sm select-none">
83+
{initials}
84+
</span>
85+
);
86+
}
87+
88+
function CaseStudyCard({ testimonial }: { testimonial: Testimonial }) {
89+
return (
90+
<div className="flex flex-col bg-card border border-border rounded-lg p-8 hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
91+
{/* Rating */}
92+
<div className="flex gap-1 mb-4">
93+
{Array.from({ length: 5 }).map((_, i) => (
94+
<Star
95+
key={i}
96+
size={18}
97+
className={i < testimonial.rating ? 'fill-accent text-accent' : 'text-muted-foreground'}
98+
/>
99+
))}
100+
</div>
101+
102+
{/* Quote */}
103+
<p className="text-foreground mb-4 italic leading-relaxed flex-1">
104+
&ldquo;{testimonial.quote}&rdquo;
105+
</p>
106+
107+
{/* Optional video testimonial */}
108+
{testimonial.videoUrl && (
109+
<div className="mb-4 rounded-md overflow-hidden border border-border">
110+
<video
111+
src={testimonial.videoUrl}
112+
controls
113+
muted
114+
playsInline
115+
className="w-full max-h-48 object-cover"
116+
aria-label={`Video testimonial from ${testimonial.author}`}
117+
/>
118+
</div>
119+
)}
120+
121+
{/* Author */}
122+
<div className="flex items-center gap-3 mb-4">
123+
<InitialsAvatar name={testimonial.author} />
124+
<div>
125+
<p className="font-semibold text-foreground leading-tight">{testimonial.author}</p>
126+
<p className="text-sm text-muted-foreground">{testimonial.role}</p>
127+
</div>
128+
</div>
129+
130+
{/* Links */}
131+
<div className="flex flex-wrap gap-3 text-sm mt-auto">
132+
{testimonial.creatorProfile && (
133+
<Link
134+
href={`/creators/${testimonial.creatorProfile.slug}`}
135+
className="text-accent hover:underline font-medium"
136+
>
137+
View creator &rarr;
138+
</Link>
139+
)}
140+
{testimonial.bountyId && (
141+
<Link
142+
href={`/bounties/${testimonial.bountyId}`}
143+
className="text-muted-foreground hover:text-foreground hover:underline"
144+
>
145+
View bounty &rarr;
146+
</Link>
147+
)}
148+
</div>
149+
</div>
150+
);
151+
}
152+
153+
interface TestimonialsSectionProps {
154+
featuredOnly?: boolean;
155+
}
156+
157+
export function TestimonialsSection({ featuredOnly = true }: TestimonialsSectionProps) {
158+
const displayed = featuredOnly
159+
? testimonials.filter((t) => t.featured)
160+
: testimonials;
161+
38162
return (
39163
<section className="py-20 sm:py-32 bg-muted/30 border-b border-border">
40164
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
41165
{/* Section Header */}
42166
<div className="text-center mb-16">
43167
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-foreground mb-4">
44-
Loved by Creators & Clients
168+
Loved by Creators &amp; Clients
45169
</h2>
46170
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
47171
See what industry leaders are saying about Stellar
@@ -50,29 +174,8 @@ export function TestimonialsSection() {
50174

51175
{/* Testimonials Grid */}
52176
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
53-
{testimonials.map((testimonial) => (
54-
<div
55-
key={testimonial.id}
56-
className="bg-card border border-border rounded-lg p-8 hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
57-
>
58-
{/* Rating */}
59-
<div className="flex gap-1 mb-4">
60-
{Array.from({ length: testimonial.rating }).map((_, i) => (
61-
<Star key={i} size={18} className="fill-accent text-accent" />
62-
))}
63-
</div>
64-
65-
{/* Quote */}
66-
<p className="text-foreground mb-6 italic leading-relaxed">
67-
"{testimonial.text}"
68-
</p>
69-
70-
{/* Author */}
71-
<div>
72-
<p className="font-semibold text-foreground">{testimonial.author}</p>
73-
<p className="text-sm text-muted-foreground">{testimonial.role}</p>
74-
</div>
75-
</div>
177+
{displayed.map((testimonial) => (
178+
<CaseStudyCard key={testimonial.id} testimonial={testimonial} />
76179
))}
77180
</div>
78181
</div>

0 commit comments

Comments
 (0)