diff --git a/src/app/dashboard/(admin)/admin/(settings)/settings/page.tsx b/src/app/dashboard/(admin)/admin/(settings)/settings/page.tsx index 59a75581a..71f077ab9 100644 --- a/src/app/dashboard/(admin)/admin/(settings)/settings/page.tsx +++ b/src/app/dashboard/(admin)/admin/(settings)/settings/page.tsx @@ -18,7 +18,6 @@ import { SelectValue, } from "~/components/ui/select"; import { toast } from "~/components/ui/use-toast"; -import { CloudinaryAsset } from "~/types"; import { InstagramIcon, LinkedinIcon, XIcon } from "./icons"; const pronouns = [ @@ -83,6 +82,7 @@ export default function SettingsPage() { }, }); if (response.data?.data) { + const { avatar_url, profile_pic_url } = response.data.data; setPronoun(response.data.data.pronouns); setSocialLinks({ x: response.data.data.social_links @@ -102,7 +102,7 @@ export default function SettingsPage() { department: response.data.data.department ?? "", username: response.data.data.username ?? "", }); - setProfilePicture(response.data.data.profile_picture); + setProfilePicture(avatar_url || profile_pic_url); setIsSuccess(true); } } catch { @@ -111,6 +111,44 @@ export default function SettingsPage() { })(); }, [data?.access_token, data?.user.id, error]); + const handleImageUpload = async (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + setImage(file); + + const formData = new FormData(); + formData.append("avatar", file); + + try { + const baseUrl = await getApiUrl(); + const UPLOAD_API_URL = `${baseUrl}/api/v1/profile/upload-image`; + + const uploadResponse = await axios.post(UPLOAD_API_URL, formData, { + headers: { + Authorization: `Bearer ${data?.access_token}`, + "Content-Type": "multipart/form-data", + }, + }); + + if (uploadResponse.status === 200) { + const { avatar_url, profile_pic_url } = uploadResponse.data.data; + const profilePicUrl = avatar_url || profile_pic_url; + + setProfilePicture(profilePicUrl); + + setFormData((previousData) => ({ + ...previousData, + profile_pic_url: profilePicture, + })); + } else { + throw new Error("."); + } + } catch { + setError("."); + } + } + }; + const submit = async () => { if (!isValidXUrl(socialLinks.x)) { return toast({ title: "Warning!", description: "Enter a valid X url" }); @@ -133,23 +171,9 @@ export default function SettingsPage() { ...formData, pronouns: pronoun, social_links: Object.values(socialLinks), - profile_picture: "", + profile_pic_url: profilePicture, }; - const formData1 = new FormData(); - formData1.append("file", image!); - formData1.append("upload_preset", "starterhouse"); - formData1.append("api_key", "673723355315667"); - - await fetch(`https://api.cloudinary.com/v1_1/dnik53vns/image/upload`, { - method: "POST", - body: formData1, - }).then(async (response) => { - const data: CloudinaryAsset = await response.json(); - payload.profile_picture = data.url; - }); - setIsPending(true); - const baseUrl = await getApiUrl(); const API_URL = `${baseUrl}/api/v1/profile/${data?.user.id}`; @@ -158,41 +182,43 @@ export default function SettingsPage() { Authorization: `Bearer ${data?.access_token}`, }, }); - // if (updated.status === 200) { - // const newSession: Session = { - // ...data, - // user: { - // ...data?.user, - // id: data?.user.id ?? "", - // first_name: data?.user.first_name ?? "", - // last_name: data?.user.last_name ?? "", - - // role: data?.user.role ?? "", - // bio: formData.bio, - // username: formData.username, - // is_superadmin: data?.user.is_superadmin, - // }, - // expires: data?.expires, - // }; - // } + + const updatedUser = { + ...data?.user, + image: payload.profile_pic_url, + }; + + const event = new CustomEvent("session", { + detail: { session: { ...data, user: updatedUser } }, + }); + + window.dispatchEvent(event); + + setIsSuccess(true); + + window.dispatchEvent( + new CustomEvent("userProfileUpdate", { + detail: { profilePicUrl: profilePicture }, + }), + ); } catch { setIsPending(false); + setError("An error occurred while saving your information"); } finally { setIsPending(false); } }; - const isFormDisabled = - !formData.bio || - !formData.department || - !formData.email || - !formData.jobTitle || - !formData.username || - !socialLinks.instagram || - !socialLinks.linkedin || - !socialLinks.x || - !profilePicture || - !pronoun; + const isFormDisabled = !( + formData.bio && + formData.department && + formData.jobTitle && + formData.username && + socialLinks.instagram && + socialLinks.linkedin && + socialLinks.x && + pronoun + ); return (
@@ -212,16 +238,12 @@ export default function SettingsPage() { className="sr-only" id="profile-picture" type="file" - onChange={(entries) => - setImage( - entries.target.files ? entries.target.files[0] : undefined, - ) - } + onChange={handleImageUpload} accept="image/jpeg,image/png,image/svg+xml" /> - {image && ( + {(image || profilePicture) && ( Picture of the author diff --git a/src/components/card/user-card.tsx b/src/components/card/user-card.tsx index 792e68cab..8f37be0ac 100644 --- a/src/components/card/user-card.tsx +++ b/src/components/card/user-card.tsx @@ -1,7 +1,10 @@ +import axios from "axios"; import { ChevronDown } from "lucide-react"; import { signOut, useSession } from "next-auth/react"; import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; +import { getApiUrl } from "~/actions/getApiUrl"; import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; import { DropdownMenu, @@ -13,6 +16,7 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; +import { toast } from "~/components/ui/use-toast"; import { cn } from "~/lib/utils"; const handleLogout = async () => { @@ -24,6 +28,54 @@ const handleLogout = async () => { const UserCard = () => { const { data: session, status } = useSession(); const { user } = session ?? {}; + const [profilePicUrl, setProfilePicUrl] = useState(""); + + const fetchProfileData = useCallback(async () => { + if (status === "authenticated" && user?.id) { + try { + const baseUrl = await getApiUrl(); + const API_URL = `${baseUrl}/api/v1/profile/${user.id}`; + const response = await axios.get(API_URL, { + headers: { + Authorization: `Bearer ${session?.access_token}`, + }, + }); + if (response.data?.data) { + const { avatar_url, profile_pic_url } = response.data.data; + setProfilePicUrl(avatar_url || profile_pic_url); + } + } catch { + toast({ + title: "Error", + description: "Failed to fetch profile data.", + variant: "destructive", + }); + } + } + }, [status, user?.id, session?.access_token]); + + useEffect(() => { + fetchProfileData(); + const handleProfileUpdate = (event: CustomEvent) => { + if (event.detail && event.detail.profilePicUrl) { + setProfilePicUrl(event.detail.profilePicUrl); + } else { + fetchProfileData(); + } + }; + + window.addEventListener( + "userProfileUpdate", + handleProfileUpdate as EventListener, + ); + + return () => { + window.removeEventListener( + "userProfileUpdate", + handleProfileUpdate as EventListener, + ); + }; + }, [fetchProfileData]); return ( @@ -38,7 +90,10 @@ const UserCard = () => { )} {status === "authenticated" && ( - + {user?.first_name?.charAt(0)} diff --git a/src/contexts/orgContext.tsx b/src/contexts/orgContext.tsx index 9ad83db4d..70ae101a7 100644 --- a/src/contexts/orgContext.tsx +++ b/src/contexts/orgContext.tsx @@ -97,7 +97,7 @@ const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { }); }); } - }, [organizations]); + }, []); useEffect(() => { if (userOrg.length > 0) { @@ -113,7 +113,7 @@ const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { ...uniqueOrgs, ]); } - }, [userOrg, organizations]); + }, []); useEffect(() => { document.body.style.overflow = isAnyModalOpen ? "hidden" : "auto";