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,128 @@
// Marketplace email notifications
// Uses existing SMTP environment variables
export interface MarketplaceEmailPayload {
to: string;
subject: string;
html: string;
}
async function sendEmail(payload: MarketplaceEmailPayload): Promise<void> {
const res = await fetch('/api/marketplace/send-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || 'Failed to send email');
}
}
export async function sendProfileApprovedEmail(to: string, name: string): Promise<void> {
await sendEmail({
to,
subject: 'Your Broadcast Beat Marketplace Profile is Approved!',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background: #0d0d0d; color: #fff; padding: 32px; border-radius: 8px;">
<div style="text-align: center; margin-bottom: 24px;">
<h1 style="color: #CC0000; font-size: 24px; margin: 0;">BROADCAST BEAT</h1>
<p style="color: #999; font-size: 12px; margin: 4px 0 0;">Production Crew & Vendor Marketplace</p>
</div>
<h2 style="color: #fff; font-size: 20px;">Welcome to the Marketplace, ${name}!</h2>
<p style="color: #ccc; line-height: 1.6;">
Your profile has been reviewed and approved by our team. You are now live in the
Broadcast Beat Production Marketplace and visible to clients, employers, and production companies.
</p>
<div style="background: #1a1a1a; border-left: 4px solid #CC0000; padding: 16px; margin: 24px 0; border-radius: 4px;">
<p style="color: #fff; margin: 0; font-weight: bold;">Next Steps:</p>
<ul style="color: #ccc; margin: 8px 0 0; padding-left: 20px; line-height: 1.8;">
<li>Complete your profile with a reel, credits, and gear list</li>
<li>Upgrade to Pro for priority placement and unlimited gig responses</li>
<li>Browse open gig requests in your specialty</li>
</ul>
</div>
<a href="${process.env.NEXT_PUBLIC_SITE_URL}/marketplace"
style="display: inline-block; background: #CC0000; color: #fff; padding: 12px 24px; border-radius: 4px; text-decoration: none; font-weight: bold;">
View Your Profile
</a>
<p style="color: #666; font-size: 12px; margin-top: 32px;">
Broadcast Beat — Official NAB Show Media Partner<br>
<a href="${process.env.NEXT_PUBLIC_SITE_URL}" style="color: #CC0000;">broadcastbeat.com</a>
</p>
</div>
`,
});
}
export async function sendProfileRejectedEmail(
to: string,
name: string,
reason: string
): Promise<void> {
await sendEmail({
to,
subject: 'Update Required: Your Broadcast Beat Marketplace Profile',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background: #0d0d0d; color: #fff; padding: 32px; border-radius: 8px;">
<div style="text-align: center; margin-bottom: 24px;">
<h1 style="color: #CC0000; font-size: 24px; margin: 0;">BROADCAST BEAT</h1>
<p style="color: #999; font-size: 12px; margin: 4px 0 0;">Production Crew & Vendor Marketplace</p>
</div>
<h2 style="color: #fff; font-size: 20px;">Profile Review — Action Required</h2>
<p style="color: #ccc; line-height: 1.6;">
Hi ${name}, our team has reviewed your marketplace profile and we need a few updates before it can go live.
</p>
<div style="background: #1a1a1a; border-left: 4px solid #f59e0b; padding: 16px; margin: 24px 0; border-radius: 4px;">
<p style="color: #f59e0b; margin: 0 0 8px; font-weight: bold;">Feedback from our team:</p>
<p style="color: #ccc; margin: 0; line-height: 1.6;">${reason}</p>
</div>
<p style="color: #ccc; line-height: 1.6;">
Please log in to your account, update your profile, and resubmit for review.
Our team typically reviews profiles within 24 hours.
</p>
<a href="${process.env.NEXT_PUBLIC_SITE_URL}/marketplace/account"
style="display: inline-block; background: #CC0000; color: #fff; padding: 12px 24px; border-radius: 4px; text-decoration: none; font-weight: bold;">
Update My Profile
</a>
<p style="color: #666; font-size: 12px; margin-top: 32px;">
Questions? Reply to this email or contact us at <a href="mailto:marketplace@broadcastbeat.com" style="color: #CC0000;">marketplace@broadcastbeat.com</a>
</p>
</div>
`,
});
}
export async function sendListingConfirmationEmail(
to: string,
name: string,
listingType: 'job' | 'gig',
title: string
): Promise<void> {
await sendEmail({
to,
subject: `Your ${listingType === 'job' ? 'Job Post' : 'Gig Request'} is Live — ${title}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background: #0d0d0d; color: #fff; padding: 32px; border-radius: 8px;">
<div style="text-align: center; margin-bottom: 24px;">
<h1 style="color: #CC0000; font-size: 24px; margin: 0;">BROADCAST BEAT</h1>
<p style="color: #999; font-size: 12px; margin: 4px 0 0;">Production Crew & Vendor Marketplace</p>
</div>
<h2 style="color: #fff; font-size: 20px;">
Your ${listingType === 'job' ? 'Job Post' : 'Gig Request'} is Live!
</h2>
<p style="color: #ccc; line-height: 1.6;">
<strong style="color: #fff;">"${title}"</strong> has been posted to the Broadcast Beat Marketplace
and is now visible to our network of broadcast and production professionals.
</p>
<a href="${process.env.NEXT_PUBLIC_SITE_URL}/marketplace/${listingType === 'job' ? 'jobs' : 'gigs'}"
style="display: inline-block; background: #CC0000; color: #fff; padding: 12px 24px; border-radius: 4px; text-decoration: none; font-weight: bold;">
View ${listingType === 'job' ? 'Job Board' : 'Gig Board'}
</a>
<p style="color: #666; font-size: 12px; margin-top: 32px;">
Broadcast Beat — Official NAB Show Media Partner
</p>
</div>
`,
});
}

View File

@@ -0,0 +1,130 @@
'use client';
// Typesense client for marketplace search
// Collections: crew_profiles, vendor_profiles
export interface TypesenseCrewProfile {
id: string;
name: string;
location: string;
skills: string[];
union_status: string;
day_rate: number;
bio: string;
slug: string;
featured: boolean;
headshot_url?: string;
}
export interface TypesenseVendorProfile {
id: string;
company_name: string;
location: string;
category: string;
description: string;
slug: string;
featured: boolean;
logo_url?: string;
}
export interface SearchFilters {
type?: 'crew' | 'vendor' | 'all';
location?: string;
skills?: string[];
category?: string;
union_status?: string;
day_rate_min?: number;
day_rate_max?: number;
featured_only?: boolean;
query?: string;
page?: number;
per_page?: number;
}
export interface SearchResult {
hits: Array<{
document: TypesenseCrewProfile | TypesenseVendorProfile;
highlight?: Record<string, any>;
}>;
found: number;
page: number;
out_of: number;
}
const TYPESENSE_HOST = process.env.NEXT_PUBLIC_TYPESENSE_HOST || '';
const TYPESENSE_PORT = process.env.NEXT_PUBLIC_TYPESENSE_PORT || '8108';
const TYPESENSE_PROTOCOL = process.env.NEXT_PUBLIC_TYPESENSE_PROTOCOL || 'http';
const TYPESENSE_API_KEY = process.env.NEXT_PUBLIC_TYPESENSE_SEARCH_KEY || '';
function buildBaseUrl(): string {
return `${TYPESENSE_PROTOCOL}://${TYPESENSE_HOST}:${TYPESENSE_PORT}`;
}
export async function searchProfiles(filters: SearchFilters): Promise<{
crew: SearchResult | null;
vendors: SearchResult | null;
error?: string;
}> {
if (!TYPESENSE_HOST) {
return { crew: null, vendors: null, error: 'Search service not configured' };
}
const q = filters.query?.trim() || '*';
const page = filters.page || 1;
const per_page = filters.per_page || 20;
try {
const results: { crew: SearchResult | null; vendors: SearchResult | null } = {
crew: null,
vendors: null,
};
const searchType = filters.type || 'all';
if (searchType === 'all' || searchType === 'crew') {
const crewParams = new URLSearchParams({
q,
query_by: 'name,skills,bio,location',
page: String(page),
per_page: String(per_page),
});
const filterParts: string[] = [];
if (filters.union_status) filterParts.push(`union_status:=${filters.union_status}`);
if (filters.day_rate_min) filterParts.push(`day_rate:>=${filters.day_rate_min}`);
if (filters.day_rate_max) filterParts.push(`day_rate:<=${filters.day_rate_max}`);
if (filters.featured_only) filterParts.push('featured:=true');
if (filterParts.length) crewParams.set('filter_by', filterParts.join(' && '));
const crewRes = await fetch(
`${buildBaseUrl()}/collections/crew_profiles/documents/search?${crewParams}`,
{ headers: { 'X-TYPESENSE-API-KEY': TYPESENSE_API_KEY } }
);
if (crewRes.ok) results.crew = await crewRes.json();
}
if (searchType === 'all' || searchType === 'vendor') {
const vendorParams = new URLSearchParams({
q,
query_by: 'company_name,category,description,location',
page: String(page),
per_page: String(per_page),
});
const filterParts: string[] = [];
if (filters.category) filterParts.push(`category:=${filters.category}`);
if (filters.featured_only) filterParts.push('featured:=true');
if (filterParts.length) vendorParams.set('filter_by', filterParts.join(' && '));
const vendorRes = await fetch(
`${buildBaseUrl()}/collections/vendor_profiles/documents/search?${vendorParams}`,
{ headers: { 'X-TYPESENSE-API-KEY': TYPESENSE_API_KEY } }
);
if (vendorRes.ok) results.vendors = await vendorRes.json();
}
return results;
} catch (err: any) {
return { crew: null, vendors: null, error: err.message };
}
}