Spaces:
Paused
Paused
| // @ts-nocheck | |
| import type { FastifyInstance } from 'fastify'; | |
| import { channelRepository } from '@core/storage/repositories/channels.js'; | |
| import { validateInput, searchSchema } from '@core/security/validation.js'; | |
| import { formatNumber, formatRelativeTime, getSortField } from '@core/utils/helpers.js'; | |
| export async function searchRoutes(app: FastifyInstance) { | |
| // Home page - redirects to channels list | |
| app.get('/', async (request, reply) => { | |
| return reply.redirect('/channels'); | |
| }); | |
| // Search API endpoint (for HTMX live search) | |
| app.get('/api/search', async (request, reply) => { | |
| const validation = validateInput(searchSchema, request.query); | |
| const params = validation.success ? validation.data : { q: '', sort: 'newest', page: 1, limit: 20 }; | |
| const tag = request.query.tag as string | undefined; | |
| const search = params.q; | |
| const sort = params.sort; | |
| const page = params.page; | |
| const limit = Math.min(params.limit, 50); | |
| const offset = (page - 1) * limit; | |
| const { field, order } = getSortField(sort); | |
| const [channels, total] = await Promise.all([ | |
| channelRepository.findPublished({ | |
| tag, | |
| search, | |
| sortBy: field, | |
| sortOrder: order, | |
| limit, | |
| offset, | |
| }), | |
| channelRepository.countPublished({ ...(tag && { tags: tag }) }), | |
| ]); | |
| return reply.send({ | |
| channels: channels.map(c => ({ | |
| id: c.id, | |
| name: c.name, | |
| description: c.description, | |
| iconUrl: c.iconUrl, | |
| followerCount: c.followerCount, | |
| formattedFollowers: formatNumber(c.followerCount), | |
| voteCount: c.voteCount, | |
| formattedVotes: formatNumber(c.voteCount), | |
| tags: c.tags, | |
| relativeTime: formatRelativeTime(c.createdAt), | |
| ownerUsername: c.ownerUsername, | |
| })), | |
| pagination: { | |
| page, | |
| totalPages: Math.ceil(total / limit), | |
| total, | |
| hasNext: page * limit < total, | |
| hasPrev: page > 1, | |
| }, | |
| }); | |
| }); | |
| // Tag autocomplete | |
| app.get('/api/tags', async (request, reply) => { | |
| const query = (request.query.q as string || '').toLowerCase(); | |
| const channels = await channelRepository.findPublished({ limit: 1000 }); | |
| const tags = new Set<string>(); | |
| channels.forEach(c => { | |
| c.tags.forEach(t => { | |
| if (!query || t.includes(query)) { | |
| tags.add(t); | |
| } | |
| }); | |
| }); | |
| return reply.send(Array.from(tags).sort().slice(0, 20)); | |
| }); | |
| // Sitemap | |
| app.get('/sitemap.xml', async (request, reply) => { | |
| const channels = await channelRepository.findPublished({ limit: 50000 }); | |
| const baseUrl = 'https://your-domain.com'; // TODO: from config | |
| const urls = [ | |
| { url: baseUrl, changefreq: 'daily', priority: '1.0' }, | |
| { url: `${baseUrl}/channels`, changefreq: 'hourly', priority: '0.9' }, | |
| ]; | |
| channels.forEach(c => { | |
| urls.push({ | |
| url: `${baseUrl}/channels/${c.id}`, | |
| lastmod: c.lastScrapedAt || c.updatedAt, | |
| changefreq: 'daily', | |
| priority: '0.7', | |
| }); | |
| }); | |
| const xml = `<?xml version="1.0" encoding="UTF-8"?> | |
| <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> | |
| ${urls.map(u => ` <url> | |
| <loc>${u.url}</loc> | |
| <lastmod>${u.lastmod ? new Date(u.lastmod).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]}</lastmod> | |
| <changefreq>${u.changefreq}</changefreq> | |
| <priority>${u.priority}</priority> | |
| </url>`).join('\n')} | |
| </urlset>`; | |
| reply.header('Content-Type', 'application/xml'); | |
| return reply.send(xml); | |
| }); | |
| // Robots.txt | |
| app.get('/robots.txt', async (request, reply) => { | |
| const baseUrl = 'https://your-domain.com'; // TODO: from config | |
| const content = `User-agent: * | |
| Allow: / | |
| Sitemap: ${baseUrl}/sitemap.xml | |
| `; | |
| reply.header('Content-Type', 'text/plain'); | |
| return reply.send(content); | |
| }); | |
| } |