From 1280002a5296dd2a6e722f6ac3fc48f4c620c589 Mon Sep 17 00:00:00 2001
From: Ategon <benjamin@barbeau.net>
Date: Thu, 16 Jan 2025 16:35:49 -0500
Subject: [PATCH] Add settings and jam joining

---
 src/app/create-game/page.tsx    |   9 ++
 src/app/settings/page.tsx       | 106 ++++++++++++++++++++++++
 src/components/navbar/index.tsx | 142 +++++++++++++++++++++++++++-----
 src/helpers/jam.ts              |  63 ++++++++++++++
 src/types/JamType.ts            |   9 ++
 5 files changed, 309 insertions(+), 20 deletions(-)
 create mode 100644 src/app/create-game/page.tsx
 create mode 100644 src/app/settings/page.tsx
 create mode 100644 src/helpers/jam.ts
 create mode 100644 src/types/JamType.ts

diff --git a/src/app/create-game/page.tsx b/src/app/create-game/page.tsx
new file mode 100644
index 0000000..30344eb
--- /dev/null
+++ b/src/app/create-game/page.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+export default function CreateGamePage() {
+  return (
+    <div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
+      <p>Game creation coming soon</p>
+    </div>
+  );
+}
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
new file mode 100644
index 0000000..58f4a08
--- /dev/null
+++ b/src/app/settings/page.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+import { getCookie, hasCookie } from "@/helpers/cookie";
+import { UserType } from "@/types/UserType";
+import { Button, Form, Input } from "@nextui-org/react";
+import { redirect, usePathname } from "next/navigation";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+
+export default function UserPage() {
+  const [user, setUser] = useState<UserType>();
+  const [profilePicture, setProfilePicture] = useState("");
+  const [errors] = useState({});
+  const pathname = usePathname();
+
+  useEffect(() => {
+    loadUser();
+    async function loadUser() {
+      if (!hasCookie("token")) {
+        setUser(undefined);
+        redirect("/");
+        return;
+      }
+
+      const response = await fetch(
+        process.env.NEXT_PUBLIC_MODE === "PROD"
+          ? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
+          : `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
+        {
+          headers: { authorization: `Bearer ${getCookie("token")}` },
+        }
+      );
+
+      if (response.status == 200) {
+        const data = await response.json();
+        setUser(data);
+
+        setProfilePicture(data.profilePicture ?? "");
+      } else {
+        setUser(undefined);
+      }
+    }
+  }, [pathname]);
+
+  return (
+    <div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
+      {!user ? (
+        "Loading settings..."
+      ) : (
+        <Form
+          className="w-full max-w-xs flex flex-col gap-4"
+          validationErrors={errors}
+          onReset={() => {
+            setProfilePicture(user.profilePicture ?? "");
+          }}
+          onSubmit={async (e) => {
+            e.preventDefault();
+
+            const response = await fetch(
+              process.env.NEXT_PUBLIC_MODE === "PROD"
+                ? "https://d2jam.com/api/v1/user"
+                : "http://localhost:3005/api/v1/user",
+              {
+                body: JSON.stringify({
+                  slug: user.slug,
+                  profilePicture: profilePicture,
+                }),
+                method: "PUT",
+                headers: {
+                  "Content-Type": "application/json",
+                  authorization: `Bearer ${getCookie("token")}`,
+                },
+              }
+            );
+
+            if (response.ok) {
+              toast.success("Changed settings");
+              setUser(await response.json());
+            } else {
+              toast.error("Failed to update settings");
+            }
+          }}
+        >
+          <Input
+            label="Profile Picture"
+            labelPlacement="outside"
+            name="profilePicture"
+            placeholder="Enter a url to an image"
+            type="text"
+            value={profilePicture}
+            onValueChange={setProfilePicture}
+          />
+
+          <div className="flex gap-2">
+            <Button color="primary" type="submit">
+              Save
+            </Button>
+            <Button type="reset" variant="flat">
+              Reset
+            </Button>
+          </div>
+        </Form>
+      )}
+    </div>
+  );
+}
diff --git a/src/components/navbar/index.tsx b/src/components/navbar/index.tsx
index ee12809..a39f8f7 100644
--- a/src/components/navbar/index.tsx
+++ b/src/components/navbar/index.tsx
@@ -16,24 +16,38 @@ import {
   DropdownMenu,
   DropdownTrigger,
   Image,
-  Spacer,
   Tooltip,
 } from "@nextui-org/react";
 import { SiDiscord, SiForgejo, SiGithub } from "@icons-pack/react-simple-icons";
-import { LogInIcon, Menu, NotebookPen, SquarePen } from "lucide-react";
+import {
+  CalendarPlus,
+  Gamepad2,
+  LogInIcon,
+  Menu,
+  NotebookPen,
+  SquarePen,
+} from "lucide-react";
 import { useEffect, useState } from "react";
 import { hasCookie, getCookie } from "@/helpers/cookie";
 import { usePathname } from "next/navigation";
 import { UserType } from "@/types/UserType";
+import { getCurrentJam, joinJam } from "@/helpers/jam";
+import { toast } from "react-toastify";
+import { JamType } from "@/types/JamType";
 
 export default function Navbar() {
   const [user, setUser] = useState<UserType>();
   const pathname = usePathname();
   const [isMobile, setIsMobile] = useState(false);
+  const [jam, setJam] = useState<JamType | null>();
+  const [isInJam, setIsInJam] = useState<boolean>();
 
   useEffect(() => {
     loadUser();
     async function loadUser() {
+      const currentJam = await getCurrentJam();
+      setJam(currentJam);
+
       if (!hasCookie("token")) {
         setUser(undefined);
         return;
@@ -48,8 +62,19 @@ export default function Navbar() {
         }
       );
 
+      const user = await response.json();
+
+      if (
+        currentJam &&
+        user.jams.filter((jam: JamType) => jam.id == currentJam.id).length > 0
+      ) {
+        setIsInJam(true);
+      } else {
+        setIsInJam(false);
+      }
+
       if (response.status == 200) {
-        setUser(await response.json());
+        setUser(user);
       } else {
         setUser(undefined);
       }
@@ -87,9 +112,52 @@ export default function Navbar() {
                 <Avatar src={user.profilePicture} />
               </DropdownTrigger>
               <DropdownMenu className="text-black">
+                {jam && isInJam ? (
+                  <DropdownItem key="create-game" href="/create-game">
+                    Create Game
+                  </DropdownItem>
+                ) : null}
+                {jam && !isInJam ? (
+                  <DropdownItem
+                    key="join-event"
+                    onPress={async () => {
+                      try {
+                        const currentJam = await getCurrentJam();
+
+                        if (!currentJam) {
+                          toast.error("There is no jam to join");
+                          return;
+                        }
+
+                        if (await joinJam(currentJam.id)) {
+                          setIsInJam(true);
+                        }
+                      } catch (error) {
+                        console.error("Error during join process:", error);
+                      }
+                    }}
+                  >
+                    Join Event
+                  </DropdownItem>
+                ) : null}
                 <DropdownItem key="create-post" href="/create-post">
                   Create Post
                 </DropdownItem>
+                <DropdownItem
+                  key="profile"
+                  className="text-black"
+                  href={`/u/${user.slug}`}
+                >
+                  Profile
+                </DropdownItem>
+                <DropdownItem
+                  showDivider
+                  key="settings"
+                  className="text-black"
+                  href="/settings"
+                >
+                  Settings
+                </DropdownItem>
                 <DropdownItem
                   key="github"
                   href="https://github.com/Ategon/Jamjar"
@@ -148,8 +216,43 @@ export default function Navbar() {
           )
         ) : (
           <div className="flex gap-3 items-center">
-            {user && (
+            {user && jam && isInJam && (
               <NavbarItem>
+                <Link href="/create-game">
+                  <Button
+                    endContent={<Gamepad2 />}
+                    className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
+                    variant="bordered"
+                  >
+                    Create Game
+                  </Button>
+                </Link>
+              </NavbarItem>
+            )}
+            {user && jam && !isInJam && (
+              <NavbarItem>
+                <Button
+                  endContent={<CalendarPlus />}
+                  className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
+                  variant="bordered"
+                  onPress={async () => {
+                    const currentJam = await getCurrentJam();
+
+                    if (!currentJam) {
+                      toast.error("There is no jam to join");
+                      return;
+                    }
+                    if (await joinJam(currentJam.id)) {
+                      setIsInJam(true);
+                    }
+                  }}
+                >
+                  Join Jam
+                </Button>
+              </NavbarItem>
+            )}
+            {user && (
+              <NavbarItem className="flex items-center">
                 <Link href="/create-post">
                   <Button
                     endContent={<SquarePen />}
@@ -159,7 +262,6 @@ export default function Navbar() {
                     Create Post
                   </Button>
                 </Link>
-                <Spacer x={32} />
               </NavbarItem>
             )}
             <NavbarItem>
@@ -241,21 +343,21 @@ export default function Navbar() {
                   <Avatar src={user.profilePicture} />
                 </DropdownTrigger>
                 <DropdownMenu>
-                  {/* <DropdownItem
-                key="profile"
-                className="text-black"
-                href="/profile"
-              >
-                Profile
-              </DropdownItem>
-              <DropdownItem
-                showDivider
-                key="settings"
-                className="text-black"
-                href="/settings"
-              >
-                Settings
-              </DropdownItem> */}
+                  <DropdownItem
+                    key="profile"
+                    className="text-black"
+                    href={`/u/${user.slug}`}
+                  >
+                    Profile
+                  </DropdownItem>
+                  <DropdownItem
+                    showDivider
+                    key="settings"
+                    className="text-black"
+                    href="/settings"
+                  >
+                    Settings
+                  </DropdownItem>
                   <DropdownItem
                     key="logout"
                     color="danger"
diff --git a/src/helpers/jam.ts b/src/helpers/jam.ts
new file mode 100644
index 0000000..c37746c
--- /dev/null
+++ b/src/helpers/jam.ts
@@ -0,0 +1,63 @@
+import { JamType } from "@/types/JamType";
+import { getCookie } from "./cookie";
+import { toast } from "react-toastify";
+
+export async function getJams(): Promise<JamType[]> {
+  const response = await fetch(
+    process.env.NEXT_PUBLIC_MODE === "PROD"
+      ? "https://d2jam.com/api/v1/jams"
+      : "http://localhost:3005/api/v1/jams"
+  );
+
+  return response.json();
+}
+
+export async function getCurrentJam(): Promise<JamType | null> {
+  const jams = await getJams();
+  const now = new Date();
+
+  // Get only jams that happen in the future
+  const futureJams = jams.filter((jam) => new Date(jam.startTime) > now);
+
+  // If theres no jams happening returns null
+  if (futureJams.length === 0) {
+    return null;
+  }
+
+  // Sort future jams by startTime (earliest first)
+  futureJams.sort(
+    (a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
+  );
+
+  return futureJams[0];
+}
+
+export async function joinJam(jamId: number) {
+  const response = await fetch(
+    process.env.NEXT_PUBLIC_MODE === "PROD"
+      ? "https://d2jam.com/api/v1/join-jam"
+      : "http://localhost:3005/api/v1/join-jam",
+    {
+      body: JSON.stringify({
+        jamId: jamId,
+        userSlug: getCookie("user"),
+      }),
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        authorization: `Bearer ${getCookie("token")}`,
+      },
+    }
+  );
+
+  if (response.status == 401) {
+    toast.error("You have already joined the jam");
+    return false;
+  } else if (response.ok) {
+    toast.success("Joined jam");
+    return true;
+  } else {
+    toast.error("Error while trying to join jam");
+    return false;
+  }
+}
diff --git a/src/types/JamType.ts b/src/types/JamType.ts
new file mode 100644
index 0000000..524de5b
--- /dev/null
+++ b/src/types/JamType.ts
@@ -0,0 +1,9 @@
+export interface JamType {
+  id: number;
+  name: string;
+  ratingHours: number;
+  slaughterHours: number;
+  startTime: Date;
+  createdAt: Date;
+  updatedAt: Date;
+}