From 65be06620ea223faaa9aa6aaf40d9326bff3790b Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Tue, 29 Jul 2025 01:09:16 +0200 Subject: [PATCH] Add profile image and fixes --- README.md | 6 - app/api/download/route.ts | 51 + app/api/profiles/[id]/route.ts | 48 + app/api/upload/route.ts | 75 ++ app/api/user/update-profile/route.ts | 5 +- app/complete-profile/page.tsx | 103 +- app/profiles/[id]/page.tsx | 83 +- app/register/page.tsx | 11 +- lib/supabase.ts | 6 + next.config.ts | 10 +- package-lock.json | 1715 +++++++++++++++++++++++++- package.json | 3 + prisma/schema.prisma | 2 +- 13 files changed, 2077 insertions(+), 41 deletions(-) create mode 100644 app/api/download/route.ts create mode 100644 app/api/profiles/[id]/route.ts create mode 100644 app/api/upload/route.ts create mode 100644 lib/supabase.ts diff --git a/README.md b/README.md index b917f6e6..c8b6c1a2 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,6 @@ After cloning the repo and navigating into it, install dependencies: npm install ``` -Create a Prisma Postgres instance by running the following command: - -``` -npx prisma init --db -``` - You now need to configure your database connection via an environment variable. First, create an `.env` file: diff --git a/app/api/download/route.ts b/app/api/download/route.ts new file mode 100644 index 00000000..3b5c43f0 --- /dev/null +++ b/app/api/download/route.ts @@ -0,0 +1,51 @@ +import type {NextApiRequest, NextApiResponse} from "next"; +import {S3Client, GetObjectCommand} from "@aws-sdk/client-s3"; +import {getSignedUrl} from "@aws-sdk/s3-request-presigner"; + +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, + }, +}); + +export async function GET( + req: Request +) { + // console.log(req) + + const {searchParams} = new URL(req.url); + const key = searchParams.get('key'); // get the key from query params + + if (!key) { + return new Response('S3 download error', { + status: 500, + headers: {'Content-Type': 'application/json'}, + }); + } + + try { + // Option 1: Generate a signed URL (client downloads directly from S3) + const signedUrl = await getSignedUrl( + s3, + new GetObjectCommand({ + Bucket: process.env.AWS_S3_BUCKET_NAME!, + Key: key, + }), + {expiresIn: 300} // 5 minutes + ); + + return new Response(JSON.stringify({url: signedUrl}), { + status: 200, + headers: {'Content-Type': 'application/json'}, + }); + + } catch (err: any) { + console.error("S3 download error:", err); + return new Response('S3 download error', { + status: 500, + headers: {'Content-Type': 'application/json'}, + }); + } +} diff --git a/app/api/profiles/[id]/route.ts b/app/api/profiles/[id]/route.ts new file mode 100644 index 00000000..b211917d --- /dev/null +++ b/app/api/profiles/[id]/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET( + request: Request, + context: { params: Promise<{ id: string }> } +) { + try { + const params = await context.params; + const { id } = params; + + // Find the user by ID + const user = await prisma.user.findUnique({ + where: { id }, + select: { + id: true, + name: true, + email: true, + image: true, + gender: true, + description: true, + createdAt: true, + }, + }); + + // If user not found, return 404 + if (!user) { + return new NextResponse(JSON.stringify({ error: "User not found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + return new NextResponse(JSON.stringify(user), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error fetching user profile:", error); + return new NextResponse( + JSON.stringify({ error: "Failed to fetch user profile" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); + } +} \ No newline at end of file diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts new file mode 100644 index 00000000..17f24675 --- /dev/null +++ b/app/api/upload/route.ts @@ -0,0 +1,75 @@ +import {NextResponse} from 'next/server'; +import {getSession} from '@/lib/auth'; +import {v4 as uuidv4} from 'uuid'; +import {GetObjectCommand, PutObjectCommand, S3Client} from '@aws-sdk/client-s3'; +import {getSignedUrl} from "@aws-sdk/s3-request-presigner"; + +const s3Client = new S3Client({ + region: process.env.AWS_REGION!, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, + }, +}); + +export async function POST(request: Request) { + try { + const session = await getSession(); + if (!session?.user?.email) { + return NextResponse.json({error: 'Not authenticated'}, {status: 401}); + } + + const formData = await request.formData(); + const file = formData.get('file') as File | null; + + if (!file) { + return NextResponse.json({error: 'No file provided'}, {status: 400}); + } + + // Validate file type + if (!file.type.startsWith('image/')) { + return NextResponse.json({error: 'Only image files are allowed'}, {status: 400}); + } + + // Validate file size (5MB max) + if (file.size > 5 * 1024 * 1024) { + return NextResponse.json({error: 'File size must be less than 5MB'}, {status: 400}); + } + + const fileExtension = file.name.split('.').pop(); + const fileName = `${uuidv4()}.${fileExtension}`; + const fileBuffer = await file.arrayBuffer(); + const key = `profile-pictures/${fileName}`; + + const uploadParams = { + Bucket: process.env.AWS_S3_BUCKET_NAME!, + Key: key, + Body: Buffer.from(fileBuffer), + ContentType: file.type, + }; + + const response = await s3Client.send(new PutObjectCommand(uploadParams)); + console.log(`Response: ${response}`); + + // get signed url + const url = await getSignedUrl( + s3Client, + new GetObjectCommand({ + Bucket: process.env.AWS_S3_BUCKET_NAME!, + Key: key, + }), + {expiresIn: 300} // 5 minutes + ); + + console.log(`Signed URL: ${url}`); + // const fileUrl = `${process.env.AWS_S3_BUCKET_NAME}/profile-pictures/${fileName}`; + + return NextResponse.json({url: url, key: key}); + } catch (error) { + console.error('Upload error:', error); + return NextResponse.json( + {error: 'Failed to upload file'}, + {status: 500} + ); + } +} diff --git a/app/api/user/update-profile/route.ts b/app/api/user/update-profile/route.ts index c695f4ea..f5f75ef5 100644 --- a/app/api/user/update-profile/route.ts +++ b/app/api/user/update-profile/route.ts @@ -13,7 +13,8 @@ export async function POST(req: Request) { ); } - const {description, gender} = await req.json(); + const {description, gender, image} = await req.json(); + console.log(`Req: ${description}, ${gender}, ${image}`) // Validate required fields if (!gender) { @@ -29,6 +30,7 @@ export async function POST(req: Request) { data: { description: description || null, gender: gender || null, + ...(image && { image }), // Only update image if provided }, select: { id: true, @@ -36,6 +38,7 @@ export async function POST(req: Request) { name: true, description: true, gender: true, + image: true, }, }); diff --git a/app/complete-profile/page.tsx b/app/complete-profile/page.tsx index ca9f23e4..fa057f23 100644 --- a/app/complete-profile/page.tsx +++ b/app/complete-profile/page.tsx @@ -1,17 +1,66 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef, ChangeEvent } from 'react'; import { useRouter } from 'next/navigation'; import { useSession } from 'next-auth/react'; +import Image from 'next/image'; export default function CompleteProfile() { const [description, setDescription] = useState(''); const [gender, setGender] = useState(''); + const [image, setImage] = useState(null); + const [key, setKey] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(''); + const fileInputRef = useRef(null); const router = useRouter(); const { data: session, update } = useSession(); + const handleImageUpload = async (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Validate file type + if (!file.type.startsWith('image/')) { + setError('Please upload an image file'); + return; + } + + // Validate file size (5MB max) + if (file.size > 5 * 1024 * 1024) { + setError('Image size must be less than 5MB'); + return; + } + + const formData = new FormData(); + formData.append('file', file); + + try { + setIsUploading(true); + setError(''); + + const response = await fetch('/api/upload', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to upload image'); + } + + const { url, key } = await response.json(); + setImage(url); + setKey(key); + } catch (error) { + console.error('Upload error:', error); + setError(error instanceof Error ? error.message : 'Failed to upload image'); + } finally { + setIsUploading(false); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -24,15 +73,19 @@ export default function CompleteProfile() { setIsSubmitting(true); setError(''); + const body = JSON.stringify({ + description, + gender, + ...(key && { image: key }), + }); + console.log(`Body: ${body}`) + // alert(body) const response = await fetch('/api/user/update-profile', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - description, - gender, - }), + body: body, }); if (!response.ok) { @@ -81,6 +134,44 @@ export default function CompleteProfile() { )}
+
+
+
+ {image ? ( + Profile + ) : ( +
+ + {session?.user?.name?.charAt(0).toUpperCase() || 'U'} + +
+ )} +
+ +
+
+