{user.fullName || user.username}
@@ -185,7 +301,65 @@ export default function ProfilePage() { ))} -My Pets
-Already have an account?{" "} - Log in here + Log in here
diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index bec8bd78..fac5a86f 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -226,10 +226,11 @@ function AppointmentsPage() { useEffect(() => { if (!authLoading && !user) { - router.push("/login"); + const target = preselectedPetId ? `/appointments?petId=${encodeURIComponent(preselectedPetId)}` : "/appointments"; + router.push(`/login?next=${encodeURIComponent(target)}`); } - }, [authLoading, user, router]); + }, [authLoading, user, router, preselectedPetId]); useEffect(() => { if (!token) { diff --git a/web/app/globals.css b/web/app/globals.css index da8ce03b..8571bfeb 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -1076,6 +1076,13 @@ body { align-items: center; justify-content: center; margin-bottom: 0.25rem; + overflow: hidden; +} + +.profile-avatar-image { + width: 100%; + height: 100%; + object-fit: cover; } .profile-name { @@ -1105,6 +1112,37 @@ body { padding-top: 1rem; } +.profile-update-form { + width: 100%; + display: grid; + gap: 0.9rem; +} + +.profile-update-title { + margin: 0.25rem 0 0; + font-size: 1.1rem; + color: #222; +} + +.profile-avatar-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; +} + +.profile-avatar-upload-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.65rem 1rem; + border-radius: 8px; + background: #fff3e0; + color: #a65c00; + font-weight: 600; + cursor: pointer; +} + .profile-field-row { display: flex; justify-content: space-between; diff --git a/web/app/login/page.js b/web/app/login/page.js index dcc78d02..2e7d1fe1 100644 --- a/web/app/login/page.js +++ b/web/app/login/page.js @@ -1,13 +1,25 @@ "use client"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { useState } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; -export default function LoginPage() { +function resolveNextPath(candidate) { + if (!candidate || !candidate.startsWith("/")) { + return "/"; + } + if (candidate.startsWith("//") || candidate.startsWith("/login") || candidate.startsWith("/register")) { + return "/"; + } + return candidate; +} + +function LoginPage() { const {login} = useAuth(); const router = useRouter(); + const searchParams = useSearchParams(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -21,7 +33,7 @@ export default function LoginPage() { try { await login(username, password); - router.push("/"); + router.push(resolveNextPath(searchParams.get("next"))); } catch (err) { @@ -68,9 +80,13 @@ export default function LoginPage() {
Don't have an account?{" "} - Register here + Register here
); } + +export default dynamic(() => Promise.resolve(LoginPage), { + ssr: false, +}); diff --git a/web/app/profile/page.js b/web/app/profile/page.js index 6b28aa35..d0b78c5e 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -7,7 +7,7 @@ import { useAuth } from "@/context/AuthContext"; const API_BASE = ""; export default function ProfilePage() { - const {user, token, loading, logout} = useAuth(); + const {user, token, loading, logout, refreshUser} = useAuth(); const router = useRouter(); const [pets, setPets] = useState([]); @@ -19,14 +19,27 @@ export default function ProfilePage() { const [breed, setBreed] = useState(""); const [submitting, setSubmitting] = useState(false); const [petError, setPetError] = useState(null); + const [profileForm, setProfileForm] = useState({ fullName: "", email: "", phone: "" }); + const [profileSubmitting, setProfileSubmitting] = useState(false); + const [profileError, setProfileError] = useState(null); + const [profileSuccess, setProfileSuccess] = useState(null); + const [avatarSubmitting, setAvatarSubmitting] = useState(false); useEffect(() => { if (!loading && !user) { - router.replace("/login"); + router.replace(`/login?next=${encodeURIComponent("/profile")}`); } }, [user, loading, router]); + useEffect(() => { + setProfileForm({ + fullName: user?.fullName || "", + email: user?.email || "", + phone: user?.phone || "", + }); + }, [user]); + const loadPets = useCallback(() => { if (!token) return; setLoadingPets(true); @@ -50,6 +63,105 @@ export default function ProfilePage() { router.push("/"); } + async function handleProfileSubmit(e) { + e.preventDefault(); + setProfileSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(profileForm), + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || `Request failed (${res.status})`); + } + + await refreshUser(); + setProfileSuccess("Profile updated successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setProfileSubmitting(false); + } + } + + async function handleAvatarUpload(file) { + if (!file) { + return; + } + + const formData = new FormData(); + formData.append("avatar", file); + setAvatarSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me/avatar`, { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + body: formData, + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || "Failed to upload avatar"); + } + + await refreshUser(); + setProfileSuccess(data?.message || "Avatar updated successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setAvatarSubmitting(false); + } + } + + async function handleAvatarDelete() { + setAvatarSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me/avatar`, { + method: "DELETE", + headers: { Authorization: `Bearer ${token}` }, + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || "Failed to delete avatar"); + } + + await refreshUser(); + setProfileSuccess(data?.message || "Avatar removed successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setAvatarSubmitting(false); + } + } + function openAddForm() { setEditingPet(null); setPetName(""); @@ -170,7 +282,11 @@ export default function ProfilePage() {Already have an account?{" "} - Log in here + Log in here