From ef358cd565c2a77170dac15ccdd0e294cf198930 Mon Sep 17 00:00:00 2001 From: s-egge Date: Wed, 29 May 2024 11:51:23 -0700 Subject: [PATCH 1/2] web: add profile page base --- apps/web/app/platform/profile/page.tsx | 14 ++++ apps/web/app/platform/profile/view.tsx | 109 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 apps/web/app/platform/profile/page.tsx create mode 100644 apps/web/app/platform/profile/view.tsx 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..dbfd3fb --- /dev/null +++ b/apps/web/app/platform/profile/view.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { Icons } from "@/components/icons"; +import { Button } from "@ui/components/ui/button"; +import { Input } from "@ui/components/ui/input"; +import { Label } from "@ui/components/ui/label"; +import { getUserById } from "@/app/lib/actions"; +import { useEffect, useState } from "react"; + +function Name({ user }: { user: any }) { + return ( + <> +

Name

+
+
+ + +
+
+ + +
+
+
+ +
+ + ); +} + +function Password() { + return ( +
+

Password

+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ ); +} + +export default function View({ session }: { session: any }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + async function fetchUser() { + setLoading(true); + const userData = await getUserById(session.userId); + setUser(userData); + console.log(userData); + setLoading(false); + } + + fetchUser(); + }, [session.userId]); + + if (loading) { + return ( +
+ +

Loading...

+
+ ); + } + + if (!user) { + return null; + } + + return ( +
+ + +
+ ); +} From ace0a63740e2930ddce255344c43fb07bedb77d3 Mon Sep 17 00:00:00 2001 From: s-egge Date: Wed, 5 Jun 2024 17:06:29 -0700 Subject: [PATCH 2/2] feat: add name and avatar change, delete profile --- apps/web/app/lib/actions.ts | 26 +++ apps/web/app/lib/types.ts | 6 + apps/web/app/platform/profile/view.tsx | 283 ++++++++++++++++++------- packages/database/prisma/schema.prisma | 6 +- 4 files changed, 247 insertions(+), 74 deletions(-) diff --git a/apps/web/app/lib/actions.ts b/apps/web/app/lib/actions.ts index 7e1447d..9908aa2 100644 --- a/apps/web/app/lib/actions.ts +++ b/apps/web/app/lib/actions.ts @@ -7,6 +7,7 @@ import { createSession, updateSession, getSession } from "./session"; import { CreateReplyFields, LoginFields, + UserFields, CourseFields, RoomFields, } from "./types"; @@ -44,6 +45,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 createCourse(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 93d40f3..5c40bff 100644 --- a/apps/web/app/lib/types.ts +++ b/apps/web/app/lib/types.ts @@ -73,6 +73,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/view.tsx b/apps/web/app/platform/profile/view.tsx index dbfd3fb..6b393b6 100644 --- a/apps/web/app/platform/profile/view.tsx +++ b/apps/web/app/platform/profile/view.tsx @@ -1,91 +1,165 @@ "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 } from "@/app/lib/actions"; -import { useEffect, useState } from "react"; +import { getUserById, updateUser, deleteUser } from "@/app/lib/actions"; +import { useMemo, useState } from "react"; -function Name({ user }: { user: any }) { - return ( - <> -

Name

-
-
- - -
-
- - -
-
-
- -
- - ); -} +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); + }; -function Password() { return ( -
-

Password

-
- - -
-
- - -
-
- - -
-
- -
-
+ + + + + + + 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 [user, setUser] = useState(null); + 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 + ); - useEffect(() => { + const user = useMemo(() => { async function fetchUser() { setLoading(true); - const userData = await getUserById(session.userId); - setUser(userData); - console.log(userData); - setLoading(false); + 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; } - fetchUser(); - }, [session.userId]); + 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 ( @@ -102,8 +176,75 @@ export default function View({ session }: { session: any }) { 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 b59c09d..0ae2d7d 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -77,7 +77,7 @@ 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[] } @@ -87,8 +87,8 @@ model Reply { body String discussionId String @map("discussionId") posterId String @map("posterId") - discussion Discussion @relation(fields: [discussionId], references: [id]) - poster User @relation(fields: [posterId], references: [id]) + discussion Discussion @relation(fields: [discussionId], references: [id], onDelete: Cascade) + poster User @relation(fields: [posterId], references: [id], onDelete: Cascade) } model Course {