initial commit: rocket.new export of broadcastbeat
This commit is contained in:
122
src/app/marketplace/jobs/[id]/page.tsx
Normal file
122
src/app/marketplace/jobs/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
112
src/app/marketplace/jobs/page.tsx
Normal file
112
src/app/marketplace/jobs/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user