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; +}