diff --git a/client/src/App.jsx b/client/src/App.jsx index 8ed8745..df10a10 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -10,6 +10,7 @@ import MoviePage from "./pages/MoviePage"; import Navbar from "./components/Navbar"; import TmdbMovie from "./pages/MoviesPage"; import SearchResultPage from "./pages/SearchResultPage"; +import EditProfile from "./pages/EditProfile"; function isTokenExpired(token) { if (!token) return true; @@ -108,6 +109,15 @@ export default function App() { } /> + + : + } + /> + ); diff --git a/client/src/api/user.api.js b/client/src/api/user.api.js index a7fec1d..9c278aa 100644 --- a/client/src/api/user.api.js +++ b/client/src/api/user.api.js @@ -7,13 +7,22 @@ export const getProfile = async (userId) => { }; export const setProfileCover = async (backdrop) => { - const res = await axiosInstance.put('/user/me', { + const res = await axiosInstance.put('/user/me/edit', { cover: backdrop }); return res.data; } +export const editProfileData = async (pfp, about) => { + const res = await axiosInstance.put('/user/me/edit', { + pfp, + about + }); + + return res.data; +} + export const searchUsers = async (query) => { const res = await axiosInstance.get(`/user/search?q=${query}`); return res.data; diff --git a/client/src/api/userFollows.api.js b/client/src/api/userFollows.api.js index a5881f2..1d189ea 100644 --- a/client/src/api/userFollows.api.js +++ b/client/src/api/userFollows.api.js @@ -3,4 +3,9 @@ import axiosInstance from "./axiosInstance"; export const followUser = async (followingId) => { const res = await axiosInstance.post('/me/follow', { followingId }); return res; +} + +export const unfollowUser = async (followingId) => { + const res = await axiosInstance.post('/me/unfollow', { followingId }); + return res; } \ No newline at end of file diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx index a5d30ee..b5a19f5 100644 --- a/client/src/components/Navbar.jsx +++ b/client/src/components/Navbar.jsx @@ -15,7 +15,7 @@ import { ClockIcon } from "lucide-react"; export default function Navbar() { const [isModalOpen, setIsModalOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const { user, logout } = useUserStore(); + const { user, setUser, logout } = useUserStore(); const location = useLocation(); const navigate = useNavigate(); @@ -87,14 +87,12 @@ export default function Navbar() { } label="Movies" /> - {/* SEARCH BAR + DROPDOWN */}
setIsDropdownOpen(true)} /> - {/* We move the dropdown styles here or into the component */}
diff --git a/client/src/pages/EditProfile.jsx b/client/src/pages/EditProfile.jsx new file mode 100644 index 0000000..4468cff --- /dev/null +++ b/client/src/pages/EditProfile.jsx @@ -0,0 +1,153 @@ +import { useState, useRef } from "react"; +import { motion } from "framer-motion"; +import useUserStore from "../store/userStore"; +import defaultPfp from "../assets/default-pfp.jpg"; +import { editProfileData } from "../api/user.api"; +import axios from "axios"; + +export default function EditProfile() { + const { user, setUser } = useUserStore(); + const [loading, setLoading] = useState(false); + const [about, setAbout] = useState(user?.user?.about || ""); + const [previewPfp, setPreviewPfp] = useState(user?.user?.pfp || defaultPfp); + const [selectedPfpFile, setSelectedPfpFile] = useState(null); + + const fileInputRef = useRef(null); + + const handleSave = async (e) => { + e.preventDefault(); + setLoading(true); + try { + const formData = new FormData(); + formData.append("file", selectedPfpFile); + formData.append("upload_preset", import.meta.env.VITE_CLOUDINARY_PRESET); + formData.append("folder", import.meta.env.VITE_CLOUDINARY_ASSET_FOLDER); + + const cloudName = import.meta.env.VITE_CLOUDINARY_CLOUD; + + const cloudinaryRes = await axios.post( + `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`, + formData + ); + + const res = await editProfileData(cloudinaryRes.data.secure_url, about); + if (res.success) { + setUser({ ...user, user: res.user }); + } + setLoading(false); + } catch (err) { + console.error(err); + setLoading(false); + } + }; + + return ( +
+
+ {/* Header */} +
+
+
+ Public Persona +
+

+ Edit Profile +

+
+ +
+ + {/* PFP Upload Section */} +
+

Profile Picture

+
+
+
+ Preview +
+ + { + const file = e.target.files[0]; + if (file) { + setSelectedPfpFile(file); + setPreviewPfp(URL.createObjectURL(file)); + } + }} + /> +
+
+

Change Avatar

+

+ Upload a high-quality cinematic square image.
+ Supports JPG, PNG or WebP. +

+
+
+
+ + {/* Bio / About Section */} +
+

Director's Statement

+
+