From 88ed74fde255c0d545e11ae6df09a0f29f7fd1b1 Mon Sep 17 00:00:00 2001 From: Zias van Nes Date: Mon, 13 Oct 2025 12:35:44 +0200 Subject: [PATCH] fix:remove api logic from page server and move to api --- src/routes/+page.server.ts | 154 ++++------------------------ src/routes/+page.svelte | 3 +- src/routes/api/finds/+server.ts | 176 ++++++++++++++++++++++++-------- 3 files changed, 155 insertions(+), 178 deletions(-) diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index ce5fd58..e8a95da 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,150 +1,40 @@ import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -import { db } from '$lib/server/db'; -import { find, findMedia, user } from '$lib/server/db/schema'; -import { eq, and, sql, desc } from 'drizzle-orm'; -import { getSignedR2Url } from '$lib/server/r2'; -export const load: PageServerLoad = async ({ locals, url }) => { +export const load: PageServerLoad = async ({ locals, url, fetch, request }) => { if (!locals.user) { - return redirect(302, '/login'); + return redirect(302, '/login'); } - // Get query parameters for location-based filtering + // Build API URL with query parameters + const apiUrl = new URL('/api/finds', url.origin); + + // Forward location filtering parameters const lat = url.searchParams.get('lat'); const lng = url.searchParams.get('lng'); - const radius = url.searchParams.get('radius') || '50'; // Default 50km radius + const radius = url.searchParams.get('radius') || '50'; + + if (lat) apiUrl.searchParams.set('lat', lat); + if (lng) apiUrl.searchParams.set('lng', lng); + apiUrl.searchParams.set('radius', radius); + apiUrl.searchParams.set('includePrivate', 'true'); // Include user's private finds + apiUrl.searchParams.set('order', 'desc'); // Newest first try { - // Build where conditions - const baseCondition = sql`(${find.isPublic} = 1 OR ${find.userId} = ${locals.user.id})`; - let whereConditions = baseCondition; - - // Add location filtering if coordinates provided - if (lat && lng) { - const radiusKm = parseFloat(radius); - // Simple bounding box query for MVP (can upgrade to proper distance calculation later) - const latOffset = radiusKm / 111; // Approximate degrees per km for latitude - const lngOffset = radiusKm / (111 * Math.cos((parseFloat(lat) * Math.PI) / 180)); - - const locationConditions = and( - baseCondition, - sql`${find.latitude} BETWEEN ${parseFloat(lat) - latOffset} AND ${ - parseFloat(lat) + latOffset - }`, - sql`${find.longitude} BETWEEN ${parseFloat(lng) - lngOffset} AND ${ - parseFloat(lng) + lngOffset - }` - ); - - if (locationConditions) { - whereConditions = locationConditions; + const response = await fetch(apiUrl.toString(), { + headers: { + Cookie: request.headers.get('Cookie') || '' } + }); + + if (!response.ok) { + throw new Error(`API request failed: ${response.status}`); } - // Get all finds with filtering - const finds = await db - .select({ - id: find.id, - title: find.title, - description: find.description, - latitude: find.latitude, - longitude: find.longitude, - locationName: find.locationName, - category: find.category, - isPublic: find.isPublic, - createdAt: find.createdAt, - userId: find.userId, - username: user.username - }) - .from(find) - .innerJoin(user, eq(find.userId, user.id)) - .where(whereConditions) - .orderBy(desc(find.createdAt)) - .limit(100); - - // Get media for all finds - const findIds = finds.map((f) => f.id); - let media: Array<{ - id: string; - findId: string; - type: string; - url: string; - thumbnailUrl: string | null; - orderIndex: number | null; - }> = []; - - if (findIds.length > 0) { - media = await db - .select({ - id: findMedia.id, - findId: findMedia.findId, - type: findMedia.type, - url: findMedia.url, - thumbnailUrl: findMedia.thumbnailUrl, - orderIndex: findMedia.orderIndex - }) - .from(findMedia) - .where( - sql`${findMedia.findId} IN (${sql.join( - findIds.map((id) => sql`${id}`), - sql`, ` - )})` - ) - .orderBy(findMedia.orderIndex); - } - - // Group media by find - const mediaByFind = media.reduce( - (acc, item) => { - if (!acc[item.findId]) { - acc[item.findId] = []; - } - acc[item.findId].push(item); - return acc; - }, - {} as Record - ); - - // Combine finds with their media and generate signed URLs - const findsWithMedia = await Promise.all( - finds.map(async (findItem) => { - const findMedia = mediaByFind[findItem.id] || []; - - // Generate signed URLs for all media items - const mediaWithSignedUrls = await Promise.all( - findMedia.map(async (mediaItem) => { - // Extract path from URL if it's still a full URL, otherwise use as-is - const path = mediaItem.url.startsWith('https://') - ? mediaItem.url.split('/').slice(3).join('/') - : mediaItem.url; - - const thumbnailPath = mediaItem.thumbnailUrl?.startsWith('https://') - ? mediaItem.thumbnailUrl.split('/').slice(3).join('/') - : mediaItem.thumbnailUrl; - - const [signedUrl, signedThumbnailUrl] = await Promise.all([ - getSignedR2Url(path, 24 * 60 * 60), // 24 hours - thumbnailPath ? getSignedR2Url(thumbnailPath, 24 * 60 * 60) : Promise.resolve(null) - ]); - - return { - ...mediaItem, - url: signedUrl, - thumbnailUrl: signedThumbnailUrl - }; - }) - ); - - return { - ...findItem, - media: mediaWithSignedUrls - }; - }) - ); + const finds = await response.json(); return { - finds: findsWithMedia + finds }; } catch (err) { console.error('Error loading finds:', err); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3c38125..9b4cace 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -17,7 +17,7 @@ locationName?: string; category?: string; isPublic: number; - createdAt: Date; + createdAt: string; // Will be converted to Date type, but is a string from api userId: string; username: string; media: Array<{ @@ -83,6 +83,7 @@ let finds = $derived( (data.finds || ([] as ServerFind[])).map((serverFind: ServerFind) => ({ ...serverFind, + createdAt: new Date(serverFind.createdAt), // Convert string to Date user: { id: serverFind.userId, username: serverFind.username diff --git a/src/routes/api/finds/+server.ts b/src/routes/api/finds/+server.ts index 368f5c7..52aca78 100644 --- a/src/routes/api/finds/+server.ts +++ b/src/routes/api/finds/+server.ts @@ -2,8 +2,9 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { find, findMedia, user } from '$lib/server/db/schema'; -import { eq, and, sql } from 'drizzle-orm'; +import { eq, and, sql, desc } from 'drizzle-orm'; import { encodeBase64url } from '@oslojs/encoding'; +import { getSignedR2Url } from '$lib/server/r2'; function generateFindId(): string { const bytes = crypto.getRandomValues(new Uint8Array(15)); @@ -17,60 +18,145 @@ export const GET: RequestHandler = async ({ url, locals }) => { const lat = url.searchParams.get('lat'); const lng = url.searchParams.get('lng'); - const radius = url.searchParams.get('radius') || '10'; + const radius = url.searchParams.get('radius') || '50'; + const includePrivate = url.searchParams.get('includePrivate') === 'true'; + const order = url.searchParams.get('order') || 'desc'; - if (!lat || !lng) { - throw error(400, 'Latitude and longitude are required'); - } + try { + // Build where conditions + const baseCondition = includePrivate + ? sql`(${find.isPublic} = 1 OR ${find.userId} = ${locals.user.id})` + : sql`${find.isPublic} = 1`; - // Query finds within radius (simplified - in production use PostGIS) - // For MVP, using a simple bounding box approach - const latNum = parseFloat(lat); - const lngNum = parseFloat(lng); - const radiusNum = parseFloat(radius); + let whereConditions = baseCondition; - // Rough conversion: 1 degree ≈ 111km - const latDelta = radiusNum / 111; - const lngDelta = radiusNum / (111 * Math.cos((latNum * Math.PI) / 180)); + // Add location filtering if coordinates provided + if (lat && lng) { + const radiusKm = parseFloat(radius); + const latOffset = radiusKm / 111; + const lngOffset = radiusKm / (111 * Math.cos((parseFloat(lat) * Math.PI) / 180)); - const finds = await db - .select({ - find: find, - user: { - id: user.id, - username: user.username + const locationConditions = and( + baseCondition, + sql`${find.latitude} BETWEEN ${parseFloat(lat) - latOffset} AND ${ + parseFloat(lat) + latOffset + }`, + sql`${find.longitude} BETWEEN ${parseFloat(lng) - lngOffset} AND ${ + parseFloat(lng) + lngOffset + }` + ); + + if (locationConditions) { + whereConditions = locationConditions; } - }) - .from(find) - .innerJoin(user, eq(find.userId, user.id)) - .where( - and( - eq(find.isPublic, 1), - // Simple bounding box filter - sql`CAST(${find.latitude} AS NUMERIC) BETWEEN ${latNum - latDelta} AND ${latNum + latDelta}`, - sql`CAST(${find.longitude} AS NUMERIC) BETWEEN ${lngNum - lngDelta} AND ${lngNum + lngDelta}` - ) - ) - .orderBy(find.createdAt); + } - // Get media for each find - const findsWithMedia = await Promise.all( - finds.map(async (item) => { - const media = await db - .select() + // Get all finds with filtering + const finds = await db + .select({ + id: find.id, + title: find.title, + description: find.description, + latitude: find.latitude, + longitude: find.longitude, + locationName: find.locationName, + category: find.category, + isPublic: find.isPublic, + createdAt: find.createdAt, + userId: find.userId, + username: user.username + }) + .from(find) + .innerJoin(user, eq(find.userId, user.id)) + .where(whereConditions) + .orderBy(order === 'desc' ? desc(find.createdAt) : find.createdAt) + .limit(100); + + // Get media for all finds + const findIds = finds.map((f) => f.id); + let media: Array<{ + id: string; + findId: string; + type: string; + url: string; + thumbnailUrl: string | null; + orderIndex: number | null; + }> = []; + + if (findIds.length > 0) { + media = await db + .select({ + id: findMedia.id, + findId: findMedia.findId, + type: findMedia.type, + url: findMedia.url, + thumbnailUrl: findMedia.thumbnailUrl, + orderIndex: findMedia.orderIndex + }) .from(findMedia) - .where(eq(findMedia.findId, item.find.id)) + .where( + sql`${findMedia.findId} IN (${sql.join( + findIds.map((id) => sql`${id}`), + sql`, ` + )})` + ) .orderBy(findMedia.orderIndex); + } - return { - ...item.find, - user: item.user, - media: media - }; - }) - ); + // Group media by find + const mediaByFind = media.reduce( + (acc, item) => { + if (!acc[item.findId]) { + acc[item.findId] = []; + } + acc[item.findId].push(item); + return acc; + }, + {} as Record + ); - return json(findsWithMedia); + // Combine finds with their media and generate signed URLs + const findsWithMedia = await Promise.all( + finds.map(async (findItem) => { + const findMedia = mediaByFind[findItem.id] || []; + + // Generate signed URLs for all media items + const mediaWithSignedUrls = await Promise.all( + findMedia.map(async (mediaItem) => { + // Extract path from URL if it's still a full URL, otherwise use as-is + const path = mediaItem.url.startsWith('https://') + ? mediaItem.url.split('/').slice(3).join('/') + : mediaItem.url; + + const thumbnailPath = mediaItem.thumbnailUrl?.startsWith('https://') + ? mediaItem.thumbnailUrl.split('/').slice(3).join('/') + : mediaItem.thumbnailUrl; + + const [signedUrl, signedThumbnailUrl] = await Promise.all([ + getSignedR2Url(path, 24 * 60 * 60), // 24 hours + thumbnailPath ? getSignedR2Url(thumbnailPath, 24 * 60 * 60) : Promise.resolve(null) + ]); + + return { + ...mediaItem, + url: signedUrl, + thumbnailUrl: signedThumbnailUrl + }; + }) + ); + + return { + ...findItem, + media: mediaWithSignedUrls + }; + }) + ); + + return json(findsWithMedia); + } catch (err) { + console.error('Error loading finds:', err); + throw error(500, 'Failed to load finds'); + } }; export const POST: RequestHandler = async ({ request, locals }) => {