initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
export const metadata = {
title: 'Billing | Broadcast Beat Marketplace',
};
export default function BillingPage() {
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-3xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
<Link href="/marketplace" className="hover:text-[#CC0000]">
Marketplace
</Link>
<span>/</span>
<Link href="/marketplace/account" className="hover:text-[#CC0000]">
Account
</Link>
<span>/</span>
<span className="text-white">Billing</span>
</div>
<h1 className="text-3xl font-black uppercase tracking-wide mb-4">Billing & Subscription</h1>
{/* Coming soon banner */}
<div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent border border-[#CC0000]/30 rounded-2xl p-8 mb-10 text-center">
<div className="text-4xl mb-4">💳</div>
<h2 className="text-2xl font-bold mb-3">Payments Coming Soon</h2>
<p className="text-gray-400 max-w-md mx-auto leading-relaxed">
We&apos;re finalizing our payment integration. Pro and Featured subscriptions will be
available shortly. Your free profile is already live and ready.
</p>
</div>
{/* Plan comparison */}
<h2 className="text-xl font-bold mb-6">Available Plans</h2>
<div className="grid md:grid-cols-3 gap-6 mb-10">
{[
{
name: 'Free',
price: '$0',
period: 'forever',
current: true,
features: [
'Basic profile',
'Appear in search',
'3 gig responses/month',
'Apply to unlimited jobs',
],
},
{
name: 'Pro',
price: '$9.99',
period: '/month',
current: false,
highlight: true,
features: [
'Priority placement',
'Unlimited gig responses',
'Post jobs & gigs',
'Profile analytics',
'Pro Member badge',
],
},
{
name: 'Studio / Vendor',
price: '$29.99',
period: '/month',
current: false,
features: [
'Everything in Pro',
'Company profile',
'Up to 5 staff profiles',
'Featured placement',
'Priority lead routing',
],
},
]?.map((plan) => (
<div
key={plan?.name}
className={`rounded-xl p-6 border relative ${
plan?.highlight
? 'bg-[#CC0000]/10 border-[#CC0000]'
: 'bg-[#1a1a1a] border-white/10'
}`}
>
{plan?.highlight && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-[#CC0000] text-white text-xs font-bold px-3 py-1 rounded-full">
MOST POPULAR
</div>
)}
{plan?.current && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-green-600 text-white text-xs font-bold px-3 py-1 rounded-full">
CURRENT PLAN
</div>
)}
<div className="font-bold text-lg mb-1">{plan?.name}</div>
<div className="text-3xl font-black text-[#CC0000] mb-4">
{plan?.price}
<span className="text-sm text-gray-400 font-normal">{plan?.period}</span>
</div>
<ul className="space-y-2 mb-6">
{plan?.features?.map((f) => (
<li key={f} className="text-sm text-gray-300 flex items-center gap-2">
<span className="text-[#CC0000]"></span> {f}
</li>
))}
</ul>
<button
disabled
className="w-full py-2.5 rounded-lg font-bold text-sm bg-white/5 text-gray-500 cursor-not-allowed border border-white/10"
>
Coming Soon
</button>
</div>
))}
</div>
{/* Job post pricing */}
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-6 mb-6">
<h3 className="font-bold text-lg mb-4">One-Time Listings (Coming Soon)</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex justify-between py-2 border-b border-white/5">
<span className="text-gray-400">Standard Job Post</span>
<span className="font-bold">$49</span>
</div>
<div className="flex justify-between py-2 border-b border-white/5">
<span className="text-gray-400">Featured Job Post</span>
<span className="font-bold">$99</span>
</div>
<div className="flex justify-between py-2 border-b border-white/5">
<span className="text-gray-400">Job Pack (5 posts)</span>
<span className="font-bold">$199</span>
</div>
<div className="flex justify-between py-2 border-b border-white/5">
<span className="text-gray-400">Job Pack (10 posts)</span>
<span className="font-bold">$349</span>
</div>
</div>
</div>
<div className="text-center">
<Link
href="/marketplace/account"
className="text-[#CC0000] hover:underline text-sm"
>
Back to Account
</Link>
</div>
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,561 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/contexts/AuthContext';
const GIG_TYPES = [
'Commercial',
'Documentary',
'Corporate Video',
'Live Event',
'News / ENG',
'Sports',
'Music Video',
'Film',
'Streaming / OTT',
'Social Media',
'Other',
];
const JOB_TYPES = [
{ value: 'full_time', label: 'Full-Time' },
{ value: 'part_time', label: 'Part-Time' },
{ value: 'contract', label: 'Contract' },
{ value: 'temp', label: 'Temporary' },
];
export default function AccountPage() {
const { user, loading } = useAuth();
const router = useRouter();
const supabase = createClient();
const [profile, setProfile] = useState<any>(null);
const [details, setDetails] = useState<any>(null);
const [profileLoading, setProfileLoading] = useState(true);
// Job form
const [showJobForm, setShowJobForm] = useState(false);
const [jobTitle, setJobTitle] = useState('');
const [jobCompany, setJobCompany] = useState('');
const [jobLocation, setJobLocation] = useState('');
const [jobType, setJobType] = useState('full_time');
const [jobSalaryMin, setJobSalaryMin] = useState('');
const [jobSalaryMax, setJobSalaryMax] = useState('');
const [jobDescription, setJobDescription] = useState('');
const [jobApplyUrl, setJobApplyUrl] = useState('');
const [jobApplyEmail, setJobApplyEmail] = useState('');
const [jobSubmitting, setJobSubmitting] = useState(false);
const [jobMsg, setJobMsg] = useState('');
// Gig form
const [showGigForm, setShowGigForm] = useState(false);
const [gigTitle, setGigTitle] = useState('');
const [gigCompany, setGigCompany] = useState('');
const [gigLocation, setGigLocation] = useState('');
const [gigType, setGigType] = useState('');
const [gigRate, setGigRate] = useState('');
const [gigStartDate, setGigStartDate] = useState('');
const [gigEndDate, setGigEndDate] = useState('');
const [gigDescription, setGigDescription] = useState('');
const [gigContact, setGigContact] = useState('');
const [gigSubmitting, setGigSubmitting] = useState(false);
const [gigMsg, setGigMsg] = useState('');
useEffect(() => {
if (!loading && !user) {
router.replace('/marketplace/join');
}
}, [user, loading, router]);
useEffect(() => {
if (!user) return;
const fetchProfile = async () => {
setProfileLoading(true);
const { data: p } = await supabase
.from('marketplace_profiles')
.select('*')
.eq('user_id', user.id)
.maybeSingle();
setProfile(p);
if (p) {
if (p.profile_type === 'crew') {
const { data: d } = await supabase
.from('marketplace_crew_details')
.select('*')
.eq('profile_id', p.id)
.maybeSingle();
setDetails(d);
} else {
const { data: d } = await supabase
.from('marketplace_vendor_details')
.select('*')
.eq('profile_id', p.id)
.maybeSingle();
setDetails(d);
}
}
setProfileLoading(false);
};
fetchProfile();
}, [user, supabase]);
const hasActiveSubscription =
profile?.subscription_tier === 'pro' || profile?.subscription_tier === 'featured';
const handlePostJob = async (e: React.FormEvent) => {
e.preventDefault();
if (!profile) return;
setJobSubmitting(true);
setJobMsg('');
try {
const { error } = await supabase.from('marketplace_jobs').insert({
profile_id: profile.id,
title: jobTitle.trim(),
company: jobCompany.trim(),
location: jobLocation.trim(),
job_type: jobType,
salary_min: jobSalaryMin ? parseInt(jobSalaryMin) : null,
salary_max: jobSalaryMax ? parseInt(jobSalaryMax) : null,
description: jobDescription.trim(),
apply_url: jobApplyUrl.trim() || null,
apply_email: jobApplyEmail.trim() || null,
status: 'active',
});
if (error) throw error;
setJobMsg('✅ Job posted successfully!');
setShowJobForm(false);
setJobTitle(''); setJobCompany(''); setJobLocation('');
setJobDescription(''); setJobApplyUrl(''); setJobApplyEmail('');
} catch (err: any) {
setJobMsg(`${err.message}`);
} finally {
setJobSubmitting(false);
}
};
const handlePostGig = async (e: React.FormEvent) => {
e.preventDefault();
if (!profile) return;
setGigSubmitting(true);
setGigMsg('');
try {
const { error } = await supabase.from('marketplace_gigs').insert({
profile_id: profile.id,
title: gigTitle.trim(),
company: gigCompany.trim(),
location: gigLocation.trim(),
gig_type: gigType,
rate: gigRate.trim() || null,
start_date: gigStartDate || null,
end_date: gigEndDate || null,
description: gigDescription.trim(),
contact_method: gigContact.trim() || null,
status: 'active',
});
if (error) throw error;
setGigMsg('✅ Gig posted successfully!');
setShowGigForm(false);
setGigTitle(''); setGigCompany(''); setGigLocation('');
setGigDescription(''); setGigContact('');
} catch (err: any) {
setGigMsg(`${err.message}`);
} finally {
setGigSubmitting(false);
}
};
if (loading || profileLoading) {
return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div>
);
}
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-4xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-6">
<Link href="/marketplace" className="hover:text-[#CC0000]">
Marketplace
</Link>
<span>/</span>
<span className="text-white">My Account</span>
</div>
<h1 className="text-3xl font-black uppercase tracking-wide mb-8">My Account</h1>
{/* No profile yet */}
{!profile && (
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-8 text-center">
<div className="text-4xl mb-4">👤</div>
<h2 className="text-xl font-bold mb-3">No Profile Yet</h2>
<p className="text-gray-400 mb-6">
Create your marketplace profile to get discovered by clients and employers.
</p>
<Link
href="/marketplace/join"
className="bg-[#CC0000] hover:bg-red-700 text-white font-bold px-6 py-3 rounded-lg transition-colors"
>
Create Profile
</Link>
</div>
)}
{/* Profile status card */}
{profile && (
<div className="grid md:grid-cols-3 gap-6 mb-8">
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-6">
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Profile Status</div>
<div
className={`text-lg font-bold capitalize ${
profile.status === 'active' ?'text-green-400'
: profile.status === 'pending' ?'text-yellow-400' :'text-red-400'
}`}
>
{profile.status === 'active' ?'✓ Active'
: profile.status === 'pending' ?'⏳ Pending Review' :'✗ Rejected'}
</div>
{profile.status === 'pending' && (
<p className="text-xs text-gray-500 mt-2">
Our team will review your profile within 24 hours.
</p>
)}
{profile.status === 'rejected' && profile.rejection_reason && (
<p className="text-xs text-red-400 mt-2">{profile.rejection_reason}</p>
)}
</div>
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-6">
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Plan</div>
<div className="text-lg font-bold capitalize text-white">
{profile.subscription_tier === 'featured' ?'⭐ Featured'
: profile.subscription_tier === 'pro' ?'🚀 Pro' :'🆓 Free'}
</div>
<Link
href="/marketplace/account/billing"
className="text-xs text-[#CC0000] hover:underline mt-2 block"
>
Manage billing
</Link>
</div>
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-6">
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Profile Type</div>
<div className="text-lg font-bold capitalize text-white">
{profile.profile_type === 'crew' ? '🎬 Crew Member' : '🏢 Vendor / Company'}
</div>
{profile.status === 'active' && (
<Link
href={`/marketplace/profile/${profile.slug}`}
className="text-xs text-[#CC0000] hover:underline mt-2 block"
>
View public profile
</Link>
)}
</div>
</div>
)}
{/* Post Job / Gig section */}
{profile && (
<div className="space-y-6">
<h2 className="text-xl font-bold">Post Listings</h2>
{/* Subscription gate notice */}
{!hasActiveSubscription && (
<div className="bg-[#1a1a1a] border border-yellow-500/30 rounded-xl p-6">
<div className="flex items-start gap-4">
<div className="text-2xl">💳</div>
<div>
<h3 className="font-bold text-yellow-400 mb-1">Pro Subscription Required</h3>
<p className="text-sm text-gray-400 mb-3">
Posting jobs and gigs requires an active Pro or Featured subscription.
Billing is coming soon your profile is ready and waiting.
</p>
<Link
href="/marketplace/account/billing"
className="text-sm text-[#CC0000] hover:underline font-semibold"
>
View billing options
</Link>
</div>
</div>
</div>
)}
{/* Job post form */}
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl overflow-hidden">
<button
onClick={() => setShowJobForm((p) => !p)}
disabled={!hasActiveSubscription}
className="w-full flex items-center justify-between p-6 text-left disabled:opacity-50"
>
<div>
<div className="font-bold text-lg">Post a Job</div>
<div className="text-sm text-gray-400">Full-time, part-time, or contract</div>
</div>
<span className="text-gray-400">{showJobForm ? '▲' : '▼'}</span>
</button>
{showJobForm && (
<form onSubmit={handlePostJob} className="px-6 pb-6 space-y-4 border-t border-white/10 pt-6">
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="block text-xs text-gray-400 mb-1">Job Title *</label>
<input
value={jobTitle}
onChange={(e) => setJobTitle(e.target.value)}
required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. Senior Broadcast Engineer"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Company *</label>
<input
value={jobCompany}
onChange={(e) => setJobCompany(e.target.value)}
required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Location *</label>
<input
value={jobLocation}
onChange={(e) => setJobLocation(e.target.value)}
required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="City, State or Remote"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Job Type</label>
<select
value={jobType}
onChange={(e) => setJobType(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
>
{JOB_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Salary Range</label>
<div className="flex gap-2">
<input
type="number"
value={jobSalaryMin}
onChange={(e) => setJobSalaryMin(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Min $"
/>
<input
type="number"
value={jobSalaryMax}
onChange={(e) => setJobSalaryMax(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Max $"
/>
</div>
</div>
<div className="col-span-2">
<label className="block text-xs text-gray-400 mb-1">Description *</label>
<textarea
value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)}
required
rows={5}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000] resize-none"
placeholder="Job description, requirements, responsibilities..."
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Apply URL</label>
<input
value={jobApplyUrl}
onChange={(e) => setJobApplyUrl(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="https://..."
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Apply Email</label>
<input
type="email"
value={jobApplyEmail}
onChange={(e) => setJobApplyEmail(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="jobs@company.com"
/>
</div>
</div>
{jobMsg && (
<p
className={`text-sm px-4 py-2 rounded-lg ${
jobMsg.startsWith('✅')
? 'bg-green-500/10 text-green-400' :'bg-red-500/10 text-red-400'
}`}
>
{jobMsg}
</p>
)}
<button
type="submit"
disabled={jobSubmitting}
className="bg-[#CC0000] hover:bg-red-700 disabled:opacity-50 text-white font-bold px-6 py-2.5 rounded-lg text-sm transition-colors"
>
{jobSubmitting ? 'Posting...' : 'Post Job'}
</button>
</form>
)}
</div>
{/* Gig post form */}
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl overflow-hidden">
<button
onClick={() => setShowGigForm((p) => !p)}
disabled={!hasActiveSubscription}
className="w-full flex items-center justify-between p-6 text-left disabled:opacity-50"
>
<div>
<div className="font-bold text-lg">Post a Gig</div>
<div className="text-sm text-gray-400">Short-term crew call or freelance request</div>
</div>
<span className="text-gray-400">{showGigForm ? '▲' : '▼'}</span>
</button>
{showGigForm && (
<form onSubmit={handlePostGig} className="px-6 pb-6 space-y-4 border-t border-white/10 pt-6">
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="block text-xs text-gray-400 mb-1">Gig Title *</label>
<input
value={gigTitle}
onChange={(e) => setGigTitle(e.target.value)}
required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. DP needed for 2-day commercial shoot"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Company / Production *</label>
<input
value={gigCompany}
onChange={(e) => setGigCompany(e.target.value)}
required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Location *</label>
<input
value={gigLocation}
onChange={(e) => setGigLocation(e.target.value)}
required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Project Type *</label>
<select
value={gigType}
onChange={(e) => setGigType(e.target.value)}
required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
>
<option value="">Select type...</option>
{GIG_TYPES.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Rate</label>
<input
value={gigRate}
onChange={(e) => setGigRate(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. $750/day or negotiable"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Start Date</label>
<input
type="date"
value={gigStartDate}
onChange={(e) => setGigStartDate(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">End Date</label>
<input
type="date"
value={gigEndDate}
onChange={(e) => setGigEndDate(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/>
</div>
<div className="col-span-2">
<label className="block text-xs text-gray-400 mb-1">Description *</label>
<textarea
value={gigDescription}
onChange={(e) => setGigDescription(e.target.value)}
required
rows={4}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000] resize-none"
placeholder="Project details, crew needed, requirements..."
/>
</div>
<div className="col-span-2">
<label className="block text-xs text-gray-400 mb-1">How to Respond</label>
<input
value={gigContact}
onChange={(e) => setGigContact(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Email address, phone, or instructions"
/>
</div>
</div>
{gigMsg && (
<p
className={`text-sm px-4 py-2 rounded-lg ${
gigMsg.startsWith('✅')
? 'bg-green-500/10 text-green-400' :'bg-red-500/10 text-red-400'
}`}
>
{gigMsg}
</p>
)}
<button
type="submit"
disabled={gigSubmitting}
className="bg-[#CC0000] hover:bg-red-700 disabled:opacity-50 text-white font-bold px-6 py-2.5 rounded-lg text-sm transition-colors"
>
{gigSubmitting ? 'Posting...' : 'Post Gig'}
</button>
</form>
)}
</div>
</div>
)}
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,419 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/contexts/AuthContext';
interface PendingProfile {
id: string;
slug: string;
profile_type: 'crew' | 'vendor';
status: string;
created_at: string;
user_id: string;
details: any;
userEmail?: string;
}
export default function AdminQueuePage() {
const { user, loading } = useAuth();
const router = useRouter();
const supabase = createClient();
const [isAdmin, setIsAdmin] = useState(false);
const [checkingAdmin, setCheckingAdmin] = useState(true);
const [profiles, setProfiles] = useState<PendingProfile[]>([]);
const [queueLoading, setQueueLoading] = useState(true);
const [actionMsg, setActionMsg] = useState<Record<string, string>>({});
const [rejectionReasons, setRejectionReasons] = useState<Record<string, string>>({});
const [filter, setFilter] = useState<'pending' | 'active' | 'rejected' | 'all'>('pending');
useEffect(() => {
if (!loading && !user) {
router.replace('/marketplace');
}
}, [user, loading, router]);
useEffect(() => {
if (!user) return;
const checkAdmin = async () => {
const { data } = await supabase.auth.getUser();
const meta = data.user?.user_metadata || {};
const appMeta = data.user?.app_metadata || {};
const adminCheck =
meta.role === 'admin' ||
appMeta.role === 'admin' ||
// Also check user_profiles role column
false;
// Also check user_profiles table
const { data: profile } = await supabase
.from('user_profiles')
.select('role')
.eq('id', user.id)
.maybeSingle();
setIsAdmin(adminCheck || profile?.role === 'admin' || profile?.role === 'super_admin');
setCheckingAdmin(false);
};
checkAdmin();
}, [user, supabase]);
const fetchQueue = useCallback(async () => {
setQueueLoading(true);
let query = supabase
.from('marketplace_profiles')
.select('*')
.order('created_at', { ascending: false });
if (filter !== 'all') {
query = query.eq('status', filter);
}
const { data: profilesData } = await query;
if (!profilesData) {
setProfiles([]);
setQueueLoading(false);
return;
}
// Fetch details for each profile
const enriched: PendingProfile[] = [];
for (const p of profilesData) {
let details = null;
if (p.profile_type === 'crew') {
const { data } = await supabase
.from('marketplace_crew_details')
.select('*')
.eq('profile_id', p.id)
.maybeSingle();
details = data;
} else {
const { data } = await supabase
.from('marketplace_vendor_details')
.select('*')
.eq('profile_id', p.id)
.maybeSingle();
details = data;
}
enriched.push({ ...p, details });
}
setProfiles(enriched);
setQueueLoading(false);
}, [filter, supabase]);
useEffect(() => {
if (isAdmin) fetchQueue();
}, [isAdmin, fetchQueue]);
const handleApprove = async (profile: PendingProfile) => {
setActionMsg((prev) => ({ ...prev, [profile.id]: 'Approving...' }));
try {
const { error } = await supabase
.from('marketplace_profiles')
.update({ status: 'active' })
.eq('id', profile.id);
if (error) throw error;
// Index in Typesense via API
await fetch('/api/marketplace/typesense-index', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profileId: profile.id }),
}).catch(() => {}); // Non-blocking
// Send approval email
await fetch('/api/marketplace/admin/notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profileId: profile.id, action: 'approve' }),
}).catch(() => {});
setActionMsg((prev) => ({ ...prev, [profile.id]: '✅ Approved' }));
fetchQueue();
} catch (err: any) {
setActionMsg((prev) => ({ ...prev, [profile.id]: `${err.message}` }));
}
};
const handleReject = async (profile: PendingProfile) => {
const reason = rejectionReasons[profile.id]?.trim();
if (!reason) {
setActionMsg((prev) => ({ ...prev, [profile.id]: '⚠️ Please enter a rejection reason' }));
return;
}
setActionMsg((prev) => ({ ...prev, [profile.id]: 'Rejecting...' }));
try {
const { error } = await supabase
.from('marketplace_profiles')
.update({ status: 'rejected', rejection_reason: reason })
.eq('id', profile.id);
if (error) throw error;
// Send rejection email
await fetch('/api/marketplace/admin/notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profileId: profile.id, action: 'reject', reason }),
}).catch(() => {});
setActionMsg((prev) => ({ ...prev, [profile.id]: '✅ Rejected' }));
fetchQueue();
} catch (err: any) {
setActionMsg((prev) => ({ ...prev, [profile.id]: `${err.message}` }));
}
};
if (loading || checkingAdmin) {
return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (!isAdmin) {
return (
<div className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center">
<div className="text-center">
<div className="text-4xl mb-4">🔒</div>
<h1 className="text-2xl font-bold mb-2">Access Denied</h1>
<p className="text-gray-400 mb-6">Admin access required.</p>
<Link href="/marketplace" className="text-[#CC0000] hover:underline">
Back to Marketplace
</Link>
</div>
</div>
);
}
const pendingCount = profiles.filter((p) => p.status === 'pending').length;
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-6xl mx-auto px-4 py-24">
{/* Header */}
<div className="flex items-center gap-2 text-sm text-gray-500 mb-6">
<Link href="/marketplace" className="hover:text-[#CC0000]">
Marketplace
</Link>
<span>/</span>
<span className="text-white">Admin Queue</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
<div>
<h1 className="text-3xl font-black uppercase tracking-wide">Profile Approval Queue</h1>
{pendingCount > 0 && (
<p className="text-yellow-400 text-sm mt-1">
{pendingCount} profile{pendingCount !== 1 ? 's' : ''} awaiting review
</p>
)}
</div>
<button
onClick={fetchQueue}
className="text-sm text-[#CC0000] hover:underline font-semibold"
>
Refresh
</button>
</div>
{/* Filter tabs */}
<div className="flex gap-2 mb-8">
{(['pending', 'active', 'rejected', 'all'] as const).map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-4 py-2 rounded-lg text-sm font-bold capitalize transition-colors ${
filter === f
? 'bg-[#CC0000] text-white'
: 'bg-[#1a1a1a] text-gray-400 hover:text-white border border-white/10'
}`}
>
{f}
</button>
))}
</div>
{queueLoading ? (
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div>
) : profiles.length === 0 ? (
<div className="text-center py-20 text-gray-500">
<div className="text-4xl mb-4"></div>
<p className="text-lg font-semibold">
{filter === 'pending' ? 'No profiles pending review' : `No ${filter} profiles`}
</p>
</div>
) : (
<div className="space-y-6">
{profiles.map((profile) => {
const name =
profile.profile_type === 'crew'
? profile.details?.name
: profile.details?.company_name;
const isPending = profile.status === 'pending';
return (
<div
key={profile.id}
className={`bg-[#1a1a1a] rounded-xl border overflow-hidden ${
isPending ? 'border-yellow-500/30' : 'border-white/10'
}`}
>
<div className="p-6">
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
<div className="flex items-start gap-4">
{/* Avatar */}
<div className="w-14 h-14 rounded-full bg-[#CC0000]/20 flex-shrink-0 overflow-hidden flex items-center justify-center text-xl font-bold text-[#CC0000]">
{(profile.details?.headshot_url || profile.details?.logo_url) ? (
<img
src={profile.details.headshot_url || profile.details.logo_url}
alt={`${name} profile photo`}
className="w-full h-full object-cover"
/>
) : (
name?.charAt(0)?.toUpperCase() || '?'
)}
</div>
<div>
<div className="flex flex-wrap items-center gap-2 mb-1">
<span className="font-bold text-lg">{name || 'Unknown'}</span>
<span
className={`text-xs px-2 py-0.5 rounded-full font-bold ${
profile.status === 'active' ?'bg-green-500/20 text-green-400'
: profile.status === 'pending' ?'bg-yellow-500/20 text-yellow-400' :'bg-red-500/20 text-red-400'
}`}
>
{profile.status}
</span>
<span
className={`text-xs px-2 py-0.5 rounded-full font-bold ${
profile.profile_type === 'crew' ?'bg-blue-500/20 text-blue-400' :'bg-purple-500/20 text-purple-400'
}`}
>
{profile.profile_type}
</span>
</div>
<div className="text-sm text-gray-400">
📍 {profile.details?.location || 'No location'}
</div>
{profile.profile_type === 'crew' && (
<div className="text-sm text-gray-500 mt-0.5">
{profile.details?.skills?.slice(0, 3).join(', ') || 'No skills listed'}
</div>
)}
{profile.profile_type === 'vendor' && (
<div className="text-sm text-gray-500 mt-0.5">
{profile.details?.category || 'No category'}
</div>
)}
<div className="text-xs text-gray-600 mt-1">
Submitted{' '}
{new Date(profile.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
</div>
</div>
{/* Profile details */}
<div className="text-sm text-gray-400 space-y-1 min-w-0">
{profile.details?.bio && (
<p className="text-xs text-gray-500 max-w-xs line-clamp-3">
{profile.details.bio}
</p>
)}
{profile.details?.reel_url && (
<a
href={profile.details.reel_url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-[#CC0000] hover:underline block"
>
View Reel
</a>
)}
{profile.details?.website && (
<a
href={profile.details.website}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-[#CC0000] hover:underline block"
>
🌐 Website
</a>
)}
</div>
</div>
{/* Action area */}
{isPending && (
<div className="mt-6 pt-6 border-t border-white/10">
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={() => handleApprove(profile)}
className="bg-green-600 hover:bg-green-700 text-white font-bold px-6 py-2.5 rounded-lg text-sm transition-colors"
>
Approve
</button>
<div className="flex flex-1 gap-2">
<input
value={rejectionReasons[profile.id] || ''}
onChange={(e) =>
setRejectionReasons((prev) => ({
...prev,
[profile.id]: e.target.value,
}))
}
className="flex-1 bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-red-500"
placeholder="Rejection reason (required)..."
/>
<button
onClick={() => handleReject(profile)}
className="bg-red-700 hover:bg-red-800 text-white font-bold px-4 py-2.5 rounded-lg text-sm transition-colors whitespace-nowrap"
>
Reject
</button>
</div>
</div>
{actionMsg[profile.id] && (
<p
className={`text-sm mt-2 ${
actionMsg[profile.id].startsWith('✅')
? 'text-green-400'
: actionMsg[profile.id].startsWith('⚠️')
? 'text-yellow-400' :'text-red-400'
}`}
>
{actionMsg[profile.id]}
</p>
)}
</div>
)}
{!isPending && actionMsg[profile.id] && (
<p className="text-sm mt-4 text-gray-400">{actionMsg[profile.id]}</p>
)}
</div>
</div>
);
})}
</div>
)}
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,107 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
export default function GigDetailPage() {
const params = useParams();
const id = params?.id as string;
const supabase = createClient();
const [gig, setGig] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!id) return;
const fetchGig = async () => {
const { data } = await supabase
.from('marketplace_gigs')
.select('*')
.eq('id', id)
.eq('status', 'active')
.maybeSingle();
setGig(data);
setLoading(false);
};
fetchGig();
}, [id]);
if (loading) {
return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (!gig) {
return (
<div className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-2">Gig Not Found</h1>
<Link href="/marketplace/gigs" className="text-[#CC0000] hover:underline">Back to Gigs</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-3xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
<Link href="/marketplace" className="hover:text-[#CC0000]">Marketplace</Link>
<span>/</span>
<Link href="/marketplace/gigs" className="hover:text-[#CC0000]">Gigs</Link>
<span>/</span>
<span className="text-white truncate">{gig.title}</span>
</div>
<div className="bg-[#1a1a1a] rounded-2xl border border-white/10 overflow-hidden">
<div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8">
<h1 className="text-2xl font-black mb-2">{gig.title}</h1>
<div className="text-[#CC0000] font-bold text-lg mb-4">{gig.company}</div>
<div className="flex flex-wrap gap-4 text-sm text-gray-300">
<span>📍 {gig.location}</span>
<span>🎬 {gig.gig_type}</span>
{gig.rate && <span>💵 {gig.rate}</span>}
{gig.start_date && (
<span>📅 {new Date(gig.start_date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
{gig.end_date && ` ${new Date(gig.end_date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}`}
</span>
)}
</div>
</div>
<div className="p-8 space-y-6">
<section>
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-3">Project Details</h2>
<div className="text-gray-300 leading-relaxed whitespace-pre-wrap">{gig.description}</div>
</section>
<div className="border-t border-white/10 pt-6">
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-4">How to Respond</h2>
{gig.contact_method ? (
<div className="bg-[#0d0d0d] rounded-lg p-4 text-sm text-gray-300">{gig.contact_method}</div>
) : (
<p className="text-gray-400 text-sm">Contact the poster directly through the platform.</p>
)}
</div>
<div className="text-xs text-gray-500 border-t border-white/10 pt-4">
Posted {new Date(gig.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
</div>
</div>
</div>
<div className="mt-6 text-center">
<Link href="/marketplace/gigs" className="text-[#CC0000] hover:underline text-sm"> Back to Gig Board</Link>
</div>
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,112 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
export default function GigsPage() {
const supabase = createClient();
const [gigs, setGigs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchGigs = async () => {
const { data } = await supabase
.from('marketplace_gigs')
.select('*, marketplace_profiles!inner(slug, subscription_tier)')
.eq('status', 'active')
.order('created_at', { ascending: false })
.limit(50);
setGigs(data || []);
setLoading(false);
};
fetchGigs();
}, []);
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-5xl mx-auto px-4 py-24">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
<div>
<div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
<Link href="/marketplace" className="hover:text-[#CC0000]">Marketplace</Link>
<span>/</span>
<span className="text-white">Gigs</span>
</div>
<h1 className="text-3xl font-black uppercase tracking-wide">Gig Board</h1>
<p className="text-gray-400 mt-1">
{loading ? '...' : `${gigs.length} open gig${gigs.length !== 1 ? 's' : ''}`}
</p>
</div>
<Link
href="/marketplace/account"
className="bg-[#CC0000] hover:bg-red-700 text-white font-bold px-5 py-3 rounded-lg text-sm transition-colors whitespace-nowrap"
>
+ Post a Gig
</Link>
</div>
<div className="bg-[#1a1a1a] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400 mb-8">
💳 <strong>Gig posting requires a Pro subscription.</strong> Billing is coming soon
create your free profile now and post gigs when payments launch.
</div>
{loading ? (
<div className="space-y-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-xl p-6 animate-pulse h-28" />
))}
</div>
) : gigs.length === 0 ? (
<div className="text-center py-20 text-gray-500">
<div className="text-4xl mb-4">🎬</div>
<p className="text-lg font-semibold mb-2">No gigs posted yet</p>
<p className="text-sm">Be the first to post a crew call on Broadcast Beat Marketplace.</p>
</div>
) : (
<div className="space-y-4">
{gigs.map((gig) => (
<Link
key={gig.id}
href={`/marketplace/gigs/${gig.id}`}
className="block bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-6 transition-all"
>
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
<div className="flex-1">
<div className="flex flex-wrap items-center gap-2 mb-1">
<h2 className="font-bold text-lg text-white">{gig.title}</h2>
{gig.marketplace_profiles?.subscription_tier === 'featured' && (
<span className="text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 px-2 py-0.5 rounded-full font-bold">Featured</span>
)}
</div>
<div className="text-[#CC0000] font-semibold text-sm mb-2">{gig.company}</div>
<div className="flex flex-wrap gap-3 text-sm text-gray-400">
<span>📍 {gig.location}</span>
<span>🎬 {gig.gig_type}</span>
{gig.rate && <span>💵 {gig.rate}</span>}
{gig.start_date && (
<span>📅 {new Date(gig.start_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
{gig.end_date && ` ${new Date(gig.end_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`}
</span>
)}
</div>
</div>
<div className="text-xs text-gray-500 whitespace-nowrap">
{new Date(gig.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</div>
</div>
{gig.description && (
<p className="text-sm text-gray-500 mt-3 line-clamp-2">{gig.description}</p>
)}
</Link>
))}
</div>
)}
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,122 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
export default function JobDetailPage() {
const params = useParams();
const id = params?.id as string;
const supabase = createClient();
const [job, setJob] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!id) return;
const fetchJob = async () => {
const { data } = await supabase
.from('marketplace_jobs')
.select('*')
.eq('id', id)
.eq('status', 'active')
.maybeSingle();
setJob(data);
setLoading(false);
};
fetchJob();
}, [id]);
if (loading) {
return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (!job) {
return (
<div className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-2">Job Not Found</h1>
<Link href="/marketplace/jobs" className="text-[#CC0000] hover:underline">Back to Jobs</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-3xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
<Link href="/marketplace" className="hover:text-[#CC0000]">Marketplace</Link>
<span>/</span>
<Link href="/marketplace/jobs" className="hover:text-[#CC0000]">Jobs</Link>
<span>/</span>
<span className="text-white truncate">{job.title}</span>
</div>
<div className="bg-[#1a1a1a] rounded-2xl border border-white/10 overflow-hidden">
<div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8">
<h1 className="text-2xl font-black mb-2">{job.title}</h1>
<div className="text-[#CC0000] font-bold text-lg mb-4">{job.company}</div>
<div className="flex flex-wrap gap-4 text-sm text-gray-300">
<span>📍 {job.location}</span>
<span className="capitalize">🕐 {job.job_type?.replace('_', ' ')}</span>
{(job.salary_min || job.salary_max) && (
<span>💵 {job.salary_min && job.salary_max
? `$${job.salary_min.toLocaleString()} $${job.salary_max.toLocaleString()}`
: job.salary_min ? `From $${job.salary_min.toLocaleString()}` : `Up to $${job.salary_max?.toLocaleString()}`}
</span>
)}
{job.expires_at && (
<span>📅 Closes {new Date(job.expires_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}</span>
)}
</div>
</div>
<div className="p-8 space-y-6">
<section>
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-3">Job Description</h2>
<div className="text-gray-300 leading-relaxed whitespace-pre-wrap">{job.description}</div>
</section>
<div className="border-t border-white/10 pt-6">
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-4">How to Apply</h2>
<div className="flex flex-wrap gap-3">
{job.apply_url && (
<a href={job.apply_url} target="_blank" rel="noopener noreferrer"
className="bg-[#CC0000] hover:bg-red-700 text-white font-bold px-6 py-3 rounded-lg transition-colors">
Apply Now
</a>
)}
{job.apply_email && (
<a href={`mailto:${job.apply_email}?subject=Application: ${job.title}`}
className="bg-white/10 hover:bg-white/20 border border-white/20 text-white font-bold px-6 py-3 rounded-lg transition-colors">
Email Application
</a>
)}
{!job.apply_url && !job.apply_email && (
<p className="text-gray-400 text-sm">Contact the employer directly for application instructions.</p>
)}
</div>
</div>
<div className="text-xs text-gray-500 border-t border-white/10 pt-4">
Posted {new Date(job.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
</div>
</div>
</div>
<div className="mt-6 text-center">
<Link href="/marketplace/jobs" className="text-[#CC0000] hover:underline text-sm"> Back to Job Board</Link>
</div>
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,112 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
export default function JobsPage() {
const supabase = createClient();
const [jobs, setJobs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchJobs = async () => {
const { data } = await supabase
.from('marketplace_jobs')
.select('*, marketplace_profiles!inner(slug, subscription_tier)')
.eq('status', 'active')
.order('created_at', { ascending: false })
.limit(50);
setJobs(data || []);
setLoading(false);
};
fetchJobs();
}, []);
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-5xl mx-auto px-4 py-24">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
<div>
<div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
<Link href="/marketplace" className="hover:text-[#CC0000]">Marketplace</Link>
<span>/</span>
<span className="text-white">Jobs</span>
</div>
<h1 className="text-3xl font-black uppercase tracking-wide">Job Board</h1>
<p className="text-gray-400 mt-1">
{loading ? '...' : `${jobs.length} open position${jobs.length !== 1 ? 's' : ''}`}
</p>
</div>
<Link
href="/marketplace/account"
className="bg-[#CC0000] hover:bg-red-700 text-white font-bold px-5 py-3 rounded-lg text-sm transition-colors whitespace-nowrap"
>
+ Post a Job
</Link>
</div>
<div className="bg-[#1a1a1a] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400 mb-8">
💳 <strong>Job posting requires a Pro subscription.</strong> Billing is coming soon
create your free profile now and post jobs when payments launch.
</div>
{loading ? (
<div className="space-y-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-xl p-6 animate-pulse h-28" />
))}
</div>
) : jobs.length === 0 ? (
<div className="text-center py-20 text-gray-500">
<div className="text-4xl mb-4">📋</div>
<p className="text-lg font-semibold mb-2">No jobs posted yet</p>
<p className="text-sm">Be the first to post a job on Broadcast Beat Marketplace.</p>
</div>
) : (
<div className="space-y-4">
{jobs.map((job) => (
<Link
key={job.id}
href={`/marketplace/jobs/${job.id}`}
className="block bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-6 transition-all"
>
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
<div className="flex-1">
<div className="flex flex-wrap items-center gap-2 mb-1">
<h2 className="font-bold text-lg text-white">{job.title}</h2>
{job.marketplace_profiles?.subscription_tier === 'featured' && (
<span className="text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 px-2 py-0.5 rounded-full font-bold">Featured</span>
)}
</div>
<div className="text-[#CC0000] font-semibold text-sm mb-2">{job.company}</div>
<div className="flex flex-wrap gap-3 text-sm text-gray-400">
<span>📍 {job.location}</span>
<span className="capitalize">🕐 {job.job_type?.replace('_', ' ')}</span>
{(job.salary_min || job.salary_max) && (
<span>💵 {job.salary_min && job.salary_max
? `$${job.salary_min.toLocaleString()} $${job.salary_max.toLocaleString()}`
: job.salary_min ? `From $${job.salary_min.toLocaleString()}` : `Up to $${job.salary_max?.toLocaleString()}`}
</span>
)}
</div>
</div>
<div className="text-xs text-gray-500 whitespace-nowrap">
{new Date(job.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</div>
</div>
{job.description && (
<p className="text-sm text-gray-500 mt-3 line-clamp-2">{job.description}</p>
)}
</Link>
))}
</div>
)}
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,673 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/contexts/AuthContext';
const SKILL_OPTIONS = [
'Director of Photography',
'Camera Operator',
'1st AC / Focus Puller',
'DIT',
'Steadicam Operator',
'Drone / UAV Operator',
'Gaffer',
'Grip / Key Grip',
'Production Sound Mixer',
'Boom Operator',
'Sound Designer',
'Audio Post / Mixing Engineer',
'Director',
'Producer',
'Line Producer',
'Production Manager',
'Assistant Director',
'Script Supervisor',
'Editor (Offline)',
'Online Editor / Finishing',
'Colorist',
'Motion Graphics Designer',
'VFX Artist / Compositor',
'Animator (2D/3D)',
'Technical Director',
'Broadcast Engineer',
'Master Control Operator',
'Graphics Operator',
'Live Event Producer',
'Livestream Producer',
'Social Media Video Producer',
'Photographer',
];
const VENDOR_CATEGORIES = [
'Full-Service Production Company',
'Equipment Rental House',
'Studio / Stage Rental',
'Post Production Facility',
'Animation Studio',
'Drone Services Company',
'Casting Agency',
'Talent Agency',
'Production Insurance',
'Catering / Craft Services',
'Transportation / Vehicles',
'Other',
];
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
export default function JoinMarketplacePage() {
const router = useRouter();
const { user } = useAuth();
const supabase = createClient();
const [step, setStep] = useState<'type' | 'details' | 'submitting' | 'done'>('type');
const [profileType, setProfileType] = useState<'crew' | 'vendor' | null>(null);
const [error, setError] = useState('');
const [uploading, setUploading] = useState(false);
// Auth state
const [authMode, setAuthMode] = useState<'login' | 'signup'>('signup');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [authError, setAuthError] = useState('');
const [authLoading, setAuthLoading] = useState(false);
// Crew fields
const [crewName, setCrewName] = useState('');
const [crewLocation, setCrewLocation] = useState('');
const [crewUnion, setCrewUnion] = useState('non_union');
const [crewRate, setCrewRate] = useState('');
const [crewBio, setCrewBio] = useState('');
const [crewSkills, setCrewSkills] = useState<string[]>([]);
const [crewEquipment, setCrewEquipment] = useState('');
const [crewReel, setCrewReel] = useState('');
const [headshotFile, setHeadshotFile] = useState<File | null>(null);
const [resumeFile, setResumeFile] = useState<File | null>(null);
// Vendor fields
const [vendorName, setVendorName] = useState('');
const [vendorLocation, setVendorLocation] = useState('');
const [vendorCategory, setVendorCategory] = useState('');
const [vendorDescription, setVendorDescription] = useState('');
const [vendorWebsite, setVendorWebsite] = useState('');
const [vendorEmail, setVendorEmail] = useState('');
const [logoFile, setLogoFile] = useState<File | null>(null);
const handleAuth = async (e: React.FormEvent) => {
e.preventDefault();
setAuthLoading(true);
setAuthError('');
try {
if (authMode === 'signup') {
const { error } = await supabase.auth.signUp({ email, password });
if (error) throw error;
} else {
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) throw error;
}
} catch (err: any) {
setAuthError(err.message);
} finally {
setAuthLoading(false);
}
};
const toggleSkill = (skill: string) => {
setCrewSkills((prev) =>
prev.includes(skill) ? prev.filter((s) => s !== skill) : [...prev, skill]
);
};
const uploadFile = async (
file: File,
bucket: string,
userId: string
): Promise<string | null> => {
const ext = file.name.split('.').pop();
const path = `${userId}/${Date.now()}.${ext}`;
const { error } = await supabase.storage.from(bucket).upload(path, file, { upsert: true });
if (error) return null;
if (bucket === 'marketplace-resumes') return path;
const { data } = supabase.storage.from(bucket).getPublicUrl(path);
return data.publicUrl;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!user) return;
setError('');
setStep('submitting');
setUploading(true);
try {
let headshotUrl: string | null = null;
let logoUrl: string | null = null;
let resumeUrl: string | null = null;
if (profileType === 'crew') {
if (!crewName.trim() || !crewLocation.trim()) {
setError('Name and location are required.');
setStep('details');
return;
}
if (headshotFile) {
headshotUrl = await uploadFile(headshotFile, 'marketplace-headshots', user.id);
}
if (resumeFile) {
resumeUrl = await uploadFile(resumeFile, 'marketplace-resumes', user.id);
}
} else {
if (!vendorName.trim() || !vendorLocation.trim() || !vendorCategory) {
setError('Company name, location, and category are required.');
setStep('details');
return;
}
if (logoFile) {
logoUrl = await uploadFile(logoFile, 'marketplace-logos', user.id);
}
}
const baseSlug = slugify(
profileType === 'crew' ? crewName : vendorName
);
const slug = `${baseSlug}-${Date.now().toString(36)}`;
// Create profile record
const { data: profile, error: profileError } = await supabase
.from('marketplace_profiles')
.insert({
user_id: user.id,
profile_type: profileType,
slug,
status: 'pending',
subscription_tier: 'free',
})
.select()
.single();
if (profileError) throw profileError;
// Create detail record
if (profileType === 'crew') {
const { error: detailError } = await supabase
.from('marketplace_crew_details')
.insert({
profile_id: profile.id,
name: crewName.trim(),
headshot_url: headshotUrl,
location: crewLocation.trim(),
union_status: crewUnion,
day_rate: crewRate ? parseInt(crewRate) : null,
bio: crewBio.trim(),
skills: crewSkills,
equipment: crewEquipment
? crewEquipment.split(',').map((s) => s.trim()).filter(Boolean)
: [],
reel_url: crewReel.trim() || null,
resume_url: resumeUrl,
});
if (detailError) throw detailError;
} else {
const { error: detailError } = await supabase
.from('marketplace_vendor_details')
.insert({
profile_id: profile.id,
company_name: vendorName.trim(),
logo_url: logoUrl,
location: vendorLocation.trim(),
category: vendorCategory,
description: vendorDescription.trim(),
website: vendorWebsite.trim() || null,
contact_email: vendorEmail.trim() || user.email,
});
if (detailError) throw detailError;
}
setStep('done');
} catch (err: any) {
setError(err.message || 'Something went wrong. Please try again.');
setStep('details');
} finally {
setUploading(false);
}
};
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-2xl mx-auto px-4 py-24">
{/* Header */}
<div className="text-center mb-10">
<Link href="/marketplace" className="text-[#CC0000] text-sm hover:underline">
Back to Marketplace
</Link>
<h1 className="text-3xl font-black uppercase tracking-wide mt-4 mb-2">
Join the Marketplace
</h1>
<p className="text-gray-400">
Create your free profile and get discovered by clients and employers.
</p>
</div>
{/* Auth gate */}
{!user && (
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-8 mb-8">
<h2 className="text-lg font-bold mb-6 text-center">
{authMode === 'signup' ? 'Create an Account' : 'Sign In to Continue'}
</h2>
<form onSubmit={handleAuth} className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="you@example.com"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="••••••••"
/>
</div>
{authError && (
<p className="text-red-400 text-sm bg-red-400/10 rounded-lg px-4 py-2">
{authError}
</p>
)}
<button
type="submit"
disabled={authLoading}
className="w-full bg-[#CC0000] hover:bg-red-700 disabled:opacity-50 text-white font-bold py-3 rounded-lg transition-colors"
>
{authLoading
? 'Please wait...'
: authMode === 'signup' ?'Create Account' :'Sign In'}
</button>
</form>
<p className="text-center text-sm text-gray-500 mt-4">
{authMode === 'signup' ? 'Already have an account?' : "Don't have an account?"}{' '}
<button
onClick={() => setAuthMode(authMode === 'signup' ? 'login' : 'signup')}
className="text-[#CC0000] hover:underline"
>
{authMode === 'signup' ? 'Sign in' : 'Sign up'}
</button>
</p>
</div>
)}
{/* Step: Choose type */}
{user && step === 'type' && (
<div className="space-y-4">
<h2 className="text-lg font-bold text-center mb-6">What type of profile?</h2>
<button
onClick={() => {
setProfileType('crew');
setStep('details');
}}
className="w-full bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/50 rounded-xl p-6 text-left transition-all group"
>
<div className="text-2xl mb-2">🎬</div>
<div className="font-bold text-lg group-hover:text-[#CC0000] transition-colors">
Crew Member / Freelancer
</div>
<div className="text-sm text-gray-400 mt-1">
DP, Editor, Sound Mixer, Colorist, Engineer, and more
</div>
</button>
<button
onClick={() => {
setProfileType('vendor');
setStep('details');
}}
className="w-full bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/50 rounded-xl p-6 text-left transition-all group"
>
<div className="text-2xl mb-2">🏢</div>
<div className="font-bold text-lg group-hover:text-[#CC0000] transition-colors">
Vendor / Company
</div>
<div className="text-sm text-gray-400 mt-1">
Production company, rental house, post facility, studio
</div>
</button>
</div>
)}
{/* Step: Crew details */}
{user && step === 'details' && profileType === 'crew' && (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="flex items-center gap-3 mb-6">
<button
type="button"
onClick={() => setStep('type')}
className="text-gray-400 hover:text-white text-sm"
>
Back
</button>
<h2 className="text-lg font-bold">Crew Member Profile</h2>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="block text-sm text-gray-400 mb-1">
Full Name <span className="text-[#CC0000]">*</span>
</label>
<input
value={crewName}
onChange={(e) => setCrewName(e.target.value)}
required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="Your professional name"
/>
</div>
<div className="col-span-2">
<label className="block text-sm text-gray-400 mb-1">
Location <span className="text-[#CC0000]">*</span>
</label>
<input
value={crewLocation}
onChange={(e) => setCrewLocation(e.target.value)}
required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="City, State / Country"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Union Status</label>
<select
value={crewUnion}
onChange={(e) => setCrewUnion(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
>
<option value="non_union">Non-Union</option>
<option value="iatse">IATSE</option>
<option value="sag_aftra">SAG-AFTRA</option>
<option value="teamsters">Teamsters</option>
<option value="nabet">NABET</option>
</select>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Day Rate (USD)</label>
<input
type="number"
value={crewRate}
onChange={(e) => setCrewRate(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. 750"
min="0"
/>
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Bio / About</label>
<textarea
value={crewBio}
onChange={(e) => setCrewBio(e.target.value)}
rows={4}
maxLength={500}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000] resize-none"
placeholder="Brief professional bio (500 chars max)"
/>
<div className="text-xs text-gray-500 text-right mt-1">{crewBio.length}/500</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Skills / Specialties</label>
<div className="flex flex-wrap gap-2">
{SKILL_OPTIONS.map((skill) => (
<button
key={skill}
type="button"
onClick={() => toggleSkill(skill)}
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
crewSkills.includes(skill)
? 'bg-[#CC0000] border-[#CC0000] text-white'
: 'bg-transparent border-white/20 text-gray-400 hover:border-white/40'
}`}
>
{skill}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Equipment Owned</label>
<input
value={crewEquipment}
onChange={(e) => setCrewEquipment(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. Sony FX9, DJI Ronin, Sennheiser MKH416 (comma-separated)"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Reel / Demo URL</label>
<input
value={crewReel}
onChange={(e) => setCrewReel(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="https://vimeo.com/..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Headshot / Photo</label>
<input
type="file"
accept="image/jpeg,image/jpg,image/png,image/webp"
onChange={(e) => setHeadshotFile(e.target.files?.[0] || null)}
className="w-full text-sm text-gray-400 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-[#CC0000] file:text-white file:cursor-pointer"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Resume / CV (PDF)</label>
<input
type="file"
accept="application/pdf,.doc,.docx"
onChange={(e) => setResumeFile(e.target.files?.[0] || null)}
className="w-full text-sm text-gray-400 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-[#1a1a1a] file:border file:border-white/20 file:text-white file:cursor-pointer"
/>
</div>
</div>
{error && (
<p className="text-red-400 text-sm bg-red-400/10 rounded-lg px-4 py-2">{error}</p>
)}
<button
type="submit"
disabled={uploading}
className="w-full bg-[#CC0000] hover:bg-red-700 disabled:opacity-50 text-white font-bold py-4 rounded-lg text-lg transition-colors"
>
{uploading ? 'Submitting...' : 'Submit Profile for Review'}
</button>
<p className="text-xs text-gray-500 text-center">
All profiles are reviewed by our team before going live. Typically within 24 hours.
</p>
</form>
)}
{/* Step: Vendor details */}
{user && step === 'details' && profileType === 'vendor' && (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="flex items-center gap-3 mb-6">
<button
type="button"
onClick={() => setStep('type')}
className="text-gray-400 hover:text-white text-sm"
>
Back
</button>
<h2 className="text-lg font-bold">Vendor / Company Profile</h2>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">
Company Name <span className="text-[#CC0000]">*</span>
</label>
<input
value={vendorName}
onChange={(e) => setVendorName(e.target.value)}
required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="Your company name"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-400 mb-1">
Location <span className="text-[#CC0000]">*</span>
</label>
<input
value={vendorLocation}
onChange={(e) => setVendorLocation(e.target.value)}
required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="City, State / Country"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">
Category <span className="text-[#CC0000]">*</span>
</label>
<select
value={vendorCategory}
onChange={(e) => setVendorCategory(e.target.value)}
required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
>
<option value="">Select category...</option>
{VENDOR_CATEGORIES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Company Description</label>
<textarea
value={vendorDescription}
onChange={(e) => setVendorDescription(e.target.value)}
rows={4}
maxLength={1000}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000] resize-none"
placeholder="Describe your services, specialties, and what makes you stand out"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Website</label>
<input
value={vendorWebsite}
onChange={(e) => setVendorWebsite(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="https://yourcompany.com"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Contact Email</label>
<input
type="email"
value={vendorEmail}
onChange={(e) => setVendorEmail(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="contact@yourcompany.com"
/>
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Company Logo</label>
<input
type="file"
accept="image/jpeg,image/jpg,image/png,image/webp,image/svg+xml"
onChange={(e) => setLogoFile(e.target.files?.[0] || null)}
className="w-full text-sm text-gray-400 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-[#CC0000] file:text-white file:cursor-pointer"
/>
</div>
{error && (
<p className="text-red-400 text-sm bg-red-400/10 rounded-lg px-4 py-2">{error}</p>
)}
<button
type="submit"
disabled={uploading}
className="w-full bg-[#CC0000] hover:bg-red-700 disabled:opacity-50 text-white font-bold py-4 rounded-lg text-lg transition-colors"
>
{uploading ? 'Submitting...' : 'Submit Profile for Review'}
</button>
<p className="text-xs text-gray-500 text-center">
All profiles are reviewed by our team before going live. Typically within 24 hours.
</p>
</form>
)}
{/* Submitting */}
{step === 'submitting' && (
<div className="text-center py-16">
<div className="w-12 h-12 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-400">Uploading your profile...</p>
</div>
)}
{/* Done */}
{step === 'done' && (
<div className="text-center py-16">
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-6 text-3xl">
</div>
<h2 className="text-2xl font-bold mb-3">Profile Submitted!</h2>
<p className="text-gray-400 mb-8 max-w-md mx-auto">
Your profile is now in our review queue. Our team will review it within 24 hours.
You&apos;ll receive an email once it&apos;s approved and live.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/marketplace/account"
className="bg-[#CC0000] hover:bg-red-700 text-white font-bold px-6 py-3 rounded-lg transition-colors"
>
View My Account
</Link>
<Link
href="/marketplace"
className="bg-white/10 hover:bg-white/20 text-white font-bold px-6 py-3 rounded-lg transition-colors"
>
Back to Marketplace
</Link>
</div>
</div>
)}
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,260 @@
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
export const metadata = {
title: 'Production Crew & Vendor Marketplace | Broadcast Beat',
description:
'Find broadcast and production crew, vendors, jobs, and gig opportunities. The professional marketplace built for the broadcast industry.',
};
const CATEGORIES = [
{ icon: '🎥', label: 'Camera & Lighting', count: 'DPs, Operators, Gaffers' },
{ icon: '🎙️', label: 'Audio', count: 'Mixers, Boom Ops, Sound Designers' },
{ icon: '🎬', label: 'Directing & Production', count: 'Directors, Producers, ADs' },
{ icon: '✂️', label: 'Post Production', count: 'Editors, Colorists, VFX' },
{ icon: '📡', label: 'Broadcast & Live', count: 'TDs, Engineers, MCOs' },
{ icon: '🏢', label: 'Vendors & Companies', count: 'Rental Houses, Post Facilities' },
];
const STATS = [
{ value: 'Phase 1', label: 'MVP Launch' },
{ value: 'Free', label: 'To Join' },
{ value: 'NAB', label: 'Official Media Partner' },
{ value: '2025', label: 'Built for Today' },
];
export default function MarketplacePage() {
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
{/* Hero */}
<section className="relative pt-24 pb-20 px-4 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-[#CC0000]/10 to-transparent pointer-events-none" />
<div className="max-w-6xl mx-auto text-center relative z-10">
<div className="inline-flex items-center gap-2 bg-[#CC0000]/20 border border-[#CC0000]/40 rounded-full px-4 py-1.5 text-sm text-[#CC0000] font-semibold mb-6">
<span className="w-2 h-2 bg-[#CC0000] rounded-full animate-pulse" />
Now Live Phase 1 MVP
</div>
<h1 className="text-4xl md:text-6xl font-black uppercase tracking-tight mb-6 leading-none">
The Broadcast Industry&apos;s
<br />
<span className="text-[#CC0000]">Production Marketplace</span>
</h1>
<p className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto mb-10 leading-relaxed">
Find crew, hire vendors, post jobs, and land gigs all in one place built by
broadcast professionals, for broadcast professionals.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/marketplace/search"
className="bg-[#CC0000] hover:bg-red-700 text-white font-bold px-8 py-4 rounded-lg text-lg transition-colors"
>
Find Crew & Vendors
</Link>
<Link
href="/marketplace/join"
className="bg-white/10 hover:bg-white/20 border border-white/20 text-white font-bold px-8 py-4 rounded-lg text-lg transition-colors"
>
Create Your Profile
</Link>
</div>
</div>
</section>
{/* Stats */}
<section className="border-y border-white/10 py-8">
<div className="max-w-6xl mx-auto px-4 grid grid-cols-2 md:grid-cols-4 gap-6">
{STATS?.map((s) => (
<div key={s?.label} className="text-center">
<div className="text-3xl font-black text-[#CC0000]">{s?.value}</div>
<div className="text-sm text-gray-400 mt-1">{s?.label}</div>
</div>
))}
</div>
</section>
{/* Categories */}
<section className="py-16 px-4">
<div className="max-w-6xl mx-auto">
<h2 className="text-2xl font-black uppercase tracking-wide text-center mb-10">
Browse by Category
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{CATEGORIES?.map((cat) => (
<Link
key={cat?.label}
href={`/marketplace/search?category=${encodeURIComponent(cat?.label)}`}
className="bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/40 rounded-xl p-6 transition-all group"
>
<div className="text-3xl mb-3">{cat?.icon}</div>
<div className="font-bold text-white group-hover:text-[#CC0000] transition-colors">
{cat?.label}
</div>
<div className="text-sm text-gray-500 mt-1">{cat?.count}</div>
</Link>
))}
</div>
</div>
</section>
{/* Three Columns: Find / Post / Join */}
<section className="py-16 px-4 bg-[#111]">
<div className="max-w-6xl mx-auto grid md:grid-cols-3 gap-8">
<div className="text-center p-8">
<div className="w-16 h-16 bg-[#CC0000]/20 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">
🔍
</div>
<h3 className="text-xl font-bold mb-3">Find Talent</h3>
<p className="text-gray-400 text-sm leading-relaxed mb-6">
Search our directory of verified broadcast professionals and production vendors.
Filter by location, specialty, rate, and union status.
</p>
<Link
href="/marketplace/search"
className="text-[#CC0000] font-semibold hover:underline text-sm"
>
Search Directory
</Link>
</div>
<div className="text-center p-8 border-x border-white/10">
<div className="w-16 h-16 bg-[#CC0000]/20 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">
📋
</div>
<h3 className="text-xl font-bold mb-3">Post Jobs & Gigs</h3>
<p className="text-gray-400 text-sm leading-relaxed mb-6">
Post full-time positions or short-term crew calls. Reach thousands of qualified
broadcast professionals instantly.
</p>
<div className="flex flex-col gap-2">
<Link
href="/marketplace/jobs"
className="text-[#CC0000] font-semibold hover:underline text-sm"
>
Browse Jobs
</Link>
<Link
href="/marketplace/gigs"
className="text-[#CC0000] font-semibold hover:underline text-sm"
>
Browse Gigs
</Link>
</div>
</div>
<div className="text-center p-8">
<div className="w-16 h-16 bg-[#CC0000]/20 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">
</div>
<h3 className="text-xl font-bold mb-3">Get Discovered</h3>
<p className="text-gray-400 text-sm leading-relaxed mb-6">
Create your free profile, get verified by our team, and start receiving leads from
clients and employers across the industry.
</p>
<Link
href="/marketplace/join"
className="text-[#CC0000] font-semibold hover:underline text-sm"
>
Create Profile
</Link>
</div>
</div>
</section>
{/* Pricing teaser */}
<section className="py-16 px-4">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-2xl font-black uppercase tracking-wide mb-4">
Simple, Transparent Pricing
</h2>
<p className="text-gray-400 mb-10">
Start free. Upgrade when you&apos;re ready to grow.
</p>
<div className="grid md:grid-cols-3 gap-6">
{[
{
name: 'Free',
price: '$0',
period: 'forever',
features: ['Basic profile', 'Appear in search', '3 gig responses/month'],
cta: 'Get Started',
href: '/marketplace/join',
highlight: false,
},
{
name: 'Pro',
price: '$9.99',
period: '/month',
features: [
'Priority placement',
'Unlimited gig responses',
'Post jobs & gigs',
'Profile analytics',
],
cta: 'Go Pro',
href: '/marketplace/join',
highlight: true,
},
{
name: 'Studio / Vendor',
price: '$29.99',
period: '/month',
features: [
'Company profile',
'Up to 5 staff profiles',
'Featured placement',
'Priority lead routing',
],
cta: 'Get Started',
href: '/marketplace/join',
highlight: false,
},
]?.map((plan) => (
<div
key={plan?.name}
className={`rounded-xl p-6 border ${
plan?.highlight
? 'bg-[#CC0000]/10 border-[#CC0000] relative'
: 'bg-[#1a1a1a] border-white/10'
}`}
>
{plan?.highlight && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-[#CC0000] text-white text-xs font-bold px-3 py-1 rounded-full">
MOST POPULAR
</div>
)}
<div className="font-bold text-lg mb-1">{plan?.name}</div>
<div className="text-3xl font-black text-[#CC0000]">
{plan?.price}
<span className="text-sm text-gray-400 font-normal">{plan?.period}</span>
</div>
<ul className="mt-4 mb-6 space-y-2">
{plan?.features?.map((f) => (
<li key={f} className="text-sm text-gray-300 flex items-center gap-2">
<span className="text-[#CC0000]"></span> {f}
</li>
))}
</ul>
<Link
href={plan?.href}
className={`block text-center py-2.5 rounded-lg font-bold text-sm transition-colors ${
plan?.highlight
? 'bg-[#CC0000] hover:bg-red-700 text-white'
: 'bg-white/10 hover:bg-white/20 text-white'
}`}
>
{plan?.cta}
</Link>
</div>
))}
</div>
{/* Stripe coming soon notice */}
<div className="mt-8 bg-[#1a1a1a] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400">
💳 <strong>Billing coming soon</strong> Pro and Studio plans are available now.
Payment processing will be enabled shortly. Create your free profile today and upgrade
when billing launches.
</div>
</div>
</section>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,193 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
export default function ProfilePage() {
const params = useParams();
const slug = params?.slug as string;
const supabase = createClient();
const [profile, setProfile] = useState<any>(null);
const [details, setDetails] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [notFoundState, setNotFoundState] = useState(false);
useEffect(() => {
if (!slug) return;
const fetchProfile = async () => {
const { data: p } = await supabase
.from('marketplace_profiles')
.select('*')
.eq('slug', slug)
.eq('status', 'active')
.maybeSingle();
if (!p) { setNotFoundState(true); setLoading(false); return; }
setProfile(p);
if (p.profile_type === 'crew') {
const { data: d } = await supabase
.from('marketplace_crew_details')
.select('*')
.eq('profile_id', p.id)
.maybeSingle();
setDetails(d);
} else {
const { data: d } = await supabase
.from('marketplace_vendor_details')
.select('*')
.eq('profile_id', p.id)
.maybeSingle();
setDetails(d);
}
setLoading(false);
};
fetchProfile();
}, [slug]);
if (loading) {
return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (notFoundState || !profile || !details) {
return (
<div className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center">
<div className="text-center">
<div className="text-4xl mb-4">👤</div>
<h1 className="text-2xl font-bold mb-2">Profile Not Found</h1>
<p className="text-gray-400 mb-6">This profile may be pending review or does not exist.</p>
<Link href="/marketplace/search" className="text-[#CC0000] hover:underline">Browse Directory</Link>
</div>
</div>
);
}
const isCrew = profile.profile_type === 'crew';
const name = isCrew ? details.name : details.company_name;
const photoUrl = isCrew ? details.headshot_url : details.logo_url;
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-4xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
<Link href="/marketplace" className="hover:text-[#CC0000]">Marketplace</Link>
<span>/</span>
<Link href="/marketplace/search" className="hover:text-[#CC0000]">Search</Link>
<span>/</span>
<span className="text-white">{name}</span>
</div>
<div className="bg-[#1a1a1a] rounded-2xl overflow-hidden border border-white/10">
<div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8">
<div className="flex flex-col sm:flex-row items-start gap-6">
<div className="w-24 h-24 rounded-full bg-[#CC0000]/20 flex-shrink-0 overflow-hidden flex items-center justify-center text-3xl font-black text-[#CC0000] border-2 border-[#CC0000]/30">
{photoUrl ? (
<img src={photoUrl} alt={`${name} profile photo`} className="w-full h-full object-cover" />
) : (
name?.charAt(0)?.toUpperCase()
)}
</div>
<div className="flex-1">
<div className="flex flex-wrap items-center gap-2 mb-2">
<h1 className="text-2xl font-black">{name}</h1>
{profile.subscription_tier === 'featured' && (
<span className="text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 px-2 py-1 rounded-full font-bold"> Featured</span>
)}
<span className="text-xs bg-green-500/20 text-green-400 border border-green-500/30 px-2 py-1 rounded-full font-bold"> Verified</span>
<span className={`text-xs px-2 py-1 rounded-full font-bold ${isCrew ? 'bg-blue-500/20 text-blue-400 border border-blue-500/30' : 'bg-purple-500/20 text-purple-400 border border-purple-500/30'}`}>
{isCrew ? 'Crew Member' : 'Vendor / Company'}
</span>
</div>
<div className="flex flex-wrap gap-4 text-sm text-gray-400">
<span>📍 {details.location}</span>
{isCrew && details.day_rate && <span>💵 ${details.day_rate.toLocaleString()}/day</span>}
{isCrew && details.union_status && details.union_status !== 'non_union' && (
<span className="uppercase text-yellow-400">🎭 {details.union_status.replace('_', '-')}</span>
)}
{!isCrew && details.category && <span>🏢 {details.category}</span>}
</div>
</div>
</div>
</div>
<div className="p-8 space-y-8">
{(details.bio || details.description) && (
<section>
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-3">About</h2>
<p className="text-gray-300 leading-relaxed">{details.bio || details.description}</p>
</section>
)}
{isCrew && details.skills && details.skills.length > 0 && (
<section>
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-3">Skills & Specialties</h2>
<div className="flex flex-wrap gap-2">
{details.skills.map((skill: string) => (
<span key={skill} className="text-sm bg-[#CC0000]/10 border border-[#CC0000]/30 text-[#CC0000] px-3 py-1 rounded-full">{skill}</span>
))}
</div>
</section>
)}
{isCrew && details.equipment && details.equipment.length > 0 && (
<section>
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-3">Equipment Owned</h2>
<div className="flex flex-wrap gap-2">
{details.equipment.map((item: string) => (
<span key={item} className="text-sm bg-white/5 border border-white/10 text-gray-300 px-3 py-1 rounded-full">{item}</span>
))}
</div>
</section>
)}
{isCrew && details.reel_url && (
<section>
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-3">Demo Reel</h2>
<a href={details.reel_url} target="_blank" rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-[#CC0000]/10 hover:bg-[#CC0000]/20 border border-[#CC0000]/30 text-[#CC0000] px-4 py-2 rounded-lg text-sm font-semibold transition-colors">
Watch Reel
</a>
</section>
)}
{!isCrew && (
<section>
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-3">Contact & Links</h2>
<div className="flex flex-wrap gap-3">
{details.website && (
<a href={details.website} target="_blank" rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-white/5 hover:bg-white/10 border border-white/10 text-white px-4 py-2 rounded-lg text-sm transition-colors">
🌐 Website
</a>
)}
{details.contact_email && (
<a href={`mailto:${details.contact_email}`}
className="inline-flex items-center gap-2 bg-[#CC0000]/10 hover:bg-[#CC0000]/20 border border-[#CC0000]/30 text-[#CC0000] px-4 py-2 rounded-lg text-sm font-semibold transition-colors">
Contact
</a>
)}
</div>
</section>
)}
<div className="border-t border-white/10 pt-6 flex items-center justify-between text-xs text-gray-500">
<span>Member since {new Date(profile.created_at).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}</span>
<Link href="/marketplace/search" className="text-[#CC0000] hover:underline"> Back to Search</Link>
</div>
</div>
</div>
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,533 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { searchProfiles, SearchFilters } from '@/lib/marketplace/typesenseClient';
import { createClient } from '@/lib/supabase/client';
import { Suspense } from 'react';
const SKILL_OPTIONS = [
'Director of Photography',
'Camera Operator',
'DIT',
'Gaffer',
'Grip',
'Production Sound Mixer',
'Boom Operator',
'Sound Designer',
'Director',
'Producer',
'Editor',
'Colorist',
'VFX Artist',
'Technical Director',
'Broadcast Engineer',
'Livestream Producer',
];
const VENDOR_CATEGORIES = [
'Full-Service Production Company',
'Equipment Rental House',
'Studio / Stage Rental',
'Post Production Facility',
'Animation Studio',
'Drone Services Company',
];
interface ProfileCard {
id: string;
slug: string;
type: 'crew' | 'vendor';
name: string;
location: string;
category?: string;
skills?: string[];
day_rate?: number;
bio?: string;
headshot_url?: string;
logo_url?: string;
featured: boolean;
union_status?: string;
}
export default function MarketplaceSearchPage() {
return (
<Suspense
fallback={
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div>
}
>
<SearchPageInner />
</Suspense>
);
}
function SearchPageInner() {
const searchParams = useSearchParams();
const router = useRouter();
const supabase = createClient();
const [query, setQuery] = useState(searchParams.get('q') || '');
const [filterType, setFilterType] = useState<'all' | 'crew' | 'vendor'>(
(searchParams.get('type') as any) || 'all'
);
const [filterLocation, setFilterLocation] = useState(searchParams.get('location') || '');
const [filterCategory, setFilterCategory] = useState(searchParams.get('category') || '');
const [filterUnion, setFilterUnion] = useState('');
const [filterRateMin, setFilterRateMin] = useState('');
const [filterRateMax, setFilterRateMax] = useState('');
const [featuredOnly, setFeaturedOnly] = useState(false);
const [results, setResults] = useState<ProfileCard[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [typesenseAvailable, setTypesenseAvailable] = useState(true);
const fetchFromSupabase = useCallback(async (filters: SearchFilters) => {
// Fallback: query Supabase directly when Typesense is unavailable
let crewQuery = supabase
.from('marketplace_crew_details')
.select(`
*,
marketplace_profiles!inner(id, slug, status, subscription_tier)
`)
.eq('marketplace_profiles.status', 'active');
if (filters.query && filters.query !== '*') {
crewQuery = crewQuery.ilike('name', `%${filters.query}%`);
}
if (filters.location) {
crewQuery = crewQuery.ilike('location', `%${filters.location}%`);
}
let vendorQuery = supabase
.from('marketplace_vendor_details')
.select(`
*,
marketplace_profiles!inner(id, slug, status, subscription_tier)
`)
.eq('marketplace_profiles.status', 'active');
if (filters.query && filters.query !== '*') {
vendorQuery = vendorQuery.ilike('company_name', `%${filters.query}%`);
}
if (filters.category) {
vendorQuery = vendorQuery.ilike('category', `%${filters.category}%`);
}
const cards: ProfileCard[] = [];
if (filters.type === 'all' || filters.type === 'crew') {
const { data: crewData } = await crewQuery.limit(20);
(crewData || []).forEach((row: any) => {
cards.push({
id: row.marketplace_profiles?.id,
slug: row.marketplace_profiles?.slug,
type: 'crew',
name: row.name,
location: row.location,
skills: row.skills || [],
day_rate: row.day_rate,
bio: row.bio,
headshot_url: row.headshot_url,
featured: row.marketplace_profiles?.subscription_tier === 'featured',
union_status: row.union_status,
});
});
}
if (filters.type === 'all' || filters.type === 'vendor') {
const { data: vendorData } = await vendorQuery.limit(20);
(vendorData || []).forEach((row: any) => {
cards.push({
id: row.marketplace_profiles?.id,
slug: row.marketplace_profiles?.slug,
type: 'vendor',
name: row.company_name,
location: row.location,
category: row.category,
bio: row.description,
logo_url: row.logo_url,
featured: row.marketplace_profiles?.subscription_tier === 'featured',
});
});
}
return cards;
}, [supabase]);
const doSearch = useCallback(async () => {
setLoading(true);
setError('');
const filters: SearchFilters = {
query: query || '*',
type: filterType,
location: filterLocation || undefined,
category: filterCategory || undefined,
union_status: filterUnion || undefined,
day_rate_min: filterRateMin ? parseInt(filterRateMin) : undefined,
day_rate_max: filterRateMax ? parseInt(filterRateMax) : undefined,
featured_only: featuredOnly,
page,
per_page: 20,
};
try {
const { crew, vendors, error: tsError } = await searchProfiles(filters);
if (tsError || (!crew && !vendors)) {
// Typesense unavailable — fall back to Supabase
setTypesenseAvailable(false);
const cards = await fetchFromSupabase(filters);
setResults(cards);
setTotal(cards.length);
} else {
setTypesenseAvailable(true);
const cards: ProfileCard[] = [];
(crew?.hits || []).forEach((hit: any) => {
const doc = hit.document;
cards.push({
id: doc.id,
slug: doc.slug,
type: 'crew',
name: doc.name,
location: doc.location,
skills: doc.skills || [],
day_rate: doc.day_rate,
bio: doc.bio,
headshot_url: doc.headshot_url,
featured: doc.featured,
union_status: doc.union_status,
});
});
(vendors?.hits || []).forEach((hit: any) => {
const doc = hit.document;
cards.push({
id: doc.id,
slug: doc.slug,
type: 'vendor',
name: doc.company_name,
location: doc.location,
category: doc.category,
bio: doc.description,
logo_url: doc.logo_url,
featured: doc.featured,
});
});
setResults(cards);
setTotal((crew?.found || 0) + (vendors?.found || 0));
}
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
}, [query, filterType, filterLocation, filterCategory, filterUnion, filterRateMin, filterRateMax, featuredOnly, page, fetchFromSupabase]);
useEffect(() => {
doSearch();
}, [doSearch]);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setPage(1);
doSearch();
};
return (
<div className="min-h-screen bg-[#0d0d0d] text-white">
<Header />
<div className="max-w-7xl mx-auto px-4 py-24">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-gray-500 mb-6">
<Link href="/marketplace" className="hover:text-[#CC0000]">
Marketplace
</Link>
<span>/</span>
<span className="text-white">Search</span>
</div>
<h1 className="text-3xl font-black uppercase tracking-wide mb-8">
Find Crew & Vendors
</h1>
{!typesenseAvailable && (
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg px-4 py-3 text-sm text-yellow-400 mb-6">
Search is running in basic mode. Connect Typesense for full-text search and advanced
filtering.
</div>
)}
<div className="flex flex-col lg:flex-row gap-8">
{/* Sidebar filters */}
<aside className="lg:w-64 flex-shrink-0">
<form onSubmit={handleSearch} className="space-y-5">
<div>
<label className="block text-xs text-gray-400 uppercase tracking-wide mb-2">
Search
</label>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Name, skill, company..."
/>
</div>
<div>
<label className="block text-xs text-gray-400 uppercase tracking-wide mb-2">
Type
</label>
<div className="flex gap-2">
{(['all', 'crew', 'vendor'] as const).map((t) => (
<button
key={t}
type="button"
onClick={() => setFilterType(t)}
className={`flex-1 py-2 text-xs font-bold rounded-lg border transition-colors capitalize ${
filterType === t
? 'bg-[#CC0000] border-[#CC0000] text-white'
: 'bg-transparent border-white/20 text-gray-400 hover:border-white/40'
}`}
>
{t}
</button>
))}
</div>
</div>
<div>
<label className="block text-xs text-gray-400 uppercase tracking-wide mb-2">
Location
</label>
<input
value={filterLocation}
onChange={(e) => setFilterLocation(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="City, State..."
/>
</div>
{(filterType === 'all' || filterType === 'crew') && (
<>
<div>
<label className="block text-xs text-gray-400 uppercase tracking-wide mb-2">
Union Status
</label>
<select
value={filterUnion}
onChange={(e) => setFilterUnion(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
>
<option value="">Any</option>
<option value="non_union">Non-Union</option>
<option value="iatse">IATSE</option>
<option value="sag_aftra">SAG-AFTRA</option>
<option value="teamsters">Teamsters</option>
</select>
</div>
<div>
<label className="block text-xs text-gray-400 uppercase tracking-wide mb-2">
Day Rate Range
</label>
<div className="flex gap-2">
<input
type="number"
value={filterRateMin}
onChange={(e) => setFilterRateMin(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Min $"
/>
<input
type="number"
value={filterRateMax}
onChange={(e) => setFilterRateMax(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Max $"
/>
</div>
</div>
</>
)}
{(filterType === 'all' || filterType === 'vendor') && (
<div>
<label className="block text-xs text-gray-400 uppercase tracking-wide mb-2">
Vendor Category
</label>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
>
<option value="">All Categories</option>
{VENDOR_CATEGORIES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
)}
<div className="flex items-center gap-2">
<input
type="checkbox"
id="featured"
checked={featuredOnly}
onChange={(e) => setFeaturedOnly(e.target.checked)}
className="accent-[#CC0000]"
/>
<label htmlFor="featured" className="text-sm text-gray-400 cursor-pointer">
Featured only
</label>
</div>
<button
type="submit"
className="w-full bg-[#CC0000] hover:bg-red-700 text-white font-bold py-2.5 rounded-lg text-sm transition-colors"
>
Search
</button>
</form>
</aside>
{/* Results */}
<div className="flex-1">
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-gray-400">
{loading ? 'Searching...' : `${total} result${total !== 1 ? 's' : ''} found`}
</p>
<Link
href="/marketplace/join"
className="text-xs text-[#CC0000] hover:underline font-semibold"
>
+ Add Your Profile
</Link>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/30 rounded-lg px-4 py-3 text-sm text-red-400 mb-4">
{error}
</div>
)}
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="bg-[#1a1a1a] rounded-xl p-5 animate-pulse h-40"
/>
))}
</div>
) : results.length === 0 ? (
<div className="text-center py-20 text-gray-500">
<div className="text-4xl mb-4">🔍</div>
<p className="text-lg font-semibold mb-2">No results found</p>
<p className="text-sm">
Try adjusting your filters or{' '}
<Link href="/marketplace/join" className="text-[#CC0000] hover:underline">
add your profile
</Link>
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{results.map((profile) => (
<Link
key={profile.id}
href={`/marketplace/profile/${profile.slug}`}
className="bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-5 transition-all block"
>
<div className="flex items-start gap-4">
{/* Avatar */}
<div className="w-14 h-14 rounded-full bg-[#CC0000]/20 flex-shrink-0 overflow-hidden flex items-center justify-center text-xl font-bold text-[#CC0000]">
{profile.headshot_url || profile.logo_url ? (
<img
src={profile.headshot_url || profile.logo_url}
alt={`${profile.name} profile photo`}
className="w-full h-full object-cover"
/>
) : (
profile.name.charAt(0).toUpperCase()
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-bold text-white truncate">{profile.name}</span>
{profile.featured && (
<span className="text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 px-2 py-0.5 rounded-full font-semibold">
Featured
</span>
)}
<span
className={`text-xs px-2 py-0.5 rounded-full font-semibold ${
profile.type === 'crew' ?'bg-blue-500/20 text-blue-400' :'bg-purple-500/20 text-purple-400'
}`}
>
{profile.type === 'crew' ? 'Crew' : 'Vendor'}
</span>
</div>
<div className="text-sm text-gray-400 mt-0.5">
{profile.category ||
profile.skills?.slice(0, 2).join(', ') ||
'Broadcast Professional'}
</div>
<div className="text-xs text-gray-500 mt-0.5 flex items-center gap-3">
<span>📍 {profile.location}</span>
{profile.day_rate && (
<span>💵 ${profile.day_rate.toLocaleString()}/day</span>
)}
{profile.union_status && profile.union_status !== 'non_union' && (
<span className="uppercase">{profile.union_status}</span>
)}
</div>
{profile.bio && (
<p className="text-xs text-gray-500 mt-2 line-clamp-2">{profile.bio}</p>
)}
</div>
</div>
</Link>
))}
</div>
)}
{/* Pagination */}
{total > 20 && (
<div className="flex justify-center gap-2 mt-8">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-4 py-2 bg-[#1a1a1a] border border-white/20 rounded-lg text-sm disabled:opacity-40 hover:border-white/40 transition-colors"
>
Prev
</button>
<span className="px-4 py-2 text-sm text-gray-400">Page {page}</span>
<button
onClick={() => setPage((p) => p + 1)}
disabled={results.length < 20}
className="px-4 py-2 bg-[#1a1a1a] border border-white/20 rounded-lg text-sm disabled:opacity-40 hover:border-white/40 transition-colors"
>
Next
</button>
</div>
)}
</div>
</div>
</div>
<Footer />
</div>
);
}