diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5817264..6f18051 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import CreateAccount from "./pages/CreateAccountPage"; import ProfilePage from "./pages/ProfilePage"; import ArtworkDetailPage from "./pages/ArtworkDetailPage"; import Footer from "./components/Footer"; +import ProtectedRoute from "./components/ProtectedRoute"; function App() { // No footers for Login and CreateAccount Pages @@ -19,7 +20,14 @@ function App() { } /> } /> } /> - } /> + + + + } + /> } /> diff --git a/frontend/src/components/Hero.tsx b/frontend/src/components/Hero.tsx index 8eecfb6..7422ed4 100644 --- a/frontend/src/components/Hero.tsx +++ b/frontend/src/components/Hero.tsx @@ -4,11 +4,13 @@ import { CircleUserRound } from "lucide-react"; import { Link } from "react-router-dom"; function Hero() { + const isLoggedIn = Boolean(sessionStorage.getItem("token")); + return (
diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..e01f97e --- /dev/null +++ b/frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,15 @@ +import { Navigate } from "react-router-dom"; + +type ProtectedRouteProps = { + children: React.ReactNode; +}; + +export default function ProtectedRoute({ children }: ProtectedRouteProps) { + const token = sessionStorage.getItem("token"); + + if (!token) { + return ; + } + + return children; +} \ No newline at end of file diff --git a/frontend/src/pages/ArtworkDetailPage.tsx b/frontend/src/pages/ArtworkDetailPage.tsx index 0cad80c..f6ff566 100644 --- a/frontend/src/pages/ArtworkDetailPage.tsx +++ b/frontend/src/pages/ArtworkDetailPage.tsx @@ -55,7 +55,7 @@ export default function ArtworkDetailPage() { type="button" aria-label="Add artwork to gallery" onClick={() => setShowGalleryModal(true)} - className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-gray-300 text-2xl font-light text-gray-500 transition hover:border-gray-500 hover:text-black" + className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-gray-300 text-2xl font-light text-gray-500 transition cursor-pointer hover:border-gray-500 hover:text-black" > + diff --git a/frontend/src/pages/CreateAccountPage.tsx b/frontend/src/pages/CreateAccountPage.tsx index 71069b5..1bc91d7 100644 --- a/frontend/src/pages/CreateAccountPage.tsx +++ b/frontend/src/pages/CreateAccountPage.tsx @@ -1,5 +1,5 @@ import { useState, type SubmitEvent as ReactSubmitEvent } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import type { User } from "../types/User"; type RegisterResponse = { @@ -7,7 +7,19 @@ type RegisterResponse = { errors?: Array<{ message: string }>; }; +type LoginResponse = { + data?: { + login: { + user: User; + token: string; + }; + }; + errors?: { message: string }[]; +}; + export default function CreateAccount() { + const navigate = useNavigate(); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [message, setMessage] = useState(""); @@ -46,8 +58,47 @@ export default function CreateAccount() { throw new Error("The server did not return the new account."); } - setMessage(`Account created for ${result.data.register.username}.`); - setPassword(""); + const loginQuery = ` + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + user { + id + username + } + token + } + } + `; + + const loginResponse = await fetch("/graphql", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: loginQuery, + variables: { username, password }, + }), + }); + + if (!loginResponse.ok) { + throw new Error(`Server error (${loginResponse.status}).`); + } + + const loginResult = (await loginResponse.json()) as LoginResponse; + + if (loginResult.errors?.length) { + throw new Error(loginResult.errors[0].message); + } + + if (!loginResult.data?.login) { + throw new Error("Account was created, but login failed."); + } + + const { user, token } = loginResult.data.login; + + sessionStorage.setItem("user", JSON.stringify(user)); + sessionStorage.setItem("token", token); + + navigate("/profile"); } catch (error) { setMessage( error instanceof Error ? error.message : "Something went wrong.", diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 247a4ef..dbd8203 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -1,4 +1,4 @@ -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { useState, type SubmitEvent as ReactSubmitEvent } from "react"; import type { User } from "../types/User"; @@ -13,6 +13,8 @@ type LoginResponse = { }; function Login() { + const navigate = useNavigate(); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [message, setMessage] = useState(""); @@ -59,8 +61,7 @@ function Login() { sessionStorage.setItem("user", JSON.stringify(user)); sessionStorage.setItem("token", token); - setMessage(`Welcome back, ${user.username}.`); - setPassword(""); + navigate("/profile"); } catch (error) { setMessage( error instanceof Error ? error.message : "Something went wrong.", diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index cb75cb2..447da68 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import type { Gallery } from "../types/Gallery"; import CreateGalleryModal from "../components/CreateGalleryModal"; import { useGalleryCoverImages } from "../hooks/useGalleryCoverImages"; +import { Link, useNavigate } from "react-router-dom"; const coverColors = [ "#F3CA34", // Yellow @@ -16,6 +17,8 @@ const coverColors = [ ]; export default function ProfilePage() { + const navigate = useNavigate(); + const [coverColor] = useState(() => { const randomIndex = Math.floor(Math.random() * coverColors.length); return coverColors[randomIndex]; @@ -258,8 +261,29 @@ export default function ProfilePage() { reader.readAsDataURL(file); } + function handleLogout() { + sessionStorage.removeItem("user"); + sessionStorage.removeItem("token"); + navigate("/"); + } + return (
+ + ← Back + + + +
+ + +