fix:remove api logic from page server and move to api
This commit is contained in:
@@ -1,150 +1,40 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
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) {
|
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 lat = url.searchParams.get('lat');
|
||||||
const lng = url.searchParams.get('lng');
|
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 {
|
try {
|
||||||
// Build where conditions
|
const response = await fetch(apiUrl.toString(), {
|
||||||
const baseCondition = sql`(${find.isPublic} = 1 OR ${find.userId} = ${locals.user.id})`;
|
headers: {
|
||||||
let whereConditions = baseCondition;
|
Cookie: request.headers.get('Cookie') || ''
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all finds with filtering
|
const finds = await response.json();
|
||||||
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<string, typeof media>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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 {
|
return {
|
||||||
...mediaItem,
|
finds
|
||||||
url: signedUrl,
|
|
||||||
thumbnailUrl: signedThumbnailUrl
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...findItem,
|
|
||||||
media: mediaWithSignedUrls
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
finds: findsWithMedia
|
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading finds:', err);
|
console.error('Error loading finds:', err);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
locationName?: string;
|
locationName?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
isPublic: number;
|
isPublic: number;
|
||||||
createdAt: Date;
|
createdAt: string; // Will be converted to Date type, but is a string from api
|
||||||
userId: string;
|
userId: string;
|
||||||
username: string;
|
username: string;
|
||||||
media: Array<{
|
media: Array<{
|
||||||
@@ -83,6 +83,7 @@
|
|||||||
let finds = $derived(
|
let finds = $derived(
|
||||||
(data.finds || ([] as ServerFind[])).map((serverFind: ServerFind) => ({
|
(data.finds || ([] as ServerFind[])).map((serverFind: ServerFind) => ({
|
||||||
...serverFind,
|
...serverFind,
|
||||||
|
createdAt: new Date(serverFind.createdAt), // Convert string to Date
|
||||||
user: {
|
user: {
|
||||||
id: serverFind.userId,
|
id: serverFind.userId,
|
||||||
username: serverFind.username
|
username: serverFind.username
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { json, error } from '@sveltejs/kit';
|
|||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { find, findMedia, user } from '$lib/server/db/schema';
|
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 { encodeBase64url } from '@oslojs/encoding';
|
||||||
|
import { getSignedR2Url } from '$lib/server/r2';
|
||||||
|
|
||||||
function generateFindId(): string {
|
function generateFindId(): string {
|
||||||
const bytes = crypto.getRandomValues(new Uint8Array(15));
|
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 lat = url.searchParams.get('lat');
|
||||||
const lng = url.searchParams.get('lng');
|
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) {
|
try {
|
||||||
throw error(400, 'Latitude and longitude are required');
|
// Build where conditions
|
||||||
|
const baseCondition = includePrivate
|
||||||
|
? sql`(${find.isPublic} = 1 OR ${find.userId} = ${locals.user.id})`
|
||||||
|
: sql`${find.isPublic} = 1`;
|
||||||
|
|
||||||
|
let whereConditions = baseCondition;
|
||||||
|
|
||||||
|
// 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 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query finds within radius (simplified - in production use PostGIS)
|
// Get all finds with filtering
|
||||||
// For MVP, using a simple bounding box approach
|
|
||||||
const latNum = parseFloat(lat);
|
|
||||||
const lngNum = parseFloat(lng);
|
|
||||||
const radiusNum = parseFloat(radius);
|
|
||||||
|
|
||||||
// Rough conversion: 1 degree ≈ 111km
|
|
||||||
const latDelta = radiusNum / 111;
|
|
||||||
const lngDelta = radiusNum / (111 * Math.cos((latNum * Math.PI) / 180));
|
|
||||||
|
|
||||||
const finds = await db
|
const finds = await db
|
||||||
.select({
|
.select({
|
||||||
find: find,
|
id: find.id,
|
||||||
user: {
|
title: find.title,
|
||||||
id: user.id,
|
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
|
username: user.username
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.from(find)
|
.from(find)
|
||||||
.innerJoin(user, eq(find.userId, user.id))
|
.innerJoin(user, eq(find.userId, user.id))
|
||||||
.where(
|
.where(whereConditions)
|
||||||
and(
|
.orderBy(order === 'desc' ? desc(find.createdAt) : find.createdAt)
|
||||||
eq(find.isPublic, 1),
|
.limit(100);
|
||||||
// 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
|
// Get media for all finds
|
||||||
const findsWithMedia = await Promise.all(
|
const findIds = finds.map((f) => f.id);
|
||||||
finds.map(async (item) => {
|
let media: Array<{
|
||||||
const media = await db
|
id: string;
|
||||||
.select()
|
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)
|
.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);
|
.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<string, typeof media>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 {
|
return {
|
||||||
...item.find,
|
...mediaItem,
|
||||||
user: item.user,
|
url: signedUrl,
|
||||||
media: media
|
thumbnailUrl: signedThumbnailUrl
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...findItem,
|
||||||
|
media: mediaWithSignedUrls
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return json(findsWithMedia);
|
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 }) => {
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user