diff --git a/apps/web/app/lib/actions.ts b/apps/web/app/lib/actions.ts index 860dd91..566d5ff 100644 --- a/apps/web/app/lib/actions.ts +++ b/apps/web/app/lib/actions.ts @@ -11,6 +11,7 @@ import { CreateMaterialFields, CreateReplyFields, LoginFields, + UserFields, PrismaRoom, RoomData, RoomFields, @@ -58,6 +59,31 @@ export async function authorizeUser(email: string, password: string) { return { success: true }; } +export async function updateUser(id: string, data: UserFields) { + const session = await getSession(); + if (!session) throw new Error("Unauthroized"); + + const { firstName, lastName, image } = data; + + await prisma.user.update({ + where: { id }, + data: { + firstName: firstName, + lastName: lastName, + avatar: image, + }, + }); +} + +export async function deleteUser(id: string) { + const session = await getSession(); + if (!session) throw new Error("Unauthorized"); + if (!id) throw new Error("No user id provided"); + + await prisma.user.delete({ where: { id } }); + revalidatePath("/"); +} + export async function createStudyGroup(data: CourseFields) { const session = await getSession(); if (!session) throw new Error("Unauthorized"); diff --git a/apps/web/app/lib/types.ts b/apps/web/app/lib/types.ts index 2a3a504..9280d14 100644 --- a/apps/web/app/lib/types.ts +++ b/apps/web/app/lib/types.ts @@ -90,6 +90,12 @@ export type LoginFields = { password: string; }; +export type UserFields = { + firstName: string; + lastName: string; + image: string; +}; + export type CreateReplyFields = { body: string; discussionId: string; diff --git a/apps/web/app/platform/profile/page.tsx b/apps/web/app/platform/profile/page.tsx new file mode 100644 index 0000000..f430778 --- /dev/null +++ b/apps/web/app/platform/profile/page.tsx @@ -0,0 +1,14 @@ +import View from "./view"; +import { getSession } from "@/app/lib/session"; + +export default async function Profile() { + const session = await getSession(); + + return ( + <> +
+ +
+ + ); +} diff --git a/apps/web/app/platform/profile/view.tsx b/apps/web/app/platform/profile/view.tsx new file mode 100644 index 0000000..6b393b6 --- /dev/null +++ b/apps/web/app/platform/profile/view.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { Icons } from "@/components/icons"; +import { Avatar, AvatarImage } from "@ui/components/ui/avatar"; +import { Button } from "@ui/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@ui/components/ui/dialog"; +import { Input } from "@ui/components/ui/input"; +import { Label } from "@ui/components/ui/label"; +import { getUserById, updateUser, deleteUser } from "@/app/lib/actions"; +import { useMemo, useState } from "react"; + +function DeleteAccountDialog({ userId }) { + const [open, setOpen] = useState(false); + const [deleteText, setDeleteText] = useState(null); + const [deleteError, setDeleteError] = useState(null); + + const handleDeleteTextChange = (e: React.ChangeEvent) => { + if (e.target.value === "") { + setDeleteError(false); + } + setDeleteText(e.target.value); + }; + + const handleDelete = () => { + if (deleteText !== "delete") { + setDeleteError(true); + return; + } + + // TODO: Handle deleting user and rerouting to home page + //deleteUser(userId); + }; + + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen) { + setDeleteError(false); + } + setOpen(nextOpen); + }; + + return ( + + + + + + + Are you sure? + + + This action is irreversible. Please type 'delete' to confirm. + + + {deleteError && ( +
+ Please type 'delete' to confirm +
+ )} + + + +
+
+ ); +} + +export default function View({ session }: { session: any }) { + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [loading, setLoading] = useState(false); + const [nameError, setNameError] = useState(null); + const [imageError, setImageError] = useState(null); + const [profileImage, setProfileImage] = useState(null); + const [profileImagePreview, setProfileImagePreview] = useState( + null + ); + const [uploadSuccessful, setUploadSuccessful] = useState( + null + ); + + const user = useMemo(() => { + async function fetchUser() { + setLoading(true); + try { + const userData = await getUserById(session.userId); + setLoading(false); + setFirstName(userData.firstName); + setLastName(userData.lastName); + return userData; + } catch (error) { + setLoading(false); + return null; + } + } + + return fetchUser(); + }, [session]); + + const handleFirstNameChange = (value: string) => { + setFirstName(value); + }; + + const handleLastNameChange = (value: string) => { + setLastName(value); + }; + + const submitUserChanges = async (e: React.FormEvent) => { + e.preventDefault(); + if (!firstName) { + setNameError("You must at least have a first name!"); + return; + } + + const updatedData = { + firstName, + lastName: lastName === "" ? null : lastName, + image: null, + }; + + setLoading(true); + setNameError(null); + + try { + await updateUser(session.userId, updatedData); + setUploadSuccessful(true); + } catch (error) { + console.error("Failed to update user:", error); + setUploadSuccessful(false); + } + setLoading(false); + }; + + const handleProfileImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] || null; + setProfileImage(file); + if (file) { + setImageError(null); + const reader = new FileReader(); + reader.onloadend = () => { + setProfileImagePreview(reader.result as string); + }; + reader.readAsDataURL(file); + } else { + setImageError("Could not load image"); + setProfileImagePreview(null); + } + }; + + if (loading) { + return ( +
+ +

Loading...

+
+ ); + } + + if (!user) { + return null; + } + + return ( +
+

Profile

+
+
+
+ + handleFirstNameChange(e.target.value)} + placeholder="First Name" + /> +
+
+ + handleLastNameChange(e.target.value)} + placeholder="Last Name" + /> +
+
+ {nameError &&
{nameError}
} +
+
+ +
+ {profileImagePreview && ( + + + + )} + +
+ {imageError && ( +
{imageError}
+ )} +
+
+ {uploadSuccessful === null ? null : uploadSuccessful ? ( +
+ User changes uploaded successfully! +
+ ) : ( +
Failed to upload user changes
+ )} +
+ +
+

+ Delete Account +

+ + +
+ ); +} diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 013f241..33256b0 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -78,18 +78,18 @@ model Discussion { body String category String posterId String - poster User @relation(fields: [posterId], references: [id]) + poster User @relation(fields: [posterId], references: [id], onDelete: Cascade) Reply Reply[] } model Reply { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - body String - discussionId String @map("discussionId") - posterId String @map("posterId") - discussion Discussion @relation(fields: [discussionId], references: [id]) - poster User @relation(fields: [posterId], references: [id]) + id String @id @default(cuid()) + createdAt DateTime @default(now()) + body String + discussionId String @map("discussionId") + posterId String @map("posterId") + discussion Discussion @relation(fields: [discussionId], references: [id], onDelete: Cascade) + poster User @relation(fields: [posterId], references: [id], onDelete: Cascade) } model Course {