refactor: add SubNav layout and per-subsite nav placeholders; switch Base to use SubNav

This commit is contained in:
2025-11-28 09:07:55 -05:00
parent de50889b2c
commit d8d6c5ec21
26 changed files with 1227 additions and 122 deletions

112
src/pages/api/search.js Normal file
View File

@@ -0,0 +1,112 @@
const rateLimitMap = new Map();
function checkRateLimit(userId, limit = 5, windowMs = 1000) {
const now = Date.now();
const key = userId;
const entry = rateLimitMap.get(key);
if (!entry || now > entry.resetTime) {
rateLimitMap.set(key, { count: 1, resetTime: now + windowMs });
return true;
}
if (entry.count >= limit) {
return false;
}
entry.count++;
return true;
}
import { getSubsiteByHost } from '../../utils/subsites.js';
export async function GET({ request }) {
const host = request.headers.get('host');
const subsite = getSubsiteByHost(host);
if (!subsite || subsite.short !== 'req') {
return new Response('Not found', { status: 404 });
}
let userId = request.headers.get('cookie')?.split(';').find(c => c.trim().startsWith('nonce='))?.split('=')[1];
const hadCookie = !!userId;
if (!userId) {
userId = crypto.randomUUID();
}
if (!checkRateLimit(userId)) {
const response = new Response(JSON.stringify({ error: 'Rate limit exceeded' }), { status: 429, headers: { 'Content-Type': 'application/json' } });
if (!hadCookie) {
response.headers.set('Set-Cookie', `nonce=${userId}; HttpOnly; Path=/; Max-Age=31536000`);
}
return response;
}
const TMDB_API_KEY = import.meta.env.TMDB_API_KEY;
if (!TMDB_API_KEY) {
console.error('TMDB_API_KEY not set');
const response = new Response(JSON.stringify([]), { status: 500, headers: { 'Content-Type': 'application/json' } });
if (!hadCookie) {
response.headers.set('Set-Cookie', `nonce=${userId}; HttpOnly; Path=/; Max-Age=31536000`);
}
return response;
}
const url = new URL(request.url);
const q = url.searchParams.get('q');
if (!q || typeof q !== 'string' || !q.trim()) {
const response = new Response(JSON.stringify([]), { status: 200, headers: { 'Content-Type': 'application/json' } });
if (!hadCookie) {
response.headers.set('Set-Cookie', `nonce=${userId}; HttpOnly; Path=/; Max-Age=31536000`);
}
return response;
}
if (q.length > 100) {
const response = new Response(JSON.stringify([]), { status: 200, headers: { 'Content-Type': 'application/json' } });
if (!hadCookie) {
response.headers.set('Set-Cookie', `nonce=${userId}; HttpOnly; Path=/; Max-Age=31536000`);
}
return response;
}
try {
const apiResponse = await fetch(`https://api.themoviedb.org/3/search/multi?api_key=${TMDB_API_KEY}&query=${encodeURIComponent(q)}`);
if (!apiResponse.ok) {
throw new Error(`TMDB API error: ${apiResponse.status}`);
}
const data = await apiResponse.json();
const seen = new Set();
const filtered = data.results
.filter(item => {
if (item.media_type !== 'movie' && item.media_type !== 'tv') return false;
const key = `${item.media_type}-${item.title || item.name}-${item.release_date?.split('-')[0] || item.first_air_date?.split('-')[0] || ''}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
})
.slice(0, 10) // Limit to 10 suggestions
.map(item => ({
label: item.title || item.name,
value: item.title || item.name,
year: item.release_date?.split('-')[0] || item.first_air_date?.split('-')[0],
mediaType: item.media_type,
overview: item.overview,
poster_path: item.poster_path,
}));
const response = new Response(JSON.stringify(filtered), { headers: { 'Content-Type': 'application/json' } });
if (!hadCookie) {
response.headers.set('Set-Cookie', `nonce=${userId}; HttpOnly; Path=/; Max-Age=31536000`);
}
return response;
} catch (error) {
console.error('Error fetching suggestions:', error);
const response = new Response(JSON.stringify([]), { status: 500, headers: { 'Content-Type': 'application/json' } });
if (!hadCookie) {
response.headers.set('Set-Cookie', `nonce=${userId}; HttpOnly; Path=/; Max-Age=31536000`);
}
return response;
}
}