mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-16 04:27:45 -04:00
Add multiple images
This commit is contained in:
@@ -45,7 +45,7 @@ export default function Header() {
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/profile"
|
||||
className="text-blue-600 hover:text-blue-800 px-3 py-2 text-sm font-medium"
|
||||
className="text-blue-600 dark:text-blue-100 hover:text-blue-800 dark:hover:text-blue-300 px-3 py-2 text-sm font-medium"
|
||||
>
|
||||
My Profile
|
||||
</Link>
|
||||
|
||||
@@ -20,51 +20,61 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File | null;
|
||||
console.log('formData', formData);
|
||||
let files = formData.get('files') as File | null | File[];
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({error: 'No file provided'}, {status: 400});
|
||||
if (files?.name) {
|
||||
files = [files];
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return NextResponse.json({error: 'Only image files are allowed'}, {status: 400});
|
||||
}
|
||||
const results = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (!file) {
|
||||
return NextResponse.json({error: 'No file provided'}, {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});
|
||||
}
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return NextResponse.json({error: 'Only image files are allowed'}, {status: 400});
|
||||
}
|
||||
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
const fileName = `${uuidv4()}.${fileExtension}`;
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
const key = `profile-pictures/${fileName}`;
|
||||
// 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 uploadParams = {
|
||||
Bucket: process.env.AWS_S3_BUCKET_NAME!,
|
||||
Key: key,
|
||||
Body: Buffer.from(fileBuffer),
|
||||
ContentType: file.type,
|
||||
};
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
const fileName = `${uuidv4()}.${fileExtension}`;
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
const key = `profile-pictures/${fileName}`;
|
||||
|
||||
const response = await s3Client.send(new PutObjectCommand(uploadParams));
|
||||
console.log(`Response: ${response}`);
|
||||
const uploadParams = {
|
||||
Bucket: process.env.AWS_S3_BUCKET_NAME!,
|
||||
Key: key,
|
||||
Body: Buffer.from(fileBuffer),
|
||||
ContentType: file.type,
|
||||
};
|
||||
|
||||
// get signed url
|
||||
const url = await getSignedUrl(
|
||||
s3Client,
|
||||
new GetObjectCommand({
|
||||
Bucket: process.env.AWS_S3_BUCKET_NAME!,
|
||||
Key: key,
|
||||
}),
|
||||
{expiresIn: 300} // 5 minutes
|
||||
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 {url: url, key: key};
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`Signed URL: ${url}`);
|
||||
// const fileUrl = `${process.env.AWS_S3_BUCKET_NAME}/profile-pictures/${fileName}`;
|
||||
|
||||
return NextResponse.json({url: url, key: key});
|
||||
return NextResponse.json(results);
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -28,6 +28,8 @@ function RegisterComponent() {
|
||||
const [conflictStyle, setConflictStyle] = useState('');
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
const [key, setKey] = useState<string | null>(null);
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [keys, setKeys] = useState<string[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -41,8 +43,6 @@ function RegisterComponent() {
|
||||
const router = useRouter();
|
||||
const {data: session, update} = useSession();
|
||||
|
||||
console.log('image', image)
|
||||
|
||||
// Fetch user profile data
|
||||
useEffect(() => {
|
||||
async function fetchUserProfile() {
|
||||
@@ -52,6 +52,7 @@ function RegisterComponent() {
|
||||
const response = await fetch('/api/profile');
|
||||
if (response.ok) {
|
||||
const userData = await response.json();
|
||||
await parseImage(userData.image, setImage);
|
||||
if (userData?.profile) {
|
||||
const {profile} = userData;
|
||||
setDescription(profile.description || '');
|
||||
@@ -63,7 +64,6 @@ function RegisterComponent() {
|
||||
if (profile.birthYear) {
|
||||
setAge(new Date().getFullYear() - profile.birthYear);
|
||||
}
|
||||
await parseImage(profile.image, setImage);
|
||||
|
||||
// Set selected interests if any
|
||||
if (profile.intellectualInterests?.length > 0) {
|
||||
@@ -71,9 +71,14 @@ function RegisterComponent() {
|
||||
.map((pi: any) => pi.interest.id);
|
||||
setSelectedInterests(new Set(interestIds));
|
||||
}
|
||||
}
|
||||
if (userData?.image) {
|
||||
await parseImage(userData.image, setImage);
|
||||
|
||||
setImages([])
|
||||
setKeys(profile?.images)
|
||||
await Promise.all(
|
||||
(profile?.images || []).map(async (img) => {
|
||||
await parseImage(img, setImages, true);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -123,7 +128,11 @@ function RegisterComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleImagesUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
return handleImageUpload(e, false);
|
||||
}
|
||||
|
||||
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>, headShot = true) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
@@ -138,7 +147,7 @@ function RegisterComponent() {
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('files', file);
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
@@ -155,10 +164,23 @@ function RegisterComponent() {
|
||||
return;
|
||||
}
|
||||
|
||||
const {url, key} = await response.json();
|
||||
setImage(url);
|
||||
setKey(key);
|
||||
} catch (error) {
|
||||
const results = await response.json();
|
||||
|
||||
const {url, key} = results[0]
|
||||
if (headShot) {
|
||||
setImage(url);
|
||||
setKey(key);
|
||||
console.log('headshot', key, url)
|
||||
} else {
|
||||
setImages(prev => [...prev, url]);
|
||||
setKeys(prev => [...prev, key]);
|
||||
}
|
||||
// console.log(url, key);
|
||||
console.log('image', key);
|
||||
console.log('images', keys);
|
||||
|
||||
} catch
|
||||
(error) {
|
||||
console.error('Upload error:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to upload image');
|
||||
} finally {
|
||||
@@ -213,39 +235,39 @@ function RegisterComponent() {
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!gender) {
|
||||
setError('Please select your gender');
|
||||
return;
|
||||
}
|
||||
if (!session?.user?.email) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
|
||||
const body = JSON.stringify({
|
||||
console.log('submit image', key);
|
||||
console.log('submit images', keys);
|
||||
|
||||
const data = {
|
||||
profile: {
|
||||
description,
|
||||
contactInfo,
|
||||
location,
|
||||
gender,
|
||||
personalityType,
|
||||
conflictStyle,
|
||||
gender: gender as Gender,
|
||||
birthYear: new Date().getFullYear() - age,
|
||||
personalityType: personalityType as PersonalityType,
|
||||
conflictStyle: conflictStyle as ConflictStyle,
|
||||
images: keys,
|
||||
},
|
||||
interests: Array.from(selectedInterests).map(id => ({
|
||||
id: id.startsWith('new-') ? undefined : id,
|
||||
name: allInterests.find(i => i.id === id)?.name || id.replace('new-', '')
|
||||
})),
|
||||
...(key && {image: key}),
|
||||
});
|
||||
console.log(`Body: ${body}`)
|
||||
|
||||
};
|
||||
console.log('data', data)
|
||||
const response = await fetch('/api/user/update-profile', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -297,56 +319,57 @@ function RegisterComponent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="gender" className={headingStyle}>
|
||||
Gender <span className="text-red-500">*</span>
|
||||
Gender
|
||||
</label>
|
||||
<select
|
||||
id="gender"
|
||||
name="gender"
|
||||
required
|
||||
// required
|
||||
value={gender || ''}
|
||||
onChange={(e) => setGender(e.target.value as Gender)}
|
||||
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
@@ -591,6 +614,69 @@ function RegisterComponent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3">Photos</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{Array.from(new Set(images)).map((img, index) => (
|
||||
<div key={index}
|
||||
className="relative group aspect-square rounded-lg overflow-hidden border border-gray-200">
|
||||
<Image
|
||||
src={img}
|
||||
alt={`Uploaded image ${index + 1}`}
|
||||
width={200}
|
||||
height={200}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setImages(prev => prev.filter((_, i) => i !== index));
|
||||
setKeys(prev => prev.filter((_, i) => i !== index));
|
||||
}}
|
||||
className="absolute top-2 right-2 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Remove image"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{Array.from(new Set(images)).length < 9 && (
|
||||
<label
|
||||
className="aspect-square flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-blue-500 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-gray-400 mb-1" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
<span className="text-sm text-gray-500">
|
||||
{images.length === 0 ? 'Add photos' : 'Add more'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{9 - Array.from(new Set(images)).length} remaining
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleImagesUpload}
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
disabled={isUploading || Array.from(new Set(images)).length >= 9}
|
||||
multiple
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{images.length === 0 && (
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Add up to 9 photos to your profile
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -1,36 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import {useEffect, useState} from "react";
|
||||
import LoadingSpinner from "@/lib/client/LoadingSpinner";
|
||||
import {parseImage} from "@/lib/client/media";
|
||||
import {usePathname} from "next/navigation";
|
||||
import {getProfile} from "@/lib/client/profile";
|
||||
|
||||
export default function ProfilePage() {
|
||||
const pathname = usePathname(); // Get the current route
|
||||
const [userData, setUserData] = useState<any>(null);
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
try {
|
||||
useEffect(() => {
|
||||
async function fetchImage() {
|
||||
const res = await fetch('/api/profile');
|
||||
const data = await res.json();
|
||||
setUserData(data);
|
||||
console.log('userData', data);
|
||||
if (data?.image) {
|
||||
await parseImage(data.image, setImage);
|
||||
}
|
||||
}
|
||||
|
||||
fetchImage();
|
||||
}, []);
|
||||
|
||||
if (!userData) {
|
||||
return <LoadingSpinner/>;
|
||||
}
|
||||
|
||||
console.log('userData', userData);
|
||||
|
||||
const header = (
|
||||
<div className="px-4 py-5 sm:px-6 flex justify-between items-center">
|
||||
@@ -49,11 +25,12 @@ export default function ProfilePage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen py-12 px-4 sm:px-6 lg:px-8">
|
||||
{getProfile(userData, image, header)}
|
||||
{getProfile('/api/profile', header)}
|
||||
</div>
|
||||
)
|
||||
;
|
||||
} catch (error) {
|
||||
} catch
|
||||
(error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
return <div className="text-center py-10">Error loading profile. Please try again later.</div>;
|
||||
}
|
||||
|
||||
@@ -1,54 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {useParams} from "next/navigation";
|
||||
import LoadingSpinner from "@/lib/client/LoadingSpinner";
|
||||
import {ProfileData} from "@/lib/client/schema";
|
||||
import {parseImage} from "@/lib/client/media";
|
||||
import {getProfile} from "@/lib/client/profile";
|
||||
|
||||
export const dynamic = "force-dynamic"; // This disables SSG and ISR
|
||||
|
||||
export default function Post() {
|
||||
const {id} = useParams();
|
||||
const [userData, setUserData] = useState<ProfileData | null>(null);
|
||||
const [image, setImage] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchImage() {
|
||||
const res = await fetch(`/api/profiles/${id}`);
|
||||
console.log('res', res);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setUserData(data);
|
||||
console.log('userData', data);
|
||||
if (data?.image) {
|
||||
await parseImage(data.image, setImage);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
fetchImage();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner/>;
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
return <div>
|
||||
<h1 className="text-center">Profile not found</h1>
|
||||
</div>;
|
||||
}
|
||||
|
||||
console.log(`Image: ${image}`)
|
||||
|
||||
return (
|
||||
|
||||
<div className="min-h-screen py-12 px-4 sm:px-6 lg:px-8">
|
||||
{getProfile(userData, image)}
|
||||
{getProfile(`/api/profiles/${id}`)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,20 +16,25 @@ export interface ProfileData {
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseImage(img: string, setImage: any) {
|
||||
export async function parseImage(img: string, setImage: any, batch = false) {
|
||||
if (!img) {
|
||||
return;
|
||||
}
|
||||
if (img.startsWith('http')) {
|
||||
console.log(`img: ${img}`)
|
||||
setImage(img);
|
||||
} else {
|
||||
let url = img;
|
||||
if (!img.startsWith('http')) {
|
||||
const imageResponse = await fetch(`/api/download?key=${img}`);
|
||||
console.log(`imageResponse: ${imageResponse}`)
|
||||
if (imageResponse.ok) {
|
||||
const imageBlob = await imageResponse.json();
|
||||
const imageUrl = imageBlob['url'];
|
||||
setImage(imageUrl);
|
||||
url = imageBlob['url'];
|
||||
}
|
||||
}
|
||||
if (url) {
|
||||
if (batch) {
|
||||
setImage(prev => [...prev, url]);
|
||||
} else {
|
||||
setImage(url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,267 +1,320 @@
|
||||
import {ProfileData} from "@/lib/client/schema";
|
||||
import Image from "next/image";
|
||||
import {pStyle} from "@/lib/client/constants";
|
||||
import {useEffect, useState} from "react";
|
||||
import {parseImage} from "@/lib/client/media";
|
||||
import LoadingSpinner from "@/lib/client/LoadingSpinner";
|
||||
|
||||
export function getProfile(url, header = null) {
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userData, setUserData] = useState<any>(null);
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
async function fetchImage() {
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
setUserData(data);
|
||||
console.log('userData', data);
|
||||
if (data?.image) {
|
||||
await parseImage(data.image, setImage);
|
||||
}
|
||||
|
||||
setImages([]);
|
||||
await Promise.all(
|
||||
(data?.profile?.images || []).map(async (img) => {
|
||||
await parseImage(img, setImages, true);
|
||||
})
|
||||
);
|
||||
console.log('images', data?.profile?.images);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
fetchImage();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner/>;
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
return <div>
|
||||
<h1 className="text-center">Profile not found</h1>
|
||||
</div>;
|
||||
}
|
||||
|
||||
console.log('userData', userData);
|
||||
|
||||
export function getProfile(userData: ProfileData, image, header = null) {
|
||||
return (
|
||||
<article className="max-w-3xl mx-auto shadow-lg rounded-lg overflow-hidden">
|
||||
{header}
|
||||
{/* Profile Header with 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 ">
|
||||
<Image
|
||||
src={image}
|
||||
alt={userData.name || 'Profile picture'}
|
||||
className="h-full w-full object-cover"
|
||||
width={200}
|
||||
height={200}
|
||||
// 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>
|
||||
<article className="max-w-3xl mx-auto shadow-lg rounded-lg overflow-hidden">
|
||||
{header}
|
||||
{/* Profile Header with Image */}
|
||||
< div
|
||||
className="bg-gradient-to-r h-32 relative">
|
||||
{
|
||||
image ? (
|
||||
<div className="absolute -bottom-16 left-8">
|
||||
<div className="h-32 w-32 rounded-full border-4 border-white overflow-hidden ">
|
||||
<Image
|
||||
src={image}
|
||||
alt={userData.name || 'Profile picture'}
|
||||
className="h-full w-full object-cover"
|
||||
width={200}
|
||||
height={200}
|
||||
// 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
|
||||
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>
|
||||
) :
|
||||
(
|
||||
<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">
|
||||
{userData.name ? userData.name.charAt(0).toUpperCase() : 'U'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Profile Content */
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="pt-20 px-8 pb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">
|
||||
{userData.name}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
< div
|
||||
className="space-y-6 pt-4 border-t border-gray-200">
|
||||
{/* Profile Content */
|
||||
}
|
||||
<div className="pt-20 px-8 pb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">
|
||||
{userData.name}
|
||||
</h1>
|
||||
|
||||
{userData?.profile?.desiredConnections && (
|
||||
< div
|
||||
className="space-y-6 pt-4 border-t border-gray-200">
|
||||
|
||||
{userData?.profile?.desiredConnections && (
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Desired
|
||||
Connections </h2>
|
||||
|
||||
< ul
|
||||
className="flex flex-wrap gap-2 mt-1">
|
||||
{userData?.profile?.desiredConnections.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value?.connection?.name
|
||||
}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.gender && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Gender </h2>
|
||||
< p
|
||||
className="mt-1 capitalize"> {userData.profile.gender} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.birthYear && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Age </h2>
|
||||
< p
|
||||
className="mt-1 capitalize"> {new Date().getFullYear() - userData.profile.birthYear} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.location && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Location </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.location} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.occupation && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Occupation </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.occupation} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.personalityType && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Personality
|
||||
Type </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.personalityType} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.conflictStyle && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Conflict
|
||||
Style </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.conflictStyle} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.intellectualInterests && (
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Desired
|
||||
Connections </h2>
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Interests </h2>
|
||||
|
||||
< ul
|
||||
className="flex flex-wrap gap-2 mt-1">
|
||||
{userData?.profile?.desiredConnections.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value?.connection?.name
|
||||
}
|
||||
</li>
|
||||
))
|
||||
{
|
||||
userData.profile.intellectualInterests.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value?.interest?.name
|
||||
}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.gender && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Gender </h2>
|
||||
< p
|
||||
className="mt-1 capitalize"> {userData.profile.gender} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
userData?.profile?.causeAreas && (
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Cause
|
||||
Areas </h2>
|
||||
|
||||
{
|
||||
userData?.profile?.birthYear && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Age </h2>
|
||||
< p
|
||||
className="mt-1 capitalize"> {new Date().getFullYear() - userData.profile.birthYear} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
< ul
|
||||
className="flex flex-wrap gap-2 mt-1">
|
||||
{
|
||||
userData.profile.causeAreas.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value?.causeArea?.name
|
||||
}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.location && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Location </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.location} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
userData?.profile?.description && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> About </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.description} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.occupation && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Occupation </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.occupation} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
userData?.profile?.contactInfo && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Contact </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.contactInfo} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.personalityType && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Personality
|
||||
Type </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.personalityType} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
userData?.profile?.promptAnswers && (
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Prompt
|
||||
Answers </h2>
|
||||
|
||||
{
|
||||
userData?.profile?.conflictStyle && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Conflict
|
||||
Style </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.conflictStyle} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
< ul
|
||||
className="flex flex-wrap gap-2 mt-1">
|
||||
{
|
||||
userData.profile.promptAnswers.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
// className="px-3 py-1 text-sm bg-gray-100 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
• {value.prompt} {value.answer}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.intellectualInterests && (
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Interests </h2>
|
||||
{
|
||||
images &&
|
||||
<div className="mb-8">
|
||||
{/*<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Photos </h2>*/}
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
{Array.from(new Set(images)).map((img, index) => ( // Set is a hack to avoid a bug where duplicates fill in images when we navigate different pages
|
||||
<div key={index}
|
||||
className="relative group aspect-square rounded-lg overflow-hidden border border-gray-200 ">
|
||||
<Image
|
||||
src={img}
|
||||
alt={`Uploaded image ${index + 1}`}
|
||||
width={150}
|
||||
height={150}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
< ul
|
||||
className="flex flex-wrap gap-2 mt-1">
|
||||
{
|
||||
userData.profile.intellectualInterests.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value?.interest?.name
|
||||
}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{/*<div>*/
|
||||
}
|
||||
{/* <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Creation Date</h2>*/
|
||||
}
|
||||
{/* <p className={pStyle}>*/
|
||||
}
|
||||
{/* {user.profile.createdAt}*/
|
||||
}
|
||||
{/* {new Date(user.profile.createdAt).toLocaleDateString("en-US", {*/
|
||||
}
|
||||
{/* year: "numeric",*/
|
||||
}
|
||||
{/* month: "long",*/
|
||||
}
|
||||
{/* day: "numeric",*/
|
||||
}
|
||||
{/* })}*/
|
||||
}
|
||||
{/* </p>*/
|
||||
}
|
||||
{/*</div>*/
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.causeAreas && (
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Cause
|
||||
Areas </h2>
|
||||
|
||||
< ul
|
||||
className="flex flex-wrap gap-2 mt-1">
|
||||
{
|
||||
userData.profile.causeAreas.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value?.causeArea?.name
|
||||
}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.description && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> About </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.description} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.contactInfo && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Contact </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.contactInfo} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.promptAnswers && (
|
||||
<div className="mt-3"><
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Prompt
|
||||
Answers </h2>
|
||||
|
||||
< ul
|
||||
className="flex flex-wrap gap-2 mt-1">
|
||||
{
|
||||
userData.profile.promptAnswers.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
// className="px-3 py-1 text-sm bg-gray-100 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
• {value.prompt} {value.answer}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
userData?.profile?.images && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Images </h2>
|
||||
< p
|
||||
className={pStyle}> {userData.profile.images} </p> TODO
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/*<div>*/
|
||||
}
|
||||
{/* <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Creation Date</h2>*/
|
||||
}
|
||||
{/* <p className={pStyle}>*/
|
||||
}
|
||||
{/* {user.profile.createdAt}*/
|
||||
}
|
||||
{/* {new Date(user.profile.createdAt).toLocaleDateString("en-US", {*/
|
||||
}
|
||||
{/* year: "numeric",*/
|
||||
}
|
||||
{/* month: "long",*/
|
||||
}
|
||||
{/* day: "numeric",*/
|
||||
}
|
||||
{/* })}*/
|
||||
}
|
||||
{/* </p>*/
|
||||
}
|
||||
{/*</div>*/
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
)
|
||||
;
|
||||
|
||||
Reference in New Issue
Block a user