diff --git a/app/(app)/alpha/additional-details/_client.tsx b/app/(app)/alpha/additional-details/_client.tsx index 5e67dac8..cf95cc53 100644 --- a/app/(app)/alpha/additional-details/_client.tsx +++ b/app/(app)/alpha/additional-details/_client.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { redirect, useRouter, useSearchParams } from "next/navigation"; import { useSession } from "next-auth/react"; import { @@ -39,6 +39,10 @@ import { Select } from "@/components/ui-components/select"; import { Button } from "@/components/ui-components/button"; import { Heading, Subheading } from "@/components/ui-components/heading"; import { Divider } from "@/components/ui-components/divider"; +import { Avatar } from "@/components/ui-components/avatar"; +import { Text } from "@/components/ui-components/text"; +import { api } from "@/server/trpc/react"; +import { imageUploadToUrl } from "@/utils/fileUpload"; type UserDetails = { username: string; @@ -51,6 +55,12 @@ type UserDetails = { levelOfStudy: string; jobTitle: string; workplace: string; + image: string; +}; + +type ProfilePhoto = { + status: "success" | "error" | "loading" | "idle"; + url: string; }; export default function AdditionalSignUpDetails({ @@ -99,9 +109,15 @@ export default function AdditionalSignUpDetails({ function SlideOne({ details }: { details: UserDetails }) { const router = useRouter(); - + const fileInputRef = useRef(null); + const [profilePhoto, setProfilePhoto] = useState({ + status: "idle", + url: details.image, + }); const { username, name, location } = details; - + const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation(); + const { mutateAsync: updateUserPhotoUrl } = + api.profile.updateProfilePhotoUrl.useMutation(); const { register, handleSubmit, @@ -111,6 +127,26 @@ function SlideOne({ details }: { details: UserDetails }) { defaultValues: { username, name, location }, }); + const handleImageChange = async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + try { + setProfilePhoto({ status: "loading", url: "" }); + const file = e.target.files[0]; + const { status, fileLocation } = await imageUploadToUrl({ + file, + updateUserPhotoUrl, + getUploadUrl, + }); + setProfilePhoto({ status: status, url: fileLocation }); + } catch (error) { + toast.error("Failed to upload profile photo. Please try again."); + setProfilePhoto({ status: "error", url: "" }); + } + } else { + toast.error("Failed to upload profile photo. Please try again."); + } + }; + const onFormSubmit = async (data: TypeSlideOneSchema) => { try { const isSuccess = await slideOneSubmitAction(data); @@ -135,6 +171,47 @@ function SlideOne({ details }: { details: UserDetails }) { + +
+ + +
+ +
+ + + + JPG, GIF or PNG. 10MB max. + +
+
+
+
diff --git a/app/(app)/alpha/additional-details/page.tsx b/app/(app)/alpha/additional-details/page.tsx index dfe5d1be..00f2a8a3 100644 --- a/app/(app)/alpha/additional-details/page.tsx +++ b/app/(app)/alpha/additional-details/page.tsx @@ -22,6 +22,7 @@ export default async function Page() { levelOfStudy: true, jobTitle: true, workplace: true, + image: true, }, where: (user, { eq }) => eq(user.id, userId), }); @@ -37,6 +38,7 @@ export default async function Page() { levelOfStudy: details?.levelOfStudy || "", jobTitle: details?.jobTitle || "", workplace: details?.workplace || "", + image: details?.image || "", }; return ; diff --git a/app/(app)/settings/_client.tsx b/app/(app)/settings/_client.tsx index 04d02cce..feb6a241 100644 --- a/app/(app)/settings/_client.tsx +++ b/app/(app)/settings/_client.tsx @@ -9,7 +9,6 @@ import { toast } from "sonner"; import type { saveSettingsInput } from "@/schema/profile"; import { saveSettingsSchema } from "@/schema/profile"; -import { uploadFile } from "@/utils/s3helpers"; import type { user } from "@/server/db/schema"; import { Button } from "@/components/ui-components/button"; import { Loader2 } from "lucide-react"; @@ -25,6 +24,7 @@ import { Textarea } from "@/components/ui-components/textarea"; import { Switch } from "@/components/ui-components/switch"; import { Divider } from "@/components/ui-components/divider"; import { Text } from "@/components/ui-components/text"; +import { imageUploadToUrl } from "@/utils/fileUpload"; type User = Pick< typeof user.$inferSelect, @@ -75,8 +75,8 @@ const Settings = ({ profile }: { profile: User }) => { }); const { mutate, isError, isSuccess } = api.profile.edit.useMutation(); - const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation(); - const { mutate: updateUserPhotoUrl } = + const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation(); + const { mutateAsync: updateUserPhotoUrl } = api.profile.updateProfilePhotoUrl.useMutation(); const { mutate: updateEmail } = api.profile.updateEmail.useMutation(); @@ -104,52 +104,23 @@ const Settings = ({ profile }: { profile: User }) => { mutate({ ...values, newsletter: weeklyNewsletter, emailNotifications }); }; - const uploadToUrl = async (signedUrl: string, file: File) => { - setProfilePhoto({ status: "loading", url: "" }); - - if (!file) { - setProfilePhoto({ status: "error", url: "" }); - toast.error("Invalid file upload."); - return; - } - - const response = await uploadFile(signedUrl, file); - const { fileLocation } = response; - await updateUserPhotoUrl({ - url: fileLocation, - }); - - return fileLocation; - }; - - const imageChange = async (e: React.ChangeEvent) => { + const handleImageChange = async (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { - const file = e.target.files[0]; - const { size, type } = file; - - await getUploadUrl( - { size, type }, - { - onError(error) { - if (error) return toast.error(error.message); - return toast.error( - "Something went wrong uploading the photo, please retry.", - ); - }, - async onSuccess(signedUrl) { - const url = await uploadToUrl(signedUrl, file); - if (!url) { - return toast.error( - "Something went wrong uploading the photo, please retry.", - ); - } - setProfilePhoto({ status: "success", url }); - toast.success( - "Profile photo successfully updated. This may take a few minutes to update around the site.", - ); - }, - }, - ); + try { + setProfilePhoto({ status: "loading", url: "" }); + const file = e.target.files[0]; + const { status, fileLocation } = await imageUploadToUrl({ + file, + updateUserPhotoUrl, + getUploadUrl, + }); + setProfilePhoto({ status: status, url: fileLocation }); + } catch (error) { + toast.error("Failed to upload profile photo. Please try again."); + setProfilePhoto({ status: "error", url: "" }); + } + } else { + toast.error("Failed to upload profile photo. Please try again."); } }; @@ -225,12 +196,12 @@ const Settings = ({ profile }: { profile: User }) => { id="file-input" name="user-photo" accept="image/png, image/gif, image/jpeg" - onChange={imageChange} + onChange={handleImageChange} className="hidden" ref={fileInputRef} /> - JPG, GIF or PNG. 1MB max. + JPG, GIF or PNG. 10MB max.
diff --git a/utils/fileUpload.ts b/utils/fileUpload.ts new file mode 100644 index 00000000..36d44942 --- /dev/null +++ b/utils/fileUpload.ts @@ -0,0 +1,56 @@ +import { toast } from "sonner"; +import { uploadFile } from "./s3helpers"; + +type UploadToUrlProps = { + file: File; + getUploadUrl: (data: { size: number; type: string }) => Promise; + updateUserPhotoUrl: (data: { + url: string; + }) => Promise<{ role: string; id: string; name: string }>; +}; + +type UploadResult = { + status: "success" | "error" | "loading"; + fileLocation: string; +}; + +export const imageUploadToUrl = async ({ + file, + updateUserPhotoUrl, + getUploadUrl, +}: UploadToUrlProps): Promise => { + if (!file) { + toast.error("Invalid file upload."); + return { status: "error", fileLocation: "" }; + } + + try { + const { size, type } = file; + const signedUrl = await getUploadUrl({ size, type }); + + if (!signedUrl) { + toast.error("Failed to upload profile photo. Please try again."); + return { status: "error", fileLocation: "" }; + } + + const response = await uploadFile(signedUrl, file); + + const { fileLocation } = response; + + if (!fileLocation) { + toast.error("Failed to retrieve file location after upload."); + return { status: "error", fileLocation: "" }; + } + await updateUserPhotoUrl({ url: fileLocation }); + toast.success("Profile photo successfully updated."); + + return { status: "success", fileLocation }; + } catch (error) { + if (error instanceof Error) { + toast.error(error.message); + } else { + toast.error("Failed to upload profile photo. Please try again."); + } + return { status: "error", fileLocation: "" }; + } +};