From 9381a236d4b120e7ba0429339ec9bd51046cd944 Mon Sep 17 00:00:00 2001 From: mina Date: Fri, 30 Oct 2026 00:41:48 +0100 Subject: [PATCH 1/4] feat: add Zustand auth store --- frontend/package-lock.json | 32 +++++++++++++++++++++++++++++++- frontend/package.json | 3 ++- frontend/src/store/authStore.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 frontend/src/store/authStore.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ea8f2bd..9dd1ad0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,7 +17,8 @@ "react-router-dom": "^7.18.4", "react-select": "^5.10.2", "rxjs": "^7.8.2", - "tailwindcss": "^4.3.3" + "tailwindcss": "^4.3.3", + "zustand": "^5.0.16" }, "devDependencies": { "@babel/core": "^7.29.7", @@ -4133,6 +4134,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.16.tgz", + "integrity": "sha512-Uv7e327ihlCfayfja3hDc0M64Upev1LWBKpRobdK4NyovDDPeeX4imWXc/wzC1yc6fkYPoAY/ncKVPAs2jrtQw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index 59f9b7e..b6deb32 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,8 @@ "react-router-dom": "^7.18.4", "react-select": "^5.10.2", "rxjs": "^7.8.2", - "tailwindcss": "^4.3.3" + "tailwindcss": "^4.3.3", + "zustand": "^5.0.16" }, "devDependencies": { "@babel/core": "^7.29.7", diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts new file mode 100644 index 0000000..efcd60f --- /dev/null +++ b/frontend/src/store/authStore.ts @@ -0,0 +1,31 @@ +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; +import type { User } from "../types/User"; + +type AuthState = { + user: User | null; + token: string | null; + setAuth: (user: User, token: string) => void; + logout: () => void; +}; + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + token: null, + + setAuth: (user, token) => { + set({ user, token }); + }, + + logout: () => { + set({ user: null, token: null }); + }, + }), + { + name: "auth", + storage: createJSONStorage(() => sessionStorage), + }, + ), +); \ No newline at end of file From a47eb93ca7c34506671a4380201fd21ae6cd1d03 Mon Sep 17 00:00:00 2001 From: mina Date: Fri, 30 Oct 2026 00:42:13 +0100 Subject: [PATCH 2/4] refactor: use auth store for login and registration --- frontend/src/pages/CreateAccountPage.tsx | 5 +++-- frontend/src/pages/LoginPage.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/CreateAccountPage.tsx b/frontend/src/pages/CreateAccountPage.tsx index 1bc91d7..f4387cb 100644 --- a/frontend/src/pages/CreateAccountPage.tsx +++ b/frontend/src/pages/CreateAccountPage.tsx @@ -1,6 +1,7 @@ import { useState, type SubmitEvent as ReactSubmitEvent } from "react"; import { Link, useNavigate } from "react-router-dom"; import type { User } from "../types/User"; +import { useAuthStore } from "../store/authStore"; type RegisterResponse = { data?: { register?: User }; @@ -19,6 +20,7 @@ type LoginResponse = { export default function CreateAccount() { const navigate = useNavigate(); + const setAuth = useAuthStore((state) => state.setAuth); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -95,8 +97,7 @@ export default function CreateAccount() { const { user, token } = loginResult.data.login; - sessionStorage.setItem("user", JSON.stringify(user)); - sessionStorage.setItem("token", token); + setAuth(user, token); navigate("/profile"); } catch (error) { diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index dbd8203..e33bb1f 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -1,6 +1,7 @@ import { Link, useNavigate } from "react-router-dom"; import { useState, type SubmitEvent as ReactSubmitEvent } from "react"; import type { User } from "../types/User"; +import { useAuthStore } from "../store/authStore"; type LoginResponse = { data?: { @@ -14,6 +15,7 @@ type LoginResponse = { function Login() { const navigate = useNavigate(); + const setAuth = useAuthStore((state) => state.setAuth); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -58,8 +60,7 @@ function Login() { const { user, token } = result.data.login; - sessionStorage.setItem("user", JSON.stringify(user)); - sessionStorage.setItem("token", token); + setAuth(user, token); navigate("/profile"); } catch (error) { From 2a68b5616759ed346d830e177c7414d5ea662537 Mon Sep 17 00:00:00 2001 From: mina Date: Fri, 30 Oct 2026 00:42:37 +0100 Subject: [PATCH 3/4] refactor: use auth store for profile navigation --- frontend/src/components/Hero.tsx | 3 ++- frontend/src/components/ProtectedRoute.tsx | 6 ++++-- frontend/src/pages/ProfilePage.tsx | 25 +++++++--------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/Hero.tsx b/frontend/src/components/Hero.tsx index 7422ed4..fd50640 100644 --- a/frontend/src/components/Hero.tsx +++ b/frontend/src/components/Hero.tsx @@ -2,9 +2,10 @@ import heroImageBack from "../assets/hero2.png"; import heroImageFront from "../assets/hero1.png"; import { CircleUserRound } from "lucide-react"; import { Link } from "react-router-dom"; +import { useAuthStore } from "../store/authStore"; function Hero() { - const isLoggedIn = Boolean(sessionStorage.getItem("token")); + const isLoggedIn = useAuthStore((state) => Boolean(state.token)); return (
diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx index e01f97e..6366ab1 100644 --- a/frontend/src/components/ProtectedRoute.tsx +++ b/frontend/src/components/ProtectedRoute.tsx @@ -1,11 +1,13 @@ import { Navigate } from "react-router-dom"; +import type { ReactNode } from "react"; +import { useAuthStore } from "../store/authStore"; type ProtectedRouteProps = { - children: React.ReactNode; + children: ReactNode; }; export default function ProtectedRoute({ children }: ProtectedRouteProps) { - const token = sessionStorage.getItem("token"); + const token = useAuthStore((state) => state.token); if (!token) { return ; diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index 447da68..a140cc3 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -1,4 +1,3 @@ -import type { User } from "../types/User"; import GalleryCard from "../components/GalleryCard"; import { LucidePlus } from "lucide-react"; import { useEffect, useState } from "react"; @@ -6,6 +5,7 @@ import type { Gallery } from "../types/Gallery"; import CreateGalleryModal from "../components/CreateGalleryModal"; import { useGalleryCoverImages } from "../hooks/useGalleryCoverImages"; import { Link, useNavigate } from "react-router-dom"; +import { useAuthStore } from "../store/authStore"; const coverColors = [ "#F3CA34", // Yellow @@ -19,6 +19,10 @@ const coverColors = [ export default function ProfilePage() { const navigate = useNavigate(); + const user = useAuthStore((state) => state.user); + const token = useAuthStore((state) => state.token); + const logout = useAuthStore((state) => state.logout); + const [coverColor] = useState(() => { const randomIndex = Math.floor(Math.random() * coverColors.length); return coverColors[randomIndex]; @@ -28,8 +32,6 @@ export default function ProfilePage() { const [showCreateGallery, setShowCreateGallery] = useState(false); const [profileImage, setProfileImage] = useState(null); - const storedUser = sessionStorage.getItem("user"); - const [galleries, setGalleries] = useState([]); const galleryCoverImages = useGalleryCoverImages(galleries); const [isLoading, setIsLoading] = useState(true); // Galleries @@ -48,8 +50,6 @@ export default function ProfilePage() { `; try { - const token = sessionStorage.getItem("token"); - const response = await fetch("/graphql", { method: "POST", headers: { @@ -79,7 +79,7 @@ export default function ProfilePage() { } void fetchProfileImage(); - }, []); + }, [token]); useEffect(() => { async function fetchGalleries() { @@ -95,8 +95,6 @@ export default function ProfilePage() { `; try { - const token = sessionStorage.getItem("token"); - const response = await fetch("/graphql", { method: "POST", headers: { @@ -125,9 +123,7 @@ export default function ProfilePage() { } void fetchGalleries(); - }, [refreshGalleries]); - - const user: User | null = storedUser ? JSON.parse(storedUser) : null; + }, [refreshGalleries, token]); async function handleCoverImageChange( event: React.ChangeEvent, @@ -160,8 +156,6 @@ export default function ProfilePage() { `; try { - const token = sessionStorage.getItem("token"); - const response = await fetch("/graphql", { method: "POST", headers: { @@ -227,8 +221,6 @@ export default function ProfilePage() { `; try { - const token = sessionStorage.getItem("token"); - const response = await fetch("/graphql", { method: "POST", headers: { @@ -262,8 +254,7 @@ export default function ProfilePage() { } function handleLogout() { - sessionStorage.removeItem("user"); - sessionStorage.removeItem("token"); + logout(); navigate("/"); } From 50fdd1acf43e7f33b30588900abe02bd65fefc6a Mon Sep 17 00:00:00 2001 From: mina Date: Fri, 30 Oct 2026 00:42:56 +0100 Subject: [PATCH 4/4] refactor: use shared auth token in gallery modals --- frontend/src/components/AddToGalleryModal.tsx | 9 ++++----- frontend/src/components/CreateGalleryModal.tsx | 5 +++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/AddToGalleryModal.tsx b/frontend/src/components/AddToGalleryModal.tsx index dde823b..cf672e7 100644 --- a/frontend/src/components/AddToGalleryModal.tsx +++ b/frontend/src/components/AddToGalleryModal.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import type { Gallery } from "../types/Gallery"; +import { useAuthStore } from "../store/authStore"; type AddToGalleryModalProps = { artworkId: number; @@ -28,6 +29,8 @@ export default function AddToGalleryModal({ const [loading, setLoading] = useState(true); const [message, setMessage] = useState(""); + const token = useAuthStore((state) => state.token); + useEffect(() => { async function fetchGalleries() { const query = ` @@ -42,8 +45,6 @@ export default function AddToGalleryModal({ `; try { - const token = sessionStorage.getItem("token"); - const response = await fetch("/graphql", { method: "POST", headers: { @@ -74,7 +75,7 @@ export default function AddToGalleryModal({ } void fetchGalleries(); - }, []); + }, [token]); async function handleAddToGallery(galleryId: string) { const mutation = ` @@ -93,8 +94,6 @@ export default function AddToGalleryModal({ try { setMessage(""); - const token = sessionStorage.getItem("token"); - const response = await fetch("/graphql", { method: "POST", headers: { diff --git a/frontend/src/components/CreateGalleryModal.tsx b/frontend/src/components/CreateGalleryModal.tsx index 0300e76..48c22fa 100644 --- a/frontend/src/components/CreateGalleryModal.tsx +++ b/frontend/src/components/CreateGalleryModal.tsx @@ -1,5 +1,6 @@ import { LucideX } from "lucide-react"; import { useState } from "react"; +import { useAuthStore } from "../store/authStore"; type CreateGalleryModalProps = { onClose: () => void; @@ -14,6 +15,8 @@ export default function CreateGalleryModal({ const [isCreating, setIsCreating] = useState(false); const [error, setError] = useState(""); + const token = useAuthStore((state) => state.token); + async function handleCreateGallery() { if (!title.trim() || isCreating) return; @@ -32,8 +35,6 @@ export default function CreateGalleryModal({ `; try { - const token = sessionStorage.getItem("token"); - const response = await fetch("/graphql", { method: "POST", headers: {