diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 27fa083..fa86ee1 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -5,6 +5,7 @@ import fetch from "node-fetch"; const router = express.Router(); +// This route is used to redirect the user to Discord's login page router.get("/login", (req, res) => { const state = crypto.randomBytes(16).toString("hex"); req.session.state = state; @@ -20,6 +21,7 @@ router.get("/login", (req, res) => { res.redirect(`https://discord.com/api/oauth2/authorize?${params}`); }); +// This route is used to exchange the code for a token router.get("/authorize", async (req, res) => { if (req.query.state !== req.session.state) { return res.status(400).json({ error: "State mismatch" }); @@ -64,4 +66,14 @@ router.get("/authorize", async (req, res) => { } }); +// This route is used to check if the user is logged in (wip , have to check if the token is valid) +router.get("/status", (req, res) => { + const token = req.cookies.token; + if (!token) { + return res.status(401).json({ error: "Unauthorized" }); + } + + return res.json({ status: "logged in" }); +}); + export default router; diff --git a/backend/routes/build.js b/backend/routes/build.js index a36a530..a8c9265 100644 --- a/backend/routes/build.js +++ b/backend/routes/build.js @@ -22,20 +22,28 @@ export default function (db) { try { if (user?.user_id && id) { const existingBuild = await db.collection("builds").findOne({ - _id: new ObjectId(id), + _id: ObjectId.createFromHexString(id), }); if (existingBuild?.user_id === user.user_id) { - await db - .collection("builds") - .updateOne({ _id: new ObjectId(id) }, { $set: { build } }); + await db.collection("builds").updateOne( + { _id: ObjectId.createFromHexString(id) }, + { + $set: { + build, + timestamp: new Date(), + }, + } + ); return res.json({ status: "updated", id }); } } const result = await db.collection("builds").insertOne({ build, - ...(user ? { user_id: user.user_id } : { timestamp: new Date() }), + ...(user + ? { user_id: user.user_id, timestamp: new Date() } + : { timestamp: new Date() }), }); res.json({ status: "created", id: result.insertedId.toString() }); @@ -44,10 +52,25 @@ export default function (db) { } }); + router.get("/saved-builds", async (req, res) => { + const user = getUser(req); + if (!user?.user_id) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const builds = await db + .collection("builds") + .find({ user_id: user.user_id }) + .sort({ timestamp: -1 }) + .toArray(); + + res.json(builds); + }); + router.get("/:id", async (req, res) => { try { const build = await db.collection("builds").findOne({ - _id: new ObjectId(req.params.id), + _id: ObjectId.createFromHexString(req.params.id), }); if (build) { diff --git a/frontend/src/components/global-header.jsx b/frontend/src/components/global-header.jsx new file mode 100644 index 0000000..f5d43c8 --- /dev/null +++ b/frontend/src/components/global-header.jsx @@ -0,0 +1,69 @@ +import React from "react"; +import { Link, Outlet, useLoaderData } from "react-router-dom"; + +const GlobalHeader = () => { + // get loader data from the router + const logged = useLoaderData(); + return ( + <> +
+

SW:LC Rune Builder

+ {logged ? ( + + + + ) : ( + + + + )} +
+ + + ); +}; + +export default GlobalHeader; + +// discord button +const DiscordButton = () => { + return ( + + ); +}; + +// profile button (only visible when logged in) +const ProfileButton = () => { + return ( + + + + ); +}; diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index 82b8ba7..70a59dc 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -12,44 +12,69 @@ import reportWebVitals from "./reportWebVitals"; import ErrorPage from "./pages/error-page"; import Builder from "./pages/build/builder"; import Callback from "./pages/Callback"; +import GlobalHeader from "./components/global-header"; +import { hasTokenCookie } from "./utils/queries"; +import Profile from "./pages/profile"; const router = createBrowserRouter([ { - path: "/", - element: , - errorElement: , - }, - { - path: "/login", + element: , loader: async () => { - // redirect to /api/login - window.location.href = "/api/login"; - return null; + return hasTokenCookie(); }, - element: null, - }, - { - path: "/build/new", - element: , errorElement: , - }, - { - path: "/build/:id", - loader: async ({ params }) => { - const response = await fetch(`/api/build/${params.id}`); - if (!response.ok) { - console.error("Build not found"); - return redirect("/build/new"); - } - const data = await response.json(); - return data; - }, - element: , - errorElement: , - }, - { - path: "/callback", - element: , + children: [ + { + path: "/", + element: , + errorElement: , + }, + { + path: "/login", + loader: async () => { + // redirect to /api/login + window.location.href = "/api/login"; + return null; + }, + element: null, + }, + { + path: "/profile", + loader: async () => { + const response = await fetch("/api/build/saved-builds"); + if (!response.ok) { + console.error("Unauthorized"); + return redirect("/login"); + } + const data = await response.json(); + return data; + }, + element: , + }, + { + path: "/build/new", + element: , + errorElement: , + }, + { + path: "/build/:id", + loader: async ({ params }) => { + const response = await fetch(`/api/build/${params.id}`); + if (!response.ok) { + console.error("Build not found"); + return redirect("/build/new"); + } + const data = await response.json(); + return data; + }, + element: , + errorElement: , + }, + { + path: "/callback", + element: , + }, + ], }, ]); diff --git a/frontend/src/pages/build/builder.jsx b/frontend/src/pages/build/builder.jsx index a441f65..4d16889 100644 --- a/frontend/src/pages/build/builder.jsx +++ b/frontend/src/pages/build/builder.jsx @@ -15,6 +15,7 @@ const PERCENTAGE_STATS = ["acc", "cdd", "cr", "cd", "res", "pen"]; // Initial state structure for a build export const initialBuildState = { + name: "", monster: null, currentRuneSlot: 1, runeSet: { @@ -54,6 +55,7 @@ function Builder() { const [build, setBuild] = React.useState({ ...initialBuildState, + name: `build-${Date.now()}`, currentRuneSlot: 1, // Ensure this exists in initial state }); @@ -237,6 +239,18 @@ function Builder() { {/* Rune Selection Panel */}
+
+

Build Name:

+ + setBuild((prev) => ({ ...prev, name: e.target.value })) + } + /> +

Rune Set:

diff --git a/frontend/src/pages/error-page.jsx b/frontend/src/pages/error-page.jsx index 6ca56f5..f5c02f4 100644 --- a/frontend/src/pages/error-page.jsx +++ b/frontend/src/pages/error-page.jsx @@ -5,7 +5,10 @@ export default function ErrorPage() { console.error(error); return ( -
+

Oops!

Sorry, an unexpected error has occurred.

diff --git a/frontend/src/pages/profile.jsx b/frontend/src/pages/profile.jsx new file mode 100644 index 0000000..1c94377 --- /dev/null +++ b/frontend/src/pages/profile.jsx @@ -0,0 +1,30 @@ +import { Link, useLoaderData } from "react-router-dom"; +import { deserializeBuildState } from "../utils/serializer"; + +const Profile = () => { + const builds = useLoaderData(); + return ( +

+

Your Builds

+
    + {builds.map((build) => { + // deserialize the build object + const b = deserializeBuildState(build.build); + return ( +
  • + +

    {b.name}

    +

    Monster: {b.monster.name}

    + +
  • + ); + })} +
+
+ ); +}; + +export default Profile; diff --git a/frontend/src/utils/queries.jsx b/frontend/src/utils/queries.jsx new file mode 100644 index 0000000..8e5a7c1 --- /dev/null +++ b/frontend/src/utils/queries.jsx @@ -0,0 +1,11 @@ +// Use endpoint /api/status to check if the user is logged in +export async function hasTokenCookie() { + return fetch("/api/status") + .then((response) => { + if (response.ok) { + return true; + } + return false; + }) + .catch(() => false); +} diff --git a/frontend/src/utils/serializer.jsx b/frontend/src/utils/serializer.jsx index 396f4fb..6861da3 100644 --- a/frontend/src/utils/serializer.jsx +++ b/frontend/src/utils/serializer.jsx @@ -3,6 +3,9 @@ import { initialBuildState } from "../pages/build/builder"; export const serializeBuildState = (state) => { // Create minimal representation of state const compressedState = { + // Build name + n: state.name, + // Monster ID only m: state.monster, @@ -47,6 +50,11 @@ export const deserializeBuildState = ( // Create a deep clone of the base state const state = JSON.parse(JSON.stringify(baseState)); + // Update name if present in decoded data + if (decoded.n) { + state.name = decoded.n; + } + // Update monster if present in decoded data if (decoded.m) { state.monster = decoded.m;