initial commit: rocket.new export of broadcastbeat
This commit is contained in:
76
src/app/api/newsletter/archive/route.ts
Normal file
76
src/app/api/newsletter/archive/route.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const topic = searchParams.get('topic') || '';
|
||||
const year = searchParams.get('year') || '';
|
||||
const month = searchParams.get('month') || '';
|
||||
|
||||
// Use service role or anon — templates are public read via this route
|
||||
const supabase = await createClient();
|
||||
|
||||
let query = supabase
|
||||
.from('newsletter_templates')
|
||||
.select('id, name, description, layout, subject_prefix, header_text, accent_color, article_blocks, created_at, updated_at')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
// Filter by year
|
||||
if (year) {
|
||||
const start = `${year}-01-01T00:00:00.000Z`;
|
||||
const end = `${parseInt(year) + 1}-01-01T00:00:00.000Z`;
|
||||
query = query.gte('created_at', start).lt('created_at', end);
|
||||
}
|
||||
|
||||
// Filter by month (requires year too)
|
||||
if (year && month) {
|
||||
const paddedMonth = month.padStart(2, '0');
|
||||
const nextMonth = parseInt(month) === 12 ? '01' : String(parseInt(month) + 1).padStart(2, '0');
|
||||
const nextYear = parseInt(month) === 12 ? String(parseInt(year) + 1) : year;
|
||||
const start = `${year}-${paddedMonth}-01T00:00:00.000Z`;
|
||||
const end = `${nextYear}-${nextMonth}-01T00:00:00.000Z`;
|
||||
query = query.gte('created_at', start).lt('created_at', end);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
console.error('Archive fetch error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
let newsletters = data || [];
|
||||
|
||||
// Filter by topic (article_blocks contain category fields)
|
||||
if (topic) {
|
||||
const topicLower = topic.toLowerCase();
|
||||
newsletters = newsletters.filter((n) => {
|
||||
const blocks = Array.isArray(n.article_blocks) ? n.article_blocks : [];
|
||||
const nameMatch = n.name?.toLowerCase().includes(topicLower);
|
||||
const descMatch = n.description?.toLowerCase().includes(topicLower);
|
||||
const blockMatch = blocks.some(
|
||||
(b: { category?: string; title?: string }) =>
|
||||
b.category?.toLowerCase().includes(topicLower) ||
|
||||
b.title?.toLowerCase().includes(topicLower)
|
||||
);
|
||||
return nameMatch || descMatch || blockMatch;
|
||||
});
|
||||
}
|
||||
|
||||
// Derive available years from all records for filter UI
|
||||
const { data: allDates } = await supabase
|
||||
.from('newsletter_templates')
|
||||
.select('created_at')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
const years = Array.from(
|
||||
new Set((allDates || []).map((r) => new Date(r.created_at).getFullYear()))
|
||||
).sort((a, b) => b - a);
|
||||
|
||||
return NextResponse.json({ newsletters, years, total: newsletters.length });
|
||||
} catch (err) {
|
||||
console.error('Archive error:', err);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
75
src/app/api/newsletter/notify-article/route.ts
Normal file
75
src/app/api/newsletter/notify-article/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const { article } = body;
|
||||
|
||||
if (!article || !article.title) {
|
||||
return NextResponse.json({ error: 'Article data is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch all active subscribers
|
||||
const { data: subscribers, error: subError } = await supabase
|
||||
.from('newsletter_subscribers')
|
||||
.select('email')
|
||||
.eq('status', 'active');
|
||||
|
||||
if (subError) {
|
||||
return NextResponse.json({ error: subError.message }, { status: 500 });
|
||||
}
|
||||
|
||||
if (!subscribers || subscribers.length === 0) {
|
||||
return NextResponse.json({ success: true, sent: 0, message: 'No active subscribers' });
|
||||
}
|
||||
|
||||
const subscriberEmails = subscribers.map((s: { email: string }) => s.email);
|
||||
|
||||
// Call the Supabase Edge Function
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
return NextResponse.json({ error: 'Supabase configuration missing' }, { status: 500 });
|
||||
}
|
||||
|
||||
const edgeFnUrl = `${supabaseUrl}/functions/v1/send-article-notification`;
|
||||
|
||||
const response = await fetch(edgeFnUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${supabaseAnonKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
article,
|
||||
subscribers: subscriberEmails,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Edge function error:', result);
|
||||
return NextResponse.json(
|
||||
{ error: result.error || 'Failed to send notifications' },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
sent: result.sent,
|
||||
failed: result.failed,
|
||||
total: subscriberEmails.length,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
console.error('notify-article error:', err);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
76
src/app/api/newsletter/subscribe/route.ts
Normal file
76
src/app/api/newsletter/subscribe/route.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { email, topics } = await req.json();
|
||||
|
||||
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return NextResponse.json({ error: 'Valid email is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
|
||||
// Upsert subscriber (re-subscribe if previously unsubscribed)
|
||||
const { error } = await supabase
|
||||
.from('newsletter_subscribers')
|
||||
.upsert(
|
||||
{
|
||||
email: email.toLowerCase().trim(),
|
||||
topics: topics || [],
|
||||
status: 'active',
|
||||
subscribed_at: new Date().toISOString(),
|
||||
unsubscribed_at: null,
|
||||
},
|
||||
{ onConflict: 'email' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase error:', error);
|
||||
return NextResponse.json({ error: 'Failed to save subscription' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Send confirmation email via SMTP
|
||||
const smtpHost = process.env.SMTP_HOST;
|
||||
const smtpPort = parseInt(process.env.SMTP_PORT || '587');
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
||||
const fromName = process.env.SMTP_FROM_NAME || 'BroadcastBeat';
|
||||
|
||||
if (smtpHost && smtpUser && smtpPass) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: { user: smtpUser, pass: smtpPass },
|
||||
});
|
||||
|
||||
const topicsList = topics && topics.length > 0
|
||||
? `<p style="color:#aaa;font-size:14px;">You'll receive updates on: <strong style="color:#3b82f6;">${topics.join(', ')}</strong></p>`
|
||||
: '';
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${fromName}" <${fromEmail}>`,
|
||||
to: email,
|
||||
subject: `Welcome to BroadcastBeat — You're subscribed!`,
|
||||
html: `
|
||||
<div style="background:#0d1520;padding:40px 20px;font-family:sans-serif;max-width:600px;margin:0 auto;">
|
||||
<h1 style="color:#3b82f6;font-size:24px;margin-bottom:8px;">You're in!</h1>
|
||||
<p style="color:#e0e0e0;font-size:16px;">Thanks for subscribing to <strong>BroadcastBeat</strong> — the digital platform for broadcast engineering.</p>
|
||||
${topicsList}
|
||||
<p style="color:#aaa;font-size:14px;margin-top:24px;">You'll be the first to know about the latest news, product launches, and industry insights.</p>
|
||||
<hr style="border:none;border-top:1px solid #1e3a5f;margin:32px 0;" />
|
||||
<p style="color:#555;font-size:12px;">You're receiving this because you signed up at broadcastbeat.com. <a href="${process.env.NEXT_PUBLIC_SITE_URL}/unsubscribe?email=${encodeURIComponent(email)}" style="color:#3b82f6;">Unsubscribe</a></p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err: unknown) {
|
||||
console.error('Subscribe error:', err);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
187
src/app/api/newsletter/subscribers/route.ts
Normal file
187
src/app/api/newsletter/subscribers/route.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
// GET: list all subscribers
|
||||
export async function GET(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const status = searchParams.get('status') || 'all';
|
||||
const search = searchParams.get('search') || '';
|
||||
|
||||
let query = supabase
|
||||
.from('newsletter_subscribers')
|
||||
.select('*')
|
||||
.order('subscribed_at', { ascending: false });
|
||||
|
||||
if (status !== 'all') query = query.eq('status', status);
|
||||
if (search) query = query.ilike('email', `%${search}%`);
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ subscribers: data || [] });
|
||||
}
|
||||
|
||||
// DELETE: unsubscribe a subscriber
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await req.json();
|
||||
if (!id) return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||
|
||||
const { error } = await supabase
|
||||
.from('newsletter_subscribers')
|
||||
.update({ status: 'unsubscribed', unsubscribed_at: new Date().toISOString() })
|
||||
.eq('id', id);
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
// PATCH: bulk actions (bulk_unsubscribe, bulk_delete)
|
||||
export async function PATCH(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { action, ids } = await req.json();
|
||||
if (!action || !Array.isArray(ids) || ids.length === 0) {
|
||||
return NextResponse.json({ error: 'action and ids[] required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (action === 'bulk_unsubscribe') {
|
||||
const { error } = await supabase
|
||||
.from('newsletter_subscribers')
|
||||
.update({ status: 'unsubscribed', unsubscribed_at: new Date().toISOString() })
|
||||
.in('id', ids)
|
||||
.eq('status', 'active');
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ success: true, count: ids.length });
|
||||
}
|
||||
|
||||
if (action === 'bulk_delete') {
|
||||
const { error } = await supabase
|
||||
.from('newsletter_subscribers')
|
||||
.delete()
|
||||
.in('id', ids);
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ success: true, count: ids.length });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
||||
}
|
||||
|
||||
// POST: send digest email to all active subscribers
|
||||
export async function POST(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { subject, previewText, articles, customMessage } = await req.json();
|
||||
|
||||
if (!subject) return NextResponse.json({ error: 'Subject is required' }, { status: 400 });
|
||||
if (!articles || articles.length === 0) {
|
||||
return NextResponse.json({ error: 'At least one article is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const smtpHost = process.env.SMTP_HOST;
|
||||
const smtpPort = parseInt(process.env.SMTP_PORT || '587');
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
||||
const fromName = process.env.SMTP_FROM_NAME || 'BroadcastBeat';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||
return NextResponse.json({ error: 'SMTP credentials not configured' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Fetch active subscribers
|
||||
const { data: subscribers, error: subError } = await supabase
|
||||
.from('newsletter_subscribers')
|
||||
.select('email')
|
||||
.eq('status', 'active');
|
||||
|
||||
if (subError) return NextResponse.json({ error: subError.message }, { status: 500 });
|
||||
if (!subscribers || subscribers.length === 0) {
|
||||
return NextResponse.json({ error: 'No active subscribers found' }, { status: 400 });
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: { user: smtpUser, pass: smtpPass },
|
||||
});
|
||||
|
||||
// Build article HTML blocks
|
||||
const articlesHtml = articles
|
||||
.map(
|
||||
(a: { title: string; excerpt?: string; url?: string; image?: string; category?: string }) => `
|
||||
<div style="border-bottom:1px solid #1e3a5f;padding:20px 0;">
|
||||
${a.category ? `<span style="color:#3b82f6;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:1px;">${a.category}</span>` : ''}
|
||||
<h2 style="color:#e0e0e0;font-size:18px;margin:8px 0 6px;">
|
||||
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#e0e0e0;text-decoration:none;">${a.title}</a>` : a.title}
|
||||
</h2>
|
||||
${a.excerpt ? `<p style="color:#888;font-size:14px;margin:0 0 10px;">${a.excerpt}</p>` : ''}
|
||||
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#3b82f6;font-size:13px;text-decoration:none;">Read more →</a>` : ''}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
const emailHtml = `
|
||||
<div style="background:#0d1520;padding:0;font-family:sans-serif;max-width:600px;margin:0 auto;">
|
||||
<!-- Header -->
|
||||
<div style="background:#111;border-bottom:2px solid #3b82f6;padding:24px 32px;">
|
||||
<h1 style="color:#3b82f6;font-size:22px;margin:0;font-weight:bold;">BroadcastBeat</h1>
|
||||
<p style="color:#555;font-size:12px;margin:4px 0 0;">Digital Platform for Broadcast Engineering</p>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div style="padding:32px;">
|
||||
${previewText ? `<p style="color:#aaa;font-size:15px;margin:0 0 24px;">${previewText}</p>` : ''}
|
||||
${customMessage ? `<div style="background:#1a2535;border-left:3px solid #3b82f6;padding:16px;margin-bottom:24px;"><p style="color:#e0e0e0;font-size:14px;margin:0;">${customMessage}</p></div>` : ''}
|
||||
${articlesHtml}
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<div style="background:#0a0f1a;padding:24px 32px;border-top:1px solid #1e3a5f;">
|
||||
<p style="color:#555;font-size:12px;margin:0;">You're receiving this because you subscribed at broadcastbeat.com.</p>
|
||||
<p style="color:#555;font-size:12px;margin:8px 0 0;">
|
||||
<a href="${siteUrl}" style="color:#3b82f6;">Visit BroadcastBeat</a> ·
|
||||
<a href="${siteUrl}/unsubscribe?email={{email}}" style="color:#3b82f6;">Unsubscribe</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Send to all subscribers (batch with BCC or individual)
|
||||
const recipientEmails = subscribers.map((s) => s.email);
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
|
||||
// Send in batches of 50 to avoid rate limits
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < recipientEmails.length; i += batchSize) {
|
||||
const batch = recipientEmails.slice(i, i + batchSize);
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: `"${fromName}" <${fromEmail}>`,
|
||||
bcc: batch,
|
||||
subject,
|
||||
html: emailHtml,
|
||||
});
|
||||
sent += batch.length;
|
||||
} catch (err) {
|
||||
console.error('Batch send error:', err);
|
||||
failed += batch.length;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, sent, failed, total: recipientEmails.length });
|
||||
}
|
||||
96
src/app/api/newsletter/templates/route.ts
Normal file
96
src/app/api/newsletter/templates/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
// GET: list all templates
|
||||
export async function GET() {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('newsletter_templates')
|
||||
.select('*')
|
||||
.order('is_preset', { ascending: false })
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ templates: data || [] });
|
||||
}
|
||||
|
||||
// POST: create a new template
|
||||
export async function POST(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const { name, description, layout, style, subject_prefix, header_text, footer_text, accent_color, article_blocks } = body;
|
||||
|
||||
if (!name?.trim()) return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('newsletter_templates')
|
||||
.insert({
|
||||
name: name.trim(),
|
||||
description: description || '',
|
||||
layout: layout || 'standard',
|
||||
style: style || 'dark',
|
||||
subject_prefix: subject_prefix || '',
|
||||
header_text: header_text || '',
|
||||
footer_text: footer_text || '',
|
||||
accent_color: accent_color || '#3b82f6',
|
||||
article_blocks: article_blocks || [],
|
||||
is_preset: false,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ template: data });
|
||||
}
|
||||
|
||||
// PUT: update a template
|
||||
export async function PUT(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const { id, ...updates } = body;
|
||||
if (!id) return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('newsletter_templates')
|
||||
.update({ ...updates, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ template: data });
|
||||
}
|
||||
|
||||
// DELETE: remove a template (only non-preset)
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await req.json();
|
||||
if (!id) return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
||||
|
||||
// Prevent deleting presets
|
||||
const { data: tpl } = await supabase
|
||||
.from('newsletter_templates')
|
||||
.select('is_preset')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (tpl?.is_preset) {
|
||||
return NextResponse.json({ error: 'Cannot delete preset templates' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('newsletter_templates').delete().eq('id', id);
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
Reference in New Issue
Block a user