Compare commits

...

7 commits

Author SHA1 Message Date
Ategon
e908237d84 Merge branch 'main' of https://github.com/Edikoyo-Jam/Jamjar 2025-01-16 16:36:20 -05:00
Ategon
1280002a52 Add settings and jam joining 2025-01-16 16:35:49 -05:00
cef03390d4 updated github link to point https://github.com/Dare2Jam/ 2025-01-16 23:24:24 +03:00
Benjamin Barbeau
8002647ec7
Update README.md 2025-01-16 14:56:08 -05:00
Benjamin Barbeau
c74f207d1a
Update README.md 2025-01-16 14:55:47 -05:00
Benjamin Barbeau
9cc4f99e3d
Update README.md 2025-01-16 14:54:39 -05:00
Benjamin Barbeau
c1693a3166
Update README.md 2025-01-16 14:50:50 -05:00
6 changed files with 340 additions and 29 deletions

View file

@ -2,15 +2,37 @@
Frontend for a game jam site
To run using next.js (for development)
## Things used
```
npm i
npm run dev
```
- Typescript (language)
- Next.js (web framework)
- Tailwind (css framework)
- Lucide (icons)
- Eslint (static code analysis)
- Framer motion (animations)
- React Toastify (toasts)
To run using docker and docker compose
## Running for development
Prerequisites:
- node.js or equivalent
To start up the site locally for development you need to:
1. Go to a spot you want to be the parent folder for where the folder for Jamjar goes (e.g. navigate to it in terminal)
2. Clone the repository aka get a local copy of the files (e.g. by running `git clone https://github.com/Dare2Jam/Jamjar.git`)
3. Go into the folder you just cloned in (e.g. using `cd Jamjar`)
4. Install dependencies needed for the site (e.g. `npm i`)
5. Create a `.env` file in the folder which is used for environment variables. In this you would set NEXT_PUBLIC_MODE to either PROD or DEV depending on what backend data you want to load in (dev loads it from a locally running jamcore, PROD loads it from the production site)
```
docker compose up --build -d
NEXT_PUBLIC_MODE=DEV
```
6. Run the site using `npm run dev` which will start up a dev server that will hot reload as you make changes (most of the time)
7. Go to https://localhost:3000 (or another port if it says it started up the site on a different port)
## Running using docker
Prerequisites:
- docker
If you want to start up the frontend using docker instead of what is above (either for development or for a production site) you can run `docker compose up --build -d` to build the image and then run it in the background. This will need to be done after any changes you make to rebuild the image

View file

@ -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>
);
}

106
src/app/settings/page.tsx Normal file
View file

@ -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>
);
}

View file

@ -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>
@ -173,7 +275,7 @@ export default function Navbar() {
}
>
<Link
href="https://github.com/Ategon/Jamjar"
href="https://github.com/Dare2Jam/"
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-red-100"
isExternal
>
@ -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"
@ -345,7 +447,7 @@ export default function Navbar() {
}
>
<Link
href="https://github.com/Ategon/Jamjar"
href="https://github.com/Dare2Jam/"
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-red-100"
isExternal
>

63
src/helpers/jam.ts Normal file
View file

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

9
src/types/JamType.ts Normal file
View file

@ -0,0 +1,9 @@
export interface JamType {
id: number;
name: string;
ratingHours: number;
slaughterHours: number;
startTime: Date;
createdAt: Date;
updatedAt: Date;
}