mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-16 04:27:45 -04:00
Add profile info
This commit is contained in:
@@ -9,17 +9,21 @@ export async function GET(
|
||||
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,
|
||||
profile: {
|
||||
include: {
|
||||
intellectualInterests: { include: { interest: true } },
|
||||
causeAreas: { include: { causeArea: true } },
|
||||
desiredConnections: { include: { connection: true } },
|
||||
promptAnswers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -31,6 +35,8 @@ export async function GET(
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Fetched user profile:", user);
|
||||
|
||||
return new NextResponse(JSON.stringify(user), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { prisma }from "@/lib/prisma";
|
||||
import { NextResponse } from "next/server";
|
||||
import {getSession} from "@/lib/auth";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
@@ -7,12 +8,50 @@ export async function GET(request: Request) {
|
||||
const profilesPerPage = 20;
|
||||
const offset = (page - 1) * profilesPerPage;
|
||||
|
||||
const session = await getSession();
|
||||
console.log(`Session: ${session?.user?.name}`);
|
||||
|
||||
// Fetch paginated posts
|
||||
const profiles = await prisma.user.findMany({
|
||||
skip: offset,
|
||||
take: profilesPerPage,
|
||||
orderBy: { createdAt: "desc" },
|
||||
// include: { author: { select: { name: true } } },
|
||||
where: { id: {not: session?.user?.id} },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
profile: {
|
||||
include: {
|
||||
intellectualInterests: { include: { interest: true } },
|
||||
causeAreas: { include: { causeArea: true } },
|
||||
desiredConnections: { include: { connection: true } },
|
||||
promptAnswers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// where: {
|
||||
// id: {
|
||||
// not: session?.user?.id, // Exclude the logged-in user
|
||||
// gender: 'FEMALE',
|
||||
// intellectualInterests: {
|
||||
// some: {
|
||||
// interest: { name: 'Philosophy' }
|
||||
// }
|
||||
// },
|
||||
// causeAreas: {
|
||||
// some: {
|
||||
// causeArea: { name: 'AI Safety' }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// include: {
|
||||
// user: true,
|
||||
// intellectualInterests: { include: { interest: true } },
|
||||
// causeAreas: { include: { causeArea: true } }
|
||||
// }
|
||||
// },
|
||||
});
|
||||
|
||||
const totalProfiles = await prisma.user.count();
|
||||
|
||||
@@ -13,33 +13,28 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const {description, gender, image} = await req.json();
|
||||
console.log(`Req: ${description}, ${gender}, ${image}`)
|
||||
|
||||
// Validate required fields
|
||||
if (!gender) {
|
||||
return NextResponse.json(
|
||||
{error: "Gender is required"},
|
||||
{status: 400}
|
||||
);
|
||||
}
|
||||
const data = await req.json();
|
||||
console.log(`Req: ${data}`)
|
||||
|
||||
// Update user with the new profile information
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {email: session.user.email},
|
||||
data: {
|
||||
description: description || null,
|
||||
gender: gender || null,
|
||||
...(image && { image }), // Only update image if provided
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
description: true,
|
||||
gender: true,
|
||||
image: true,
|
||||
...(data.image && { image: data.image }),
|
||||
profile: {
|
||||
upsert: {
|
||||
create: data.profile,
|
||||
update: data.profile,
|
||||
},
|
||||
},
|
||||
},
|
||||
// , // Only update image if provided
|
||||
// select: {
|
||||
// id: true,
|
||||
// email: true,
|
||||
// name: true,
|
||||
// image: true,
|
||||
// },
|
||||
});
|
||||
|
||||
return NextResponse.json(updatedUser);
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, ChangeEvent, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import {ChangeEvent, useEffect, useRef, useState} from 'react';
|
||||
import {useRouter} from 'next/navigation';
|
||||
import {useSession} from 'next-auth/react';
|
||||
import Image from 'next/image';
|
||||
import {ConflictStyle, Gender, PersonalityType} from "@prisma/client";
|
||||
|
||||
export default function CompleteProfile() {
|
||||
const [description, setDescription] = useState('');
|
||||
const [contactInfo, setContactInfo] = useState('');
|
||||
const [location, setLocation] = useState('');
|
||||
const [gender, setGender] = useState('');
|
||||
const [personalityType, setPersonalityType] = useState('');
|
||||
const [conflictStyle, setConflictStyle] = useState('');
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
const [key, setKey] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@@ -15,7 +20,22 @@ export default function CompleteProfile() {
|
||||
const [error, setError] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const router = useRouter();
|
||||
const { data: session, update } = useSession();
|
||||
const {data: session, update} = useSession();
|
||||
|
||||
// const [selected, setSelected] = useState(new Set(selectedInterests));
|
||||
//
|
||||
// const toggleInterest = (interestId) => {
|
||||
// setSelected((prev) => {
|
||||
// const newSet = new Set(prev);
|
||||
// newSet.has(interestId) ? newSet.delete(interestId) : newSet.add(interestId);
|
||||
// return newSet;
|
||||
// });
|
||||
// };
|
||||
|
||||
// const handleInterestSubmit = (e) => {
|
||||
// e.preventDefault();
|
||||
// onSave(Array.from(selected)); // send to API
|
||||
// };
|
||||
|
||||
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -39,7 +59,7 @@ export default function CompleteProfile() {
|
||||
try {
|
||||
setIsUploading(true);
|
||||
setError('');
|
||||
|
||||
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
@@ -51,7 +71,7 @@ export default function CompleteProfile() {
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, key } = await response.json();
|
||||
const {url, key} = await response.json();
|
||||
setImage(url);
|
||||
setKey(key);
|
||||
} catch (error) {
|
||||
@@ -64,7 +84,7 @@ export default function CompleteProfile() {
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
if (!gender) {
|
||||
setError('Please select your gender');
|
||||
return;
|
||||
@@ -75,9 +95,15 @@ export default function CompleteProfile() {
|
||||
setError('');
|
||||
|
||||
const body = JSON.stringify({
|
||||
description,
|
||||
gender,
|
||||
...(key && { image: key }),
|
||||
profile: {
|
||||
description,
|
||||
contactInfo,
|
||||
location,
|
||||
gender,
|
||||
personalityType,
|
||||
conflictStyle,
|
||||
},
|
||||
...(key && {image: key}),
|
||||
});
|
||||
console.log(`Body: ${body}`)
|
||||
// alert(body)
|
||||
@@ -97,7 +123,7 @@ export default function CompleteProfile() {
|
||||
|
||||
// Update the session to reflect the changes
|
||||
await update();
|
||||
|
||||
|
||||
// Redirect to the home page or dashboard
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
@@ -115,6 +141,21 @@ export default function CompleteProfile() {
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const genderOptions = Object.values(Gender);
|
||||
const personalityOptions = Object.values(PersonalityType);
|
||||
const conflictOptions = Object.values(ConflictStyle);
|
||||
|
||||
// const answers = [
|
||||
// {
|
||||
// prompt: 'What is your favorite color?',
|
||||
// answer: 'Blue',
|
||||
// },
|
||||
// {
|
||||
// prompt: 'What is your favorite animal?',
|
||||
// answer: 'Dog',
|
||||
// }
|
||||
// ]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
@@ -122,17 +163,17 @@ export default function CompleteProfile() {
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Complete Your Profile
|
||||
</h2>
|
||||
{/*<p className="mt-2 text-center text-sm text-gray-600">*/}
|
||||
{/* Help us know you better (this information can be updated later)*/}
|
||||
{/*</p>*/}
|
||||
</div>
|
||||
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border-l-4 border-red-400 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
<svg className="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||
clipRule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
@@ -167,7 +208,9 @@ export default function CompleteProfile() {
|
||||
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" />
|
||||
<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"
|
||||
@@ -181,7 +224,8 @@ export default function CompleteProfile() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div className="pt-4">
|
||||
|
||||
<div>
|
||||
<label htmlFor="gender" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Gender <span className="text-red-500">*</span>
|
||||
@@ -191,18 +235,88 @@ export default function CompleteProfile() {
|
||||
name="gender"
|
||||
required
|
||||
value={gender}
|
||||
onChange={(e) => setGender(e.target.value)}
|
||||
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 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
>
|
||||
<option value="">Select your gender</option>
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
{/*<option value="non-binary">Non-binary</option>*/}
|
||||
<option value="other">Other</option>
|
||||
{/*<option value="prefer-not-to-say">Prefer not to say</option>*/}
|
||||
{genderOptions.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g.replace(/_/g, ' ')} {/* optional: format label */}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<label htmlFor="location" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Location
|
||||
</label>
|
||||
<textarea
|
||||
id="location"
|
||||
name="location"
|
||||
rows={1}
|
||||
required
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<label htmlFor="personality" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Personality <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="personality"
|
||||
name="personality"
|
||||
required
|
||||
value={personalityType}
|
||||
onChange={(e) => setPersonalityType(e.target.value as PersonalityType)}
|
||||
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
>
|
||||
<option value="">Select your personality</option>
|
||||
{personalityOptions.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g.replace(/_/g, ' ')} {/* optional: format label */}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<label htmlFor="conflictStyle" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Conflict / Disagreement Style <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="conflictStyle"
|
||||
name="conflictStyle"
|
||||
required
|
||||
value={conflictStyle}
|
||||
onChange={(e) => setConflictStyle(e.target.value as ConflictStyle)}
|
||||
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
>
|
||||
<option value="">Select your conflictStyle</option>
|
||||
{conflictOptions.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
{g.replace(/_/g, ' ')} {/* optional: format label */}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* <div className="pt-4">*/}
|
||||
{/* {allInterests.map((interest) => (*/}
|
||||
{/* <label key={interest.id} className="flex items-center space-x-2">*/}
|
||||
{/* <input*/}
|
||||
{/* type="checkbox"*/}
|
||||
{/* checked={selected.has(interest.id)}*/}
|
||||
{/* onChange={() => toggleInterest(interest.id)}*/}
|
||||
{/* />*/}
|
||||
{/* <span>{interest.name}</span>*/}
|
||||
{/* </label>*/}
|
||||
{/* ))}*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<div className="pt-4">
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
About You <span className="text-red-500">*</span>
|
||||
@@ -215,12 +329,50 @@ export default function CompleteProfile() {
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Tell us a bit about yourself"
|
||||
placeholder="Tell us a bit about yourself. You can link to social media profiles or dating / connection documents."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*<div className="pt-4">*/}
|
||||
{/* <label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">*/}
|
||||
{/* Prompts*/}
|
||||
{/* </label>*/}
|
||||
{/* {answers.map(({ prompt, answer }) => (*/}
|
||||
{/* <div key={prompt}>*/}
|
||||
{/* <label className="block font-medium">{prompt}</label>*/}
|
||||
{/* <input*/}
|
||||
{/* type="text"*/}
|
||||
{/* value={answer}*/}
|
||||
{/* onChange={(e) => handleChange(prompt, e.target.value)}*/}
|
||||
{/* className="w-full border px-2 py-1"*/}
|
||||
{/* />*/}
|
||||
{/* </div>*/}
|
||||
{/* ))}*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<div className="pt-4">
|
||||
<label htmlFor="contact" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Contact Info
|
||||
</label>
|
||||
<textarea
|
||||
id="contact"
|
||||
name="contact"
|
||||
rows={4}
|
||||
required
|
||||
value={contactInfo}
|
||||
onChange={(e) => setContactInfo(e.target.value)}
|
||||
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Add your contact info here (email, phone, Google Form, etc.) until the chat feature is ready"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
This will be visible on your public profile
|
||||
Note that all the information will be public and crawlable for now—in the future, we will add different
|
||||
levels of privacy.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -233,7 +385,7 @@ export default function CompleteProfile() {
|
||||
>
|
||||
{isSubmitting || isUploading ? 'Saving...' : 'Save and Continue'}
|
||||
</button>
|
||||
|
||||
|
||||
{/*<div className="mt-4 text-center">*/}
|
||||
{/* <button*/}
|
||||
{/* type="button"*/}
|
||||
|
||||
15
app/page.tsx
15
app/page.tsx
@@ -1,9 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import ProfilePage from "@/app/profiles/page";
|
||||
|
||||
export const dynamic = "force-dynamic"; // This disables SSG and ISR
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
|
||||
export default function HomePage() {
|
||||
const profilePage = () => {
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50 text-gray-900 flex flex-col">
|
||||
<ProfilePage />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50 text-gray-900 flex flex-col">
|
||||
{/* Header */}
|
||||
@@ -36,6 +48,9 @@ export default function HomePage() {
|
||||
Learn More
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
{profilePage()}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {notFound, useParams} from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import LoadingSpinner from "@/lib/LoadingSpinner";
|
||||
|
||||
interface ProfileData {
|
||||
name?: string;
|
||||
image?: string;
|
||||
gender?: string;
|
||||
description?: string;
|
||||
profile?: any;
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic"; // This disables SSG and ISR
|
||||
|
||||
export default function Post() {
|
||||
const {id} = useParams();
|
||||
const [profile, setProfile] = useState<ProfileData | null>(null);
|
||||
const [user, setUser] = useState<ProfileData | null>(null);
|
||||
const [image, setImage] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Post() {
|
||||
notFound();
|
||||
}
|
||||
const data = await response.json();
|
||||
setProfile(data);
|
||||
setUser(data);
|
||||
const img = data.image;
|
||||
console.log(`Data image: ${img}`)
|
||||
|
||||
@@ -56,10 +56,10 @@ export default function Post() {
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return <div></div>;
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
if (!user) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function Post() {
|
||||
<div className="h-32 w-32 rounded-full border-4 border-white overflow-hidden bg-white">
|
||||
<Image
|
||||
src={image}
|
||||
alt={profile.name || 'Profile picture'}
|
||||
alt={user.name || 'Profile picture'}
|
||||
className="h-full w-full object-cover"
|
||||
width={200}
|
||||
height={200}
|
||||
@@ -92,7 +92,7 @@ export default function Post() {
|
||||
<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'}
|
||||
{user.name ? user.name.charAt(0).toUpperCase() : 'U'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -101,27 +101,140 @@ export default function Post() {
|
||||
{/* Profile Content */}
|
||||
<div className="pt-20 px-8 pb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{profile.name}
|
||||
{user.name}
|
||||
</h1>
|
||||
|
||||
<div className="space-y-6 pt-4 border-t border-gray-200">
|
||||
{profile.gender && (
|
||||
|
||||
{user.profile.desiredConnections && (
|
||||
<div className="mt-3"> <
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Connection</h2>
|
||||
|
||||
<ul className="flex flex-wrap gap-2 mt-1">
|
||||
{user.profile.desiredConnections.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-gray-100 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value.connection.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user.profile.gender && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Gender</h2>
|
||||
<p className="mt-1 capitalize">{profile.gender}</p>
|
||||
<p className="mt-1 capitalize">{user.profile.gender}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profile.description && (
|
||||
{user.profile.location && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Location</h2>
|
||||
<p className="mt-1 text-gray-800 whitespace-pre-line">{user.profile.location}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user.profile.personalityType && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Personality Type</h2>
|
||||
<p className="mt-1 text-gray-800 whitespace-pre-line">{user.profile.personalityType}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user.profile.conflictStyle && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Conflit Style</h2>
|
||||
<p className="mt-1 text-gray-800 whitespace-pre-line">{user.profile.conflictStyle}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user.profile.intellectualInterests && (
|
||||
<div className="mt-3"> <
|
||||
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Interests</h2>
|
||||
|
||||
<ul className="flex flex-wrap gap-2 mt-1">
|
||||
{user.profile.intellectualInterests.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-gray-100 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value.interest.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user.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">
|
||||
{user.profile.causeAreas.map((value, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="px-3 py-1 text-sm bg-gray-100 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
{value.causeArea.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user.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">
|
||||
{user.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>
|
||||
)}
|
||||
|
||||
{user.profile.description && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">About</h2>
|
||||
<p className="mt-1 text-gray-800 whitespace-pre-line">{profile.description}</p>
|
||||
<p className="mt-1 text-gray-800 whitespace-pre-line">{user.profile.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{user.profile.contactInfo && (
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Contact</h2>
|
||||
<p className="mt-1 text-gray-800 whitespace-pre-line">{user.profile.contactInfo}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*<div>*/}
|
||||
{/* <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider">Creation Date</h2>*/}
|
||||
{/* <p className="mt-1 text-gray-800 whitespace-pre-line">*/}
|
||||
{/* {user.profile.createdAt}*/}
|
||||
{/* {new Date(user.profile.createdAt).toLocaleDateString("en-US", {*/}
|
||||
{/* year: "numeric",*/}
|
||||
{/* month: "long",*/}
|
||||
{/* day: "numeric",*/}
|
||||
{/* })}*/}
|
||||
{/* </p>*/}
|
||||
{/*</div>*/}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import {useEffect, useState} from "react";
|
||||
import LoadingSpinner from "@/lib/LoadingSpinner";
|
||||
|
||||
|
||||
// Disable static generation
|
||||
@@ -11,6 +12,7 @@ type Profile = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
profile: any;
|
||||
};
|
||||
|
||||
|
||||
@@ -42,23 +44,21 @@ export default function ProfilePage() {
|
||||
fetchProfile();
|
||||
}, []);
|
||||
|
||||
if (!profiles) return <p>Loading...</p>;
|
||||
if (!profiles) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col items-center py-24 px-8">
|
||||
<h1 className="text-5xl font-extrabold mb-12 text-[#333333]">Profiles</h1>
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 w-full max-w-6xl mb-8">
|
||||
{profiles.map((profile) => (
|
||||
<Link key={profile.id} href={`/profiles/${profile.id}`} className="group">
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-1 w-full max-w-6xl mb-8">
|
||||
{profiles.map((user) => (
|
||||
<Link key={user.id} href={`/profiles/${user.id}`} className="group">
|
||||
<div className="border rounded-lg shadow-md bg-white p-6 hover:shadow-lg transition-shadow duration-300">
|
||||
<h2 className="text-2xl font-semibold text-gray-900 group-hover:underline mb-2">{profile.name}</h2>
|
||||
<p className="text-xs text-gray-400 mb-4">
|
||||
{new Date(profile.createdAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 group-hover:underline mb-2">{user.name}</h2>
|
||||
{user.profile.description && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-400 mb-4">{user.profile.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
9
lib/LoadingSpinner.tsx
Normal file
9
lib/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
export default function LoadingSpinner() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-white">
|
||||
<div className="w-12 h-12 border-4 border-gray-300 border-t-gray-800 rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,8 +20,7 @@ model User {
|
||||
sessions Session[]
|
||||
|
||||
// Profile Information
|
||||
gender String?
|
||||
description String?
|
||||
profile Profile?
|
||||
|
||||
// Optional for WebAuthn support
|
||||
Authenticator Authenticator[]
|
||||
@@ -30,6 +29,106 @@ model User {
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Profile {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
location String? // Need to normalize later for geospatial
|
||||
description String?
|
||||
contactInfo String?
|
||||
gender Gender?
|
||||
personalityType PersonalityType?
|
||||
conflictStyle ConflictStyle?
|
||||
|
||||
promptAnswers PromptAnswer[] // See below (list of answers)
|
||||
desiredConnections ProfileConnection[]
|
||||
intellectualInterests ProfileInterest[] // Many-to-many (see below)
|
||||
causeAreas ProfileCauseArea[] // Many-to-many (see below)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum Gender {
|
||||
Male
|
||||
Female
|
||||
NonBinary
|
||||
Other
|
||||
}
|
||||
|
||||
enum PersonalityType {
|
||||
Introvert
|
||||
Extrovert
|
||||
Ambivert
|
||||
}
|
||||
|
||||
enum ConflictStyle {
|
||||
Competing
|
||||
Avoidant
|
||||
Compromising
|
||||
Accommodating
|
||||
Collaborating
|
||||
}
|
||||
|
||||
model Connection {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
users ProfileConnection[]
|
||||
}
|
||||
|
||||
model Interest {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
users ProfileInterest[]
|
||||
}
|
||||
|
||||
model CauseArea {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
users ProfileCauseArea[]
|
||||
}
|
||||
|
||||
// Join tables
|
||||
model ProfileConnection {
|
||||
profileId String
|
||||
connectionId String
|
||||
|
||||
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
connection Connection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([profileId, connectionId])
|
||||
}
|
||||
|
||||
// Join tables
|
||||
model ProfileInterest {
|
||||
profileId String
|
||||
interestId String
|
||||
|
||||
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
interest Interest @relation(fields: [interestId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([profileId, interestId])
|
||||
}
|
||||
|
||||
model ProfileCauseArea {
|
||||
profileId String
|
||||
causeAreaId String
|
||||
|
||||
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
causeArea CauseArea @relation(fields: [causeAreaId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([profileId, causeAreaId])
|
||||
}
|
||||
|
||||
model PromptAnswer {
|
||||
id String @id @default(cuid())
|
||||
profileId String
|
||||
prompt String
|
||||
answer String
|
||||
|
||||
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
|
||||
101
prisma/seed.ts
101
prisma/seed.ts
@@ -1,29 +1,92 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import {prisma} from "@/lib/prisma";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const users = await Promise.all([
|
||||
prisma.user.create({
|
||||
data: {
|
||||
email: 'alice@example.com',
|
||||
name: 'Alice',
|
||||
password: await bcrypt.hash('password123', 10),
|
||||
description: 'Alice in Wonderland'
|
||||
},
|
||||
}),
|
||||
]);
|
||||
// Create some interests & cause areas
|
||||
await prisma.interest.createMany({
|
||||
data: [
|
||||
{name: 'Philosophy'},
|
||||
{name: 'AI Safety'},
|
||||
{name: 'Economics'},
|
||||
{name: 'Mathematics'},
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
console.log('Seeding completed.');
|
||||
await prisma.causeArea.createMany({
|
||||
data: [
|
||||
{name: 'Climate Change'},
|
||||
{name: 'AI Alignment'},
|
||||
{name: 'Animal Welfare'},
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
await prisma.connection.createMany({
|
||||
data: [
|
||||
{name: 'Debate Partner'},
|
||||
{name: 'Friendship'},
|
||||
{name: 'Relationship'},
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
// Get actual Interest & CauseArea objects
|
||||
const allInterests = await prisma.interest.findMany();
|
||||
const allCauseAreas = await prisma.causeArea.findMany();
|
||||
const allConnections = await prisma.connection.findMany();
|
||||
|
||||
// Create mock users
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: `user${i}@bayesbond.com`,
|
||||
name: `User ${i}`,
|
||||
image: null,
|
||||
profile: {
|
||||
create: {
|
||||
location: i % 2 === 0 ? 'New York' : 'San Francisco',
|
||||
description: 'I’m a data scientist with a deep interest in decision theory, AI alignment, and effective altruism. When I’m not analyzing models or debating Bayesian reasoning, I enjoy exploring behavioral economics and reading philosophy (Kant and Parfit are favorites). Looking to connect with others who value curiosity, intellectual honesty, and constructive debate. Open to collaborating on research, causal impact projects, or just good conversations over coffee.',
|
||||
gender: i % 2 === 0 ? 'Male' : 'Female',
|
||||
personalityType: i % 3 === 0 ? 'Extrovert' : 'Introvert',
|
||||
conflictStyle: 'Avoidant',
|
||||
contactInfo: `Email: user${i}@bayesbond.com\nPhone: +1 (123) 456-7890`,
|
||||
desiredConnections: {
|
||||
create: [
|
||||
{connectionId: allConnections[i % allConnections.length].id},
|
||||
],
|
||||
},
|
||||
intellectualInterests: {
|
||||
create: [
|
||||
{interestId: allInterests[i % allInterests.length].id},
|
||||
{interestId: allInterests[(i + 1) % allInterests.length].id},
|
||||
],
|
||||
},
|
||||
causeAreas: {
|
||||
create: [
|
||||
{causeAreaId: allCauseAreas[i % allCauseAreas.length].id},
|
||||
],
|
||||
},
|
||||
promptAnswers: {
|
||||
create: [
|
||||
{prompt: 'What motivates you?', answer: 'Curiosity and truth.'},
|
||||
{prompt: 'How do you relate to your closest friends?', answer: 'By sharing our passions.'},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Created user ${user.email}`);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user