mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-16 04:27:45 -04:00
Add profile image and fixes
This commit is contained in:
@@ -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:
|
||||
|
||||
51
app/api/download/route.ts
Normal file
51
app/api/download/route.ts
Normal file
@@ -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'},
|
||||
});
|
||||
}
|
||||
}
|
||||
48
app/api/profiles/[id]/route.ts
Normal file
48
app/api/profiles/[id]/route.ts
Normal file
@@ -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" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
75
app/api/upload/route.ts
Normal file
75
app/api/upload/route.ts
Normal file
@@ -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}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [key, setKey] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const router = useRouter();
|
||||
const { data: session, update } = useSession();
|
||||
|
||||
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
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() {
|
||||
)}
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="relative">
|
||||
<div className="h-32 w-32 rounded-full overflow-hidden border-4 border-white shadow-md">
|
||||
{image ? (
|
||||
<Image
|
||||
src={image}
|
||||
alt="Profile"
|
||||
width={128}
|
||||
height={128}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-gray-200 flex items-center justify-center">
|
||||
<span className="text-4xl text-gray-500">
|
||||
{session?.user?.name?.charAt(0).toUpperCase() || 'U'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label
|
||||
className="absolute -bottom-2 -right-2 bg-blue-600 text-white rounded-full p-2 cursor-pointer hover:bg-blue-700 transition-colors"
|
||||
title="Upload photo"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4 5a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V7a2 2 0 00-2-2h-1.586a1 1 0 01-.707-.293l-1.121-1.121A2 2 0 0011.172 3H8.828a2 2 0 00-1.414.586L6.293 4.707A1 1 0 015.586 5H4zm6 9a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleImageUpload}
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="gender" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@@ -131,7 +222,7 @@ export default function CompleteProfile() {
|
||||
isSubmitting ? 'opacity-70 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{isSubmitting ? 'Saving...' : 'Save and Continue'}
|
||||
{isSubmitting || isUploading ? 'Saving...' : 'Save and Continue'}
|
||||
</button>
|
||||
|
||||
{/*<div className="mt-4 text-center">*/}
|
||||
|
||||
@@ -1,45 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {notFound, useParams} from "next/navigation";
|
||||
|
||||
interface ProfileData {
|
||||
name?: string;
|
||||
image?: string;
|
||||
gender?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic"; // This disables SSG and ISR
|
||||
|
||||
import { prisma }from "@/lib/prisma";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
export default function Post() {
|
||||
const {id} = useParams();
|
||||
const [profile, setProfile] = useState<ProfileData | null>(null);
|
||||
const [image, setImage] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
export default async function Post({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
// const profileId = id;
|
||||
useEffect(() => {
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/profiles/${id}`);
|
||||
if (!response.ok) {
|
||||
notFound();
|
||||
}
|
||||
const data = await response.json();
|
||||
setProfile(data);
|
||||
console.log(`Data image: ${data.image}`)
|
||||
|
||||
const profile = await prisma.user.findUnique({
|
||||
where: { id: id },
|
||||
// include: {
|
||||
// author: true,
|
||||
// },
|
||||
});
|
||||
// If user has an image key, fetch the image
|
||||
if (data.image) {
|
||||
const imageResponse = await fetch(`/api/download?key=${data.image}`);
|
||||
console.log(`imageResponse: ${imageResponse}`)
|
||||
if (imageResponse.ok) {
|
||||
const imageBlob = await imageResponse.json();
|
||||
const imageUrl = imageBlob['url'];
|
||||
setImage(imageUrl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProfile();
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
console.log(`Image: ${image}`)
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<article className="max-w-3xl mx-auto bg-white shadow-lg rounded-lg overflow-hidden">
|
||||
{/* Profile Header with Image */}
|
||||
<div className="bg-gradient-to-r from-blue-600 to-blue-800 h-48 relative">
|
||||
{profile.image ? (
|
||||
<div className="bg-gradient-to-r h-16 relative">
|
||||
{image ? (
|
||||
<div className="absolute -bottom-16 left-8">
|
||||
<div className="h-32 w-32 rounded-full border-4 border-white overflow-hidden bg-white">
|
||||
<img
|
||||
src={profile.image}
|
||||
src={image}
|
||||
alt={profile.name || 'Profile picture'}
|
||||
className="h-full w-full object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.onerror = null;
|
||||
target.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(profile.name || 'U')}&background=random`;
|
||||
}}
|
||||
// onError={(e) => {
|
||||
// const target = e.target as HTMLImageElement;
|
||||
// target.onerror = null;
|
||||
// target.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(profile.name || 'U')}&background=random`;
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute -bottom-16 left-8 h-32 w-32 rounded-full border-4 border-white bg-gray-200 flex items-center justify-center">
|
||||
<div
|
||||
className="absolute -bottom-16 left-8 h-32 w-32 rounded-full border-4 border-white bg-gray-200 flex items-center justify-center">
|
||||
<span className="text-4xl font-bold text-gray-600">
|
||||
{profile.name ? profile.name.charAt(0).toUpperCase() : 'U'}
|
||||
</span>
|
||||
|
||||
@@ -13,10 +13,15 @@ export default function RegisterPage() {
|
||||
const [registrationSuccess, setRegistrationSuccess] = useState(false);
|
||||
const [registeredEmail, setRegisteredEmail] = useState('');
|
||||
|
||||
function redirect() {
|
||||
// Redirect to complete profile page
|
||||
window.location.href = '/complete-profile';
|
||||
}
|
||||
|
||||
const handleGoogleSignUp = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await signIn('google', {callbackUrl: '/'});
|
||||
await signIn('google', {callbackUrl: '/complete-profile'});
|
||||
} catch (error) {
|
||||
setError('Failed to sign up with Google');
|
||||
setIsLoading(false);
|
||||
@@ -65,8 +70,8 @@ export default function RegisterPage() {
|
||||
throw new Error("Failed to sign in after registration");
|
||||
}
|
||||
|
||||
// Redirect to complete profile page
|
||||
window.location.href = '/complete-profile';
|
||||
redirect()
|
||||
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : "Registration failed");
|
||||
setIsLoading(false);
|
||||
|
||||
6
lib/supabase.ts
Normal file
6
lib/supabase.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
export const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'bayesbond.s3.eu-north-1.amazonaws.com',
|
||||
pathname: '/**', // allow all paths
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
1715
package-lock.json
generated
1715
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.10.0",
|
||||
"@aws-sdk/client-s3": "^3.855.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.855.0",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^6.12.0",
|
||||
"@prisma/extension-accelerate": "^2.0.2",
|
||||
"@react-email/render": "^1.1.3",
|
||||
"@supabase/supabase-js": "^2.53.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user