feat:profile pictures
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
createdAt: string; // Will be converted to Date type, but is a string from api
|
||||
userId: string;
|
||||
username: string;
|
||||
profilePictureUrl?: string | null;
|
||||
likeCount?: number;
|
||||
isLikedByUser?: boolean;
|
||||
media: Array<{
|
||||
@@ -47,6 +48,7 @@
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
profilePictureUrl?: string | null;
|
||||
};
|
||||
likeCount?: number;
|
||||
isLiked?: boolean;
|
||||
@@ -70,6 +72,7 @@
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
profilePictureUrl?: string | null;
|
||||
};
|
||||
likeCount?: number;
|
||||
isLiked?: boolean;
|
||||
@@ -92,7 +95,8 @@
|
||||
createdAt: new Date(serverFind.createdAt), // Convert string to Date
|
||||
user: {
|
||||
id: serverFind.userId,
|
||||
username: serverFind.username
|
||||
username: serverFind.username,
|
||||
profilePictureUrl: serverFind.profilePictureUrl
|
||||
},
|
||||
likeCount: serverFind.likeCount,
|
||||
isLiked: serverFind.isLikedByUser,
|
||||
|
||||
@@ -65,6 +65,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
createdAt: find.createdAt,
|
||||
userId: find.userId,
|
||||
username: user.username,
|
||||
profilePictureUrl: user.profilePictureUrl,
|
||||
likeCount: sql<number>`COALESCE(COUNT(DISTINCT ${findLike.id}), 0)`,
|
||||
isLikedByUser: sql<boolean>`CASE WHEN EXISTS(
|
||||
SELECT 1 FROM ${findLike}
|
||||
@@ -76,7 +77,7 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
.innerJoin(user, eq(find.userId, user.id))
|
||||
.leftJoin(findLike, eq(find.id, findLike.findId))
|
||||
.where(whereConditions)
|
||||
.groupBy(find.id, user.username)
|
||||
.groupBy(find.id, user.username, user.profilePictureUrl)
|
||||
.orderBy(order === 'desc' ? desc(find.createdAt) : find.createdAt)
|
||||
.limit(100);
|
||||
|
||||
@@ -147,8 +148,20 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
})
|
||||
);
|
||||
|
||||
// Generate signed URL for user profile picture if it exists
|
||||
let userProfilePictureUrl = findItem.profilePictureUrl;
|
||||
if (userProfilePictureUrl && !userProfilePictureUrl.startsWith('http')) {
|
||||
try {
|
||||
userProfilePictureUrl = await getSignedR2Url(userProfilePictureUrl, 24 * 60 * 60);
|
||||
} catch (error) {
|
||||
console.error('Failed to generate signed URL for user profile picture:', error);
|
||||
userProfilePictureUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...findItem,
|
||||
profilePictureUrl: userProfilePictureUrl,
|
||||
media: mediaWithSignedUrls,
|
||||
isLikedByUser: Boolean(findItem.isLikedByUser)
|
||||
};
|
||||
|
||||
51
src/routes/api/profile-picture/delete/+server.ts
Normal file
51
src/routes/api/profile-picture/delete/+server.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { deleteFromR2 } from '$lib/server/r2';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export const DELETE: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
const { userId } = await request.json();
|
||||
|
||||
if (!userId) {
|
||||
return json({ error: 'userId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get current user to find profile picture URL
|
||||
const currentUser = await db.select().from(user).where(eq(user.id, userId)).limit(1);
|
||||
|
||||
if (!currentUser.length) {
|
||||
return json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const userRecord = currentUser[0];
|
||||
|
||||
if (!userRecord.profilePictureUrl) {
|
||||
return json({ error: 'No profile picture to delete' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Extract the base path from the stored URL (should be like users/123/profile-1234567890-abcdef.webp)
|
||||
const basePath = userRecord.profilePictureUrl.replace('.webp', '');
|
||||
|
||||
// Delete all variants (WebP and JPEG, main and thumbnails)
|
||||
const deletePromises = [
|
||||
deleteFromR2(`${basePath}.webp`),
|
||||
deleteFromR2(`${basePath}.jpg`),
|
||||
deleteFromR2(`${basePath}-thumb.webp`),
|
||||
deleteFromR2(`${basePath}-thumb.jpg`)
|
||||
];
|
||||
|
||||
// Execute all deletions, but don't fail if some files don't exist
|
||||
await Promise.allSettled(deletePromises);
|
||||
|
||||
// Update user profile picture URL in database
|
||||
await db.update(user).set({ profilePictureUrl: null }).where(eq(user.id, userId));
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Profile picture delete error:', error);
|
||||
return json({ error: 'Failed to delete profile picture' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
45
src/routes/api/profile-picture/upload/+server.ts
Normal file
45
src/routes/api/profile-picture/upload/+server.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { processAndUploadProfilePicture } from '$lib/server/media-processor';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
const userId = formData.get('userId') as string;
|
||||
|
||||
if (!file || !userId) {
|
||||
return json({ error: 'File and userId are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return json({ error: 'File must be an image' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate file size (5MB max)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
return json({ error: 'File size must be less than 5MB' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Process and upload profile picture
|
||||
const result = await processAndUploadProfilePicture(file, userId);
|
||||
|
||||
// Update user profile picture URL in database (store the WebP path)
|
||||
await db.update(user).set({ profilePictureUrl: result.url }).where(eq(user.id, userId));
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
profilePictureUrl: result.url,
|
||||
thumbnailUrl: result.thumbnailUrl,
|
||||
fallbackUrl: result.fallbackUrl,
|
||||
fallbackThumbnailUrl: result.fallbackThumbnailUrl
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Profile picture upload error:', error);
|
||||
return json({ error: 'Failed to upload profile picture' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user