From 5285a15335d03912e472dccfd0cbe03ee72fc341 Mon Sep 17 00:00:00 2001 From: Zias van Nes Date: Sat, 22 Nov 2025 20:04:25 +0100 Subject: [PATCH] feat:big update to public finds --- src/routes/+page.server.ts | 14 +- src/routes/+page.svelte | 61 ++- src/routes/api/finds/+server.ts | 46 +- src/routes/api/finds/[findId]/+server.ts | 115 ++++ src/routes/finds/[findId]/+page.server.ts | 43 ++ src/routes/finds/[findId]/+page.svelte | 621 ++++++++++++++++++++++ 6 files changed, 862 insertions(+), 38 deletions(-) create mode 100644 src/routes/api/finds/[findId]/+server.ts create mode 100644 src/routes/finds/[findId]/+page.server.ts create mode 100644 src/routes/finds/[findId]/+page.svelte diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index df81ae7..f570a1f 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,11 +1,6 @@ -import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ locals, url, fetch, request }) => { - if (!locals.user) { - return redirect(302, '/login'); - } - // Build API URL with query parameters const apiUrl = new URL('/api/finds', url.origin); @@ -17,8 +12,13 @@ export const load: PageServerLoad = async ({ locals, url, fetch, request }) => { 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('includeFriends', 'true'); // Include friends' finds + + // Only include private and friends' finds if user is logged in + if (locals.user) { + apiUrl.searchParams.set('includePrivate', 'true'); // Include user's private finds + apiUrl.searchParams.set('includeFriends', 'true'); // Include friends' finds + } + apiUrl.searchParams.set('order', 'desc'); // Newest first try { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1a8adfa..22e22dc 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -258,14 +258,22 @@
- - + {#if data.user} + + + {:else} + + {/if}
@@ -393,6 +401,43 @@ flex-shrink: 0; } + .login-prompt { + width: 100%; + text-align: center; + padding: 1rem; + } + + .login-prompt h3 { + font-family: 'Washington', serif; + font-size: 1.25rem; + margin: 0 0 0.5rem 0; + color: hsl(var(--foreground)); + } + + .login-prompt p { + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + margin: 0 0 1rem 0; + } + + .login-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + transition: all 0.2s ease; + } + + .login-button:hover { + background: hsl(var(--primary) / 0.9); + } + .finds-list-container { flex: 1; overflow-y: auto; diff --git a/src/routes/api/finds/+server.ts b/src/routes/api/finds/+server.ts index f8d9315..f1c9bae 100644 --- a/src/routes/api/finds/+server.ts +++ b/src/routes/api/finds/+server.ts @@ -12,10 +12,6 @@ function generateFindId(): string { } export const GET: RequestHandler = async ({ url, locals }) => { - if (!locals.user) { - throw error(401, 'Unauthorized'); - } - const lat = url.searchParams.get('lat'); const lng = url.searchParams.get('lng'); const radius = url.searchParams.get('radius') || '50'; @@ -25,9 +21,9 @@ export const GET: RequestHandler = async ({ url, locals }) => { const includeFriends = url.searchParams.get('includeFriends') === 'true'; try { - // Get user's friends if needed + // Get user's friends if needed and user is logged in let friendIds: string[] = []; - if (includeFriends || includePrivate) { + if (locals.user && (includeFriends || includePrivate)) { const friendships = await db .select({ userId: friendship.userId, @@ -37,7 +33,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { .where( and( eq(friendship.status, 'accepted'), - or(eq(friendship.userId, locals.user!.id), eq(friendship.friendId, locals.user!.id)) + or(eq(friendship.userId, locals.user.id), eq(friendship.friendId, locals.user.id)) ) ); @@ -47,12 +43,12 @@ export const GET: RequestHandler = async ({ url, locals }) => { // Build privacy conditions const conditions = [sql`${find.isPublic} = 1`]; // Always include public finds - if (includePrivate) { + if (locals.user && includePrivate) { // Include user's own finds (both public and private) - conditions.push(sql`${find.userId} = ${locals.user!.id}`); + conditions.push(sql`${find.userId} = ${locals.user.id}`); } - if (includeFriends && friendIds.length > 0) { + if (locals.user && includeFriends && friendIds.length > 0) { // Include friends' finds (both public and private) conditions.push( sql`${find.userId} IN (${sql.join( @@ -103,19 +99,23 @@ export const GET: RequestHandler = async ({ url, locals }) => { username: user.username, profilePictureUrl: user.profilePictureUrl, likeCount: sql`COALESCE(COUNT(DISTINCT ${findLike.id}), 0)`, - isLikedByUser: sql`CASE WHEN EXISTS( - SELECT 1 FROM ${findLike} - WHERE ${findLike.findId} = ${find.id} - AND ${findLike.userId} = ${locals.user.id} - ) THEN 1 ELSE 0 END`, - isFromFriend: sql`CASE WHEN ${ - friendIds.length > 0 - ? sql`${find.userId} IN (${sql.join( - friendIds.map((id) => sql`${id}`), - sql`, ` - )})` - : sql`FALSE` - } THEN 1 ELSE 0 END` + isLikedByUser: locals.user + ? sql`CASE WHEN EXISTS( + SELECT 1 FROM ${findLike} + WHERE ${findLike.findId} = ${find.id} + AND ${findLike.userId} = ${locals.user.id} + ) THEN 1 ELSE 0 END` + : sql`0`, + isFromFriend: locals.user + ? sql`CASE WHEN ${ + friendIds.length > 0 + ? sql`${find.userId} IN (${sql.join( + friendIds.map((id) => sql`${id}`), + sql`, ` + )})` + : sql`FALSE` + } THEN 1 ELSE 0 END` + : sql`0` }) .from(find) .innerJoin(user, eq(find.userId, user.id)) diff --git a/src/routes/api/finds/[findId]/+server.ts b/src/routes/api/finds/[findId]/+server.ts new file mode 100644 index 0000000..cdf01e0 --- /dev/null +++ b/src/routes/api/finds/[findId]/+server.ts @@ -0,0 +1,115 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { db } from '$lib/server/db'; +import { find, findMedia, user, findLike, findComment } from '$lib/server/db/schema'; +import { eq, sql } from 'drizzle-orm'; +import { getLocalR2Url } from '$lib/server/r2'; + +export const GET: RequestHandler = async ({ params, locals }) => { + const findId = params.findId; + + if (!findId) { + throw error(400, 'Find ID is required'); + } + + try { + // Get the find with user info and like count + const findResult = 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, + profilePictureUrl: user.profilePictureUrl, + likeCount: sql`COALESCE(COUNT(DISTINCT ${findLike.id}), 0)`, + commentCount: sql`COALESCE(( + SELECT COUNT(*) FROM ${findComment} + WHERE ${findComment.findId} = ${find.id} + ), 0)`, + isLikedByUser: locals.user + ? sql`CASE WHEN EXISTS( + SELECT 1 FROM ${findLike} + WHERE ${findLike.findId} = ${find.id} + AND ${findLike.userId} = ${locals.user.id} + ) THEN 1 ELSE 0 END` + : sql`0` + }) + .from(find) + .innerJoin(user, eq(find.userId, user.id)) + .leftJoin(findLike, eq(find.id, findLike.findId)) + .where(eq(find.id, findId)) + .groupBy(find.id, user.username, user.profilePictureUrl) + .limit(1); + + if (findResult.length === 0) { + throw error(404, 'Find not found'); + } + + const findData = findResult[0]; + + // Check if the find is public or if user has access + const isOwner = locals.user && findData.userId === locals.user.id; + const isPublic = findData.isPublic === 1; + + if (!isPublic && !isOwner) { + throw error(403, 'This find is private'); + } + + // Get media for the find + const 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, findId)) + .orderBy(findMedia.orderIndex); + + // Generate signed URLs for media + const mediaWithSignedUrls = await Promise.all( + media.map(async (mediaItem) => { + const localUrl = getLocalR2Url(mediaItem.url); + const localThumbnailUrl = + mediaItem.thumbnailUrl && !mediaItem.thumbnailUrl.startsWith('/') + ? getLocalR2Url(mediaItem.thumbnailUrl) + : mediaItem.thumbnailUrl; + + return { + ...mediaItem, + url: localUrl, + thumbnailUrl: localThumbnailUrl + }; + }) + ); + + // Generate local proxy URL for user profile picture + let userProfilePictureUrl = findData.profilePictureUrl; + if (userProfilePictureUrl && !userProfilePictureUrl.startsWith('http')) { + userProfilePictureUrl = getLocalR2Url(userProfilePictureUrl); + } + + return json({ + ...findData, + profilePictureUrl: userProfilePictureUrl, + media: mediaWithSignedUrls, + isLikedByUser: Boolean(findData.isLikedByUser) + }); + } catch (err) { + console.error('Error loading find:', err); + if (err instanceof Error && 'status' in err) { + throw err; + } + throw error(500, 'Failed to load find'); + } +}; diff --git a/src/routes/finds/[findId]/+page.server.ts b/src/routes/finds/[findId]/+page.server.ts new file mode 100644 index 0000000..a591db3 --- /dev/null +++ b/src/routes/finds/[findId]/+page.server.ts @@ -0,0 +1,43 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params, fetch, url, request }) => { + const findId = params.findId; + + if (!findId) { + throw error(400, 'Find ID is required'); + } + + try { + // Build API URL + const apiUrl = new URL(`/api/finds/${findId}`, url.origin); + + // Fetch the find data - no auth required for public finds + const response = await fetch(apiUrl.toString(), { + headers: { + Cookie: request.headers.get('Cookie') || '' + } + }); + + if (!response.ok) { + if (response.status === 404) { + throw error(404, 'Find not found'); + } else if (response.status === 403) { + throw error(403, 'This find is private'); + } + throw error(response.status, 'Failed to load find'); + } + + const find = await response.json(); + + return { + find + }; + } catch (err) { + console.error('Error loading find:', err); + if (err instanceof Error && 'status' in err) { + throw err; + } + throw error(500, 'Failed to load find'); + } +}; diff --git a/src/routes/finds/[findId]/+page.svelte b/src/routes/finds/[findId]/+page.svelte new file mode 100644 index 0000000..e09c11b --- /dev/null +++ b/src/routes/finds/[findId]/+page.svelte @@ -0,0 +1,621 @@ + + + + {data.find ? `${data.find.title} - Serengo` : 'Find - Serengo'} + + + + + {#if ogImage} + + {/if} + + + + {#if ogImage} + + {/if} + + +
+
+ {}} + /> +
+ +
+ {#if data.find} +
+
+
+ + +
+
+ +
+ {#if data.find.media && data.find.media.length > 0} +
+
+ {#if data.find.media[currentMediaIndex].type === 'photo'} + {data.find.title} + {:else} + + {/if} + + {#if data.find.media.length > 1} + + + {/if} +
+ + {#if data.find.media.length > 1} +
+ {#each data.find.media as _, index (index)} + + {/each} +
+ {/if} +
+ {/if} + +
+ {#if data.find.description} +

{data.find.description}

+ {/if} + + {#if data.find.locationName} +
+ + + + + {data.find.locationName} +
+ {/if} + +
+
+
+ +
+ +
+
+
+ {:else} +
+

Find not found

+

This find does not exist or is private.

+
+ {/if} +
+
+ +