initial commit: rocket.new export of broadcastbeat
This commit is contained in:
128
src/lib/marketplace/emailService.ts
Normal file
128
src/lib/marketplace/emailService.ts
Normal 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>
|
||||
`,
|
||||
});
|
||||
}
|
||||
130
src/lib/marketplace/typesenseClient.ts
Normal file
130
src/lib/marketplace/typesenseClient.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user