|
| 1 | +import { NextResponse } from 'next/server'; |
| 2 | +import { withRateLimit } from '@/lib/ratelimit'; |
| 3 | +import { edgeLog, CDN_CACHE_HEADERS } from '@/../infra/edge-config'; |
| 4 | +import { withSecurityHeaders, sanitizeObject } from '@/lib/security'; |
| 5 | + |
| 6 | +export const runtime = 'edge'; |
| 7 | + |
| 8 | +interface SearchResult { |
| 9 | + id: string; |
| 10 | + title: string; |
| 11 | + category: 'course' | 'instructor' | 'topic' | 'investment'; |
| 12 | + instructor?: string; |
| 13 | + rating?: number; |
| 14 | + price?: number; |
| 15 | + image?: string; |
| 16 | + description?: string; |
| 17 | +} |
| 18 | + |
| 19 | +interface CategorizedResults { |
| 20 | + courses: SearchResult[]; |
| 21 | + instructors: SearchResult[]; |
| 22 | + topics: SearchResult[]; |
| 23 | + investments: SearchResult[]; |
| 24 | +} |
| 25 | + |
| 26 | +// In-memory catalogue backing the header search endpoint. |
| 27 | +// In production this would be replaced by a database query or |
| 28 | +// an external search-index service (Algolia, Meilisearch, etc.). |
| 29 | +const SEARCH_INDEX: SearchResult[] = [ |
| 30 | + // --- Courses --- |
| 31 | + { id: 'CS-101', title: 'Advanced UI/UX Masterclass for Digital Products', category: 'course', instructor: 'Dr. Sarah Connor', rating: 4.9, price: 84.99, image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCil99ZvcYdOTX2ikzfmezXpX8PM1gdqvlH9POOIWtf_oLnLUykcTbi_AmwTRyHd91SesbJSqYVeMRowIU8LwxAMfmlo6f_Pz1u8SrLX6MRh78Y4jM36RyPsfr3f6KzeGWxm4kZXTGblOlSET-GxfYpd_Nzdu1P0AVyTV6bpa_jwzSu1ZfKYb7SrSNphUJwOZowmvViRp-dqkbRQYrkWbuITbMKEf_FdUEefC39x97j6p9fuc1eF7A1Z3QFUZVecRUTOyGnzRBp9_Q' }, |
| 32 | + { id: 'MK-204', title: 'Data-Driven Growth Strategies for Startups', category: 'course', instructor: 'James Wilson', rating: 4.7, price: 49.99, image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAvzbzuSjw86VeI3OOXElSHJGSEvvzouGyeAW34jd3Px-Io4QsMww1pfJW7UOrjmNJJJ_DaLMU81Np88h_XCXTqDiDlcl9mNDYwjXXpxgNJJDTWfwEiAukRqT_aWTm9KNnK-6hjRAZZ1EPYs-Sz8NXTSel_BwuZekfnzd5n0JxMSoI2ke-DWJeJv42Df58VwTZCOadgRyZ6ktv0syTD-xYuIuPk4fe0vYDgPEqVPNAigyQsq3AxWqvfnpntYxcLt3ABxf7aaTSb1cQ' }, |
| 33 | + { id: 'GD-009', title: 'Professional Branding: From Concept to Launch', category: 'course', instructor: 'Dr. Sarah Connor', rating: 4.5, price: 119.0, image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuDCreAkOdseX9ZVH36oFGC3i0Q7D-DQz4x3zJL5NDtMSwfta8Qw886FBxG-IqaeUipdJkrptvGNhoKABpUsxpWM3mdOIUqxuJd_fIs7rU40m72S62pZ5kpdbKsqlJVJyqsi2WWJEkXEWJbKJwxP8BauVhNPiCvvFftKmHNbg7YM1sd5mq65ce7SyfNYQF70fI6FtiiqDZsVImVhLv0vn7St75HI9mz129PyeaPssxNqC7teU9A8by_hsz-wROUBDqIZLBErccnO0oQ' }, |
| 34 | + { id: 'WD-300', title: 'Modern Web Design with Figma and Webflow', category: 'course', instructor: 'Dr. Sarah Connor', rating: 4.6, price: 99.0, image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAC6NhUUHDRqyDaSzJlgNygr56BoDO_8fQcX3et2Wnp8VDx35N3nXgqbR-Xt6LuiN8hBJJaqe85edLCUHF5Bfgu7Px9PPIjEpbNqBu_b_BBAFHrqEGmReNkqR478aid53gP2dmZgNyG55_bI7DNrVWMgb85BYuA7qVKC-a25Qn5MboAwsL8FmgvO-VnOtPKgxx-yRNmHY4bAzrF8d9nAusUx1x_WpNRHMsGsuZkOktb-WhkFhppcyDN489FP793xIA_CTxtWMSe0r4' }, |
| 35 | + { id: 'SM-881', title: 'Social Media Marketing: Zero to Hero', category: 'course', instructor: 'James Wilson', rating: 4.9, price: 24.99, image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAUd6QuR7Rp0azAg6bhUKGrn8eqsVDX9Ry8ezok66U35Rnc39EGF8e_4RTwbxJKMv-73ahqf8f5ZB9b93NmWiNPSK8EnFcIr6DteWxx7yWzlyXRTn5Ti-5vmkE6VCeszMwgLEntCvZqhhTZrqr2Jn9RHsXJsFVCynv4RsUCpiG3B-L_NPzkyO3IFkYBfoMOmsHnLUsFI4SXKQGRME81AAq--3NMDs_chLm5cXIc-AdEeXiMFUIGBH9GllgnintdJWon-xXTl_1BWos' }, |
| 36 | + { id: 'CS-102', title: 'Prototyping High Fidelity Interfaces', category: 'course', instructor: 'Dr. Sarah Connor', rating: 4.7, price: 55.0, image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCil99ZvcYdOTX2ikzfmezXpX8PM1gdqvlH9POOIWtf_oLnLUykcTbi_AmwTRyHd91SesbJSqYVeMRowIU8LwxAMfmlo6f_Pz1u8SrLX6MRh78Y4jM36RyPsfr3f6KzeGWxm4kZXTGblOlSET-GxfYpd_Nzdu1P0AVyTV6bpa_jwzSu1ZfKYb7SrSNphUJwOZowmvViRp-dqkbRQYrkWbuITbMKEf_FdUEefC39x97j6p9fuc1eF7A1Z3QFUZVecRUTOyGnzRBp9_Q' }, |
| 37 | + { id: 'FI-101', title: 'Investment Fundamentals for Creators', category: 'course', instructor: 'Dr. Sarah Connor', rating: 4.8, price: 59.99, image: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?auto=format&fit=crop&w=1200&q=80' }, |
| 38 | + { id: 'FI-102', title: 'Strategic Investment Planning for Startups', category: 'course', instructor: 'James Wilson', rating: 4.7, price: 69.99, image: 'https://images.unsplash.com/photo-1542223616-0d4b4e2aa77d?auto=format&fit=crop&w=1200&q=80' }, |
| 39 | + |
| 40 | + // --- Instructors --- |
| 41 | + { id: 'INST-001', title: 'Dr. Sarah Connor', category: 'instructor', image: 'https://ui-avatars.com/api/?name=Sarah+Connor', description: 'Expert in UI/UX Design and Web Development' }, |
| 42 | + { id: 'INST-002', title: 'James Wilson', category: 'instructor', image: 'https://ui-avatars.com/api/?name=James+Wilson', description: 'Marketing and Growth Strategy Specialist' }, |
| 43 | + { id: 'INST-003', title: 'Dr. Emily Chen', category: 'instructor', image: 'https://ui-avatars.com/api/?name=Emily+Chen', description: 'Data Science and Machine Learning Professor' }, |
| 44 | + { id: 'INST-004', title: 'Alex Rivera', category: 'instructor', image: 'https://ui-avatars.com/api/?name=Alex+Rivera', description: 'Full-Stack Developer and Cloud Architect' }, |
| 45 | + |
| 46 | + // --- Topics --- |
| 47 | + { id: 'TOPIC-001', title: 'UI/UX Design', category: 'topic', description: 'Learn user interface and user experience design' }, |
| 48 | + { id: 'TOPIC-002', title: 'Digital Marketing', category: 'topic', description: 'Master digital marketing strategies' }, |
| 49 | + { id: 'TOPIC-003', title: 'Web Development', category: 'topic', description: 'Build modern web applications' }, |
| 50 | + { id: 'TOPIC-004', title: 'Data Analysis', category: 'topic', description: 'Analyze and visualize data effectively' }, |
| 51 | + { id: 'TOPIC-005', title: 'Machine Learning', category: 'topic', description: 'Explore AI and machine learning fundamentals' }, |
| 52 | + { id: 'TOPIC-006', title: 'Investment Planning', category: 'topic', description: 'Strategies for capital allocation and growth' }, |
| 53 | + |
| 54 | + // --- Investment Features --- |
| 55 | + { id: 'INV-101', title: 'Investment Fundamentals for Creators', category: 'investment', description: 'Understand capital allocation, growth opportunities, and risk management.' }, |
| 56 | + { id: 'INV-102', title: 'Investment Planning for Startups', category: 'investment', description: 'Learn how to build investor-ready business plans and pitch decks.' }, |
| 57 | + { id: 'INV-103', title: 'Strategic Capital Deployment', category: 'investment', description: 'Align project goals with effective investment and sourcing strategies.' }, |
| 58 | + { id: 'INV-104', title: 'Angel Investing 101', category: 'investment', description: 'Navigate early-stage investing, valuation, and portfolio diversification.' }, |
| 59 | +]; |
| 60 | + |
| 61 | +function normalize(text: string): string { |
| 62 | + return text.toLowerCase().trim().replace(/[^a-z0-9\s]/g, ''); |
| 63 | +} |
| 64 | + |
| 65 | +function matchScore(item: SearchResult, queryTerms: string[]): number { |
| 66 | + const titleNorm = normalize(item.title); |
| 67 | + const descNorm = item.description ? normalize(item.description) : ''; |
| 68 | + const instructorNorm = item.instructor ? normalize(item.instructor) : ''; |
| 69 | + |
| 70 | + let score = 0; |
| 71 | + for (const term of queryTerms) { |
| 72 | + if (titleNorm.includes(term)) score += 3; |
| 73 | + if (instructorNorm.includes(term)) score += 2; |
| 74 | + if (descNorm.includes(term)) score += 1; |
| 75 | + } |
| 76 | + return score; |
| 77 | +} |
| 78 | + |
| 79 | +export async function GET(request: Request): Promise<NextResponse> { |
| 80 | + edgeLog('info', '/api/search', 'GET request received'); |
| 81 | + |
| 82 | + const { addHeaders, rateLimitResponse } = withRateLimit(request, 'READ'); |
| 83 | + if (rateLimitResponse) { |
| 84 | + return withSecurityHeaders(rateLimitResponse) as NextResponse; |
| 85 | + } |
| 86 | + |
| 87 | + const { searchParams } = new URL(request.url); |
| 88 | + const q = searchParams.get('q')?.trim() ?? ''; |
| 89 | + |
| 90 | + if (!q) { |
| 91 | + const empty: CategorizedResults = { |
| 92 | + courses: [], |
| 93 | + instructors: [], |
| 94 | + topics: [], |
| 95 | + investments: [], |
| 96 | + }; |
| 97 | + const response = withSecurityHeaders( |
| 98 | + addHeaders(NextResponse.json(empty)), |
| 99 | + ); |
| 100 | + response.headers.set('Cache-Control', CDN_CACHE_HEADERS.private); |
| 101 | + return response; |
| 102 | + } |
| 103 | + |
| 104 | + const queryTerms = normalize(q).split(/\s+/).filter(Boolean); |
| 105 | + |
| 106 | + // Score and classify every index item |
| 107 | + const scored = SEARCH_INDEX |
| 108 | + .map((item) => ({ item, score: matchScore(item, queryTerms) })) |
| 109 | + .filter((entry) => entry.score > 0); |
| 110 | + |
| 111 | + // Build categorized results, capped at 3 per category |
| 112 | + const categorized: CategorizedResults = { |
| 113 | + courses: scored |
| 114 | + .filter((e) => e.item.category === 'course') |
| 115 | + .sort((a, b) => b.score - a.score) |
| 116 | + .slice(0, 3) |
| 117 | + .map((e) => e.item), |
| 118 | + instructors: scored |
| 119 | + .filter((e) => e.item.category === 'instructor') |
| 120 | + .sort((a, b) => b.score - a.score) |
| 121 | + .slice(0, 3) |
| 122 | + .map((e) => e.item), |
| 123 | + topics: scored |
| 124 | + .filter((e) => e.item.category === 'topic') |
| 125 | + .sort((a, b) => b.score - a.score) |
| 126 | + .slice(0, 3) |
| 127 | + .map((e) => e.item), |
| 128 | + investments: scored |
| 129 | + .filter((e) => e.item.category === 'investment') |
| 130 | + .sort((a, b) => b.score - a.score) |
| 131 | + .slice(0, 3) |
| 132 | + .map((e) => e.item), |
| 133 | + }; |
| 134 | + |
| 135 | + const sanitized = sanitizeObject(categorized); |
| 136 | + const response = withSecurityHeaders( |
| 137 | + addHeaders(NextResponse.json(sanitized)), |
| 138 | + ); |
| 139 | + response.headers.set('Cache-Control', CDN_CACHE_HEADERS.private); |
| 140 | + return response; |
| 141 | +} |
0 commit comments