Compare commits

..

No commits in common. "2a89f75ac6733b7e22d5d78dd98226df3620b72b" and "3ca54b39a1404e0461b8aee5310ea3d4218f4393" have entirely different histories.

13 changed files with 430 additions and 809 deletions

View file

@ -1,94 +1,72 @@
"use client"; "use client";
import Editor from "@/components/editor"; import Editor from "@/components/editor";
import { getCookie } from "@/helpers/cookie"; import { getCookie, hasCookie } from "@/helpers/cookie";
import { Button, Form, Input, Spacer } from "@nextui-org/react"; import { Avatar, Button, Form, Input, Spacer } from "@nextui-org/react";
import { LoaderCircle } from "lucide-react"; import { LoaderCircle } from "lucide-react";
import { useEffect, useState } from "react"; import { redirect } from "next/navigation";
import { ReactNode, useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import { Select, SelectItem } from "@nextui-org/react"; import Select, { MultiValue, StylesConfig } from "react-select";
import { useTheme } from "next-themes";
import Timers from "@/components/timers"; import Timers from "@/components/timers";
import Streams from "@/components/streams"; import Streams from "@/components/streams";
import { UserType } from "@/types/UserType";
import { useRouter } from 'next/navigation';
import { GameType } from "@/types/GameType";
import { PlatformType, DownloadLinkType } from "@/types/DownloadLinkType";
export default function CreateGamePage() { export default function CreateGamePage() {
const router = useRouter();
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [waitingPost, setWaitingPost] = useState(false); const [waitingPost, setWaitingPost] = useState(false);
const [editGame, setEditGame] = useState(false);
/*
const [selectedTags, setSelectedTags] = useState<MultiValue<{ const [selectedTags, setSelectedTags] = useState<MultiValue<{
value: string; value: string;
label: ReactNode; label: ReactNode;
isFixed: boolean; isFixed: boolean;
}> | null>(null); }> | null>(null);
*/
const [mounted, setMounted] = useState<boolean>(false); const [mounted, setMounted] = useState<boolean>(false);
const [options, setOptions] = useState<
{
value: string;
label: ReactNode;
id: number;
isFixed: boolean;
}[]
>();
const { theme } = useTheme();
// Add these state variables after existing useState declarations
const [windowsLink, setWindowsLink] = useState("");
const [linuxLink, setLinuxLink] = useState("");
const [macLink, setMacLink] = useState("");
const [webGLLink, setWebGLLink] = useState("");
const [gameSlug, setGameSlug] = useState("");
const [thumbnailUrl, setThumbnailUrl] = useState("");
const [authorSearch, setAuthorSearch] = useState("");
const [selectedAuthors, setSelectedAuthors] = useState<Array<{id: number, name: string}>>([]);
const [searchResults, setSearchResults] = useState<Array<{ id: number; name: string }>>([]);
const [gameSlug, setGameSlug] = useState(""); // Add this function to handle author search
const [prevSlug, setPrevGameSlug] = useState(""); const handleAuthorSearch = async (query: string) => {
const [game, setGame] = useState<GameType>(); if (query.length < 2) return;
const [thumbnailUrl, setThumbnailUrl] = useState("");
const [authorSearch, setAuthorSearch] = useState(""); const response = await fetch(
const [selectedAuthors, setSelectedAuthors] = useState<Array<UserType>>([]); process.env.NEXT_PUBLIC_MODE === "PROD"
const [searchResults, setSearchResults] = useState<Array<UserType>>([]); ? `https://d2jam.com/api/v1/users/search?q=${query}`
const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false); : `http://localhost:3005/api/v1/users/search?q=${query}`,
const [user, setUser] = useState<UserType>(); {
const [downloadLinks, setDownloadLinks] = useState<DownloadLinkType[]>([]); headers: { authorization: `Bearer ${getCookie("token")}` },
const [editorKey, setEditorKey] = useState(0); credentials: "include",
const [isMobile, setIsMobile] = useState<boolean>(false);
const urlRegex = /^(https?:\/\/)/;
const sanitizeSlug = (value: string): string => {
return value
.toLowerCase() // Convert to lowercase
.replace(/\s+/g, '-') // Replace whitespace with hyphens
.replace(/[^a-z0-9-]/g, '') // Only allow lowercase letters, numbers, and hyphens
.substring(0, 50); // Limit length to 50 characters
};
const handleAuthorSearch = async (query: string) => {
if (query.length < 3) return;
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/user/search?q=${query}`
: `http://localhost:3005/api/v1/user/search?q=${query}`,
{
headers: { authorization: `Bearer ${getCookie("token")}` },
credentials: "include",
}
);
if (response.ok) {
const data = await response.json();
setSearchResults(data);
} }
}; );
useEffect(() => { if (response.ok) {
const handleResize = () => { const data = await response.json();
setIsMobile(window.innerWidth <= 768); setSearchResults(data);
}; }
handleResize(); };
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
const load = async () => { const load = async () => {
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
@ -99,10 +77,9 @@ export default function CreateGamePage() {
credentials: "include", credentials: "include",
} }
); );
const localuser = await response.json();
setUser(localuser); const user = await response.json();
/*
const tagResponse = await fetch( const tagResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/tags` ? `https://d2jam.com/api/v1/tags`
@ -118,7 +95,7 @@ export default function CreateGamePage() {
}[] = []; }[] = [];
for (const tag of await tagResponse.json()) { for (const tag of await tagResponse.json()) {
if (tag.modOnly && localuser && !localuser.mod) { if (tag.modOnly && !user.mod) {
continue; continue;
} }
newoptions.push({ newoptions.push({
@ -147,129 +124,136 @@ export default function CreateGamePage() {
setOptions(newoptions); setOptions(newoptions);
setSelectedTags(newoptions.filter((tag) => tag.isFixed)); setSelectedTags(newoptions.filter((tag) => tag.isFixed));
} }
*/
}; };
load(); load();
},[]); }, []);
useEffect(() => { const styles: StylesConfig<
const checkExistingGame = async () => { {
value: string;
const response = await fetch( label: ReactNode;
process.env.NEXT_PUBLIC_MODE === "PROD" isFixed: boolean;
? `https://d2jam.com/api/v1/self/current-game?username=${getCookie("user")}` },
: `http://localhost:3005/api/v1/self/current-game?username=${getCookie("user")}`, true
{ > = {
headers: { authorization: `Bearer ${getCookie("token")}` }, multiValue: (base, state) => {
credentials: "include", return {
} ...base,
); backgroundColor: state.data.isFixed
console.log("say"); ? theme == "dark"
if (response.ok) { ? "#222"
const gameData = await response.json(); : "#ddd"
if (gameData) { : theme == "dark"
setEditGame(true); ? "#444"
setTitle(gameData.name); : "#eee",
setGameSlug(gameData.slug); };
setPrevGameSlug(gameData.slug); },
setContent(gameData.description); multiValueLabel: (base, state) => {
setEditorKey((prev) => prev + 1); return {
setThumbnailUrl(gameData.thumbnail); ...base,
setDownloadLinks(gameData.downloadLinks); color: state.data.isFixed
setGame(gameData); ? theme == "dark"
const uniqueAuthors = [gameData.author, ...gameData.contributors] ? "#ddd"
.filter((author, index, self) => : "#222"
index === self.findIndex((a) => a.id === author.id) : theme == "dark"
); ? "#fff"
setSelectedAuthors(uniqueAuthors); : "#444",
fontWeight: state.data.isFixed ? "normal" : "bold",
} paddingRight: state.data.isFixed ? "8px" : "2px",
else };
{ },
setSelectedAuthors(user ? [user] : []); multiValueRemove: (base, state) => {
} return {
} ...base,
else display: state.data.isFixed ? "none" : "flex",
{ color: theme == "dark" ? "#ddd" : "#222",
};
setEditGame(false); },
setTitle(""); control: (styles) => ({
setGameSlug(""); ...styles,
setContent(""); backgroundColor: theme == "dark" ? "#181818" : "#fff",
setEditorKey((prev) => prev + 1); minWidth: "300px",
setThumbnailUrl(""); }),
setDownloadLinks([]); menu: (styles) => ({
...styles,
} backgroundColor: theme == "dark" ? "#181818" : "#fff",
}; color: theme == "dark" ? "#fff" : "#444",
}),
if (mounted && user) { option: (styles, { isFocused }) => ({
checkExistingGame(); ...styles,
} backgroundColor: isFocused
? theme == "dark"
},[user,mounted]); ? "#333"
: "#ddd"
: undefined,
}),
};
return ( return (
<div className="static flex items-top mt-20 justify-center top-0 left-0"> <div className="static flex items-top mt-20 justify-center top-0 left-0">
<Form
<Form className="w-full max-w-2xl flex flex-col gap-4"
className="w-full max-w-2xl flex flex-col gap-4" validationErrors={errors}
validationErrors={errors} onSubmit={async (e) => {
onSubmit={async (e) => { e.preventDefault();
e.preventDefault();
if (!title && !content) { if (!title && !content) {
setErrors({ setErrors({
title: "Please enter a valid title", title: "Please enter a valid title",
content: "Please enter valid content", content: "Please enter valid content",
}); });
toast.error("Please enter valid content"); toast.error("Please enter valid content");
return; return;
} }
if (!title) { if (!title) {
setErrors({ title: "Please enter a valid title" }); setErrors({ title: "Please enter a valid title" });
return; return;
} }
if (!content) { if (!content) {
setErrors({ content: "Please enter valid content" }); setErrors({ content: "Please enter valid content" });
toast.error("Please enter valid content"); toast.error("Please enter valid content");
return; return;
} }
const userSlug = getCookie("user"); // Retrieve user slug from cookies if (!hasCookie("token")) {
if (!userSlug) { setErrors({ content: "You are not logged in" });
toast.error("You are not logged in."); return;
return; }
}
const sanitizedHtml = sanitizeHtml(content); const sanitizedHtml = sanitizeHtml(content);
setWaitingPost(true); setWaitingPost(true);
try { const tags = [];
const requestMethod = editGame ? "PUT" : "POST";
const endpoint = editGame ? `/games/${prevSlug}` : "/games/create"; if (selectedTags) {
for (const tag of selectedTags) {
tags.push(
options?.filter((option) => option.value == tag.value)[0].id
);
}
}
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1${endpoint}` ? "https://d2jam.com/api/v1/post"
: `http://localhost:3005/api/v1${endpoint}`, : "http://localhost:3005/api/v1/post",
{ {
body: JSON.stringify({ body: JSON.stringify({
name: title, title: title,
slug: gameSlug, content: sanitizedHtml,
description: sanitizedHtml, username: getCookie("user"),
thumbnail: thumbnailUrl, tags,
downloadLinks: downloadLinks.map((link) => ({ gameSlug,
url: link.url, thumbnailUrl,
platform: link.platform, windowsLink,
})), linuxLink,
userSlug, macLink,
contributors: selectedAuthors.map((author) => author.id), webGLLink,
authors: selectedAuthors.map(author => author.id)
}), }),
method: requestMethod, method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
authorization: `Bearer ${getCookie("token")}`, authorization: `Bearer ${getCookie("token")}`,
@ -278,59 +262,41 @@ export default function CreateGamePage() {
} }
); );
if (response.status === 401) { if (response.status == 401) {
setErrors({ content: "Invalid user" }); setErrors({ content: "Invalid user" });
setWaitingPost(false); setWaitingPost(false);
return; return;
} }
if (response.ok) { if (response.ok) {
toast.success(gameSlug ? "Game updated successfully!" : "Game created successfully!"); toast.success("Successfully created post");
setWaitingPost(false); setWaitingPost(false);
router.push(`/games/${gameSlug || sanitizeSlug(title)}`); redirect("/");
} else { } else {
const error = await response.text(); toast.error("An error occured");
toast.error(error || "Failed to create game");
setWaitingPost(false); setWaitingPost(false);
} }
} catch (error) { }}
console.error("Error creating game:", error); >
toast.error("Failed to create game.");
}
}}
>
<div>
<h1 className="text-2xl font-bold mb-4 flex">
{gameSlug ? "Edit Game" : "Create New Game"}
</h1>
</div>
<Input <Input
isRequired isRequired
label="Game Name" label="Game Name"
labelPlacement="outside" labelPlacement="outside"
name="title" name="title"
placeholder="Enter your game name" placeholder="Enter your game name"
type="text" type="text"
value={title} value={title}
onValueChange={(value) => { onValueChange={setTitle}
setTitle(value); />
if (!isSlugManuallyEdited) {
setGameSlug(sanitizeSlug(value));
}
}}
/>
<Input <Input
label="Game Slug" label="Game Slug"
labelPlacement="outside" labelPlacement="outside"
placeholder="your-game-name" placeholder="your-game-name"
value={gameSlug} value={gameSlug}
onValueChange={(value) => { onValueChange={setGameSlug}
setGameSlug(sanitizeSlug(value)); description="This will be used in the URL: d2jam.com/games/your-game-name"
setIsSlugManuallyEdited(true); />
}}
description="This will be used in the URL: d2jam.com/games/your-game-name"
/>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-sm font-medium">Add Authors</label> <label className="text-sm font-medium">Add Authors</label>
@ -349,9 +315,7 @@ export default function CreateGamePage() {
key={user.id} key={user.id}
className="flex justify-between items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer" className="flex justify-between items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer"
onClick={() => { onClick={() => {
if (!selectedAuthors.some(a => a.id === user.id)) { setSelectedAuthors([...selectedAuthors, user]);
setSelectedAuthors([...selectedAuthors, user]);
}
setSearchResults([]); setSearchResults([]);
setAuthorSearch(""); setAuthorSearch("");
}} }}
@ -362,155 +326,121 @@ export default function CreateGamePage() {
</div> </div>
)} )}
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
{selectedAuthors.map((author) => ( {selectedAuthors.map((author) => (
<div <div
key={author.id} key={author.id}
className="bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-100 px-3 py-1 rounded-full flex items-center gap-2" className="bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-100 px-3 py-1 rounded-full flex items-center gap-2"
>
<span>{author.name}</span>
{((game && author.id !== game.authorId) || (!game && author.id !== user?.id)) && (
<button
onClick={() => setSelectedAuthors(selectedAuthors.filter(a => a.id !== author.id))}
className="text-sm hover:text-red-500"
> >
× <span>{author.name}</span>
</button> <button
)} onClick={() => setSelectedAuthors(selectedAuthors.filter(a => a.id !== author.id))}
className="text-sm hover:text-red-500"
>
×
</button>
</div>
))}
</div> </div>
))}
</div>
</div> </div>
<label className="text-sm font-medium">Game Description</label> <label className="text-sm font-medium">Game Description</label>
<Editor key={editorKey} content={content} setContent={setContent} gameEditor /> <Editor content={content} setContent={setContent} gameEditor />
<Spacer />
{mounted && (
<Select
styles={styles}
isMulti
value={selectedTags}
onChange={(value) => setSelectedTags(value)}
options={options}
isClearable={false}
isOptionDisabled={() =>
selectedTags != null && selectedTags.length >= 5
}
/>
)}
<Spacer /> <Spacer />
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Input
label="Thumbnail URL"
labelPlacement="outside"
placeholder="https://example.com/thumbnail.png"
value={thumbnailUrl}
onValueChange={setThumbnailUrl}
/>
<div className="grid grid-cols-2 gap-4">
<Input <Input
label="Thumbnail URL" label="Windows Download"
labelPlacement="outside" placeholder="https://example.com/game-windows.zip"
placeholder="https://example.com/thumbnail.png" value={windowsLink}
value={thumbnailUrl} onValueChange={setWindowsLink}
onValueChange={setThumbnailUrl}
/> />
<Input
label="Linux Download"
placeholder="https://example.com/game-linux.zip"
value={linuxLink}
onValueChange={setLinuxLink}
/>
<Input
label="Mac Download"
placeholder="https://example.com/game-mac.zip"
value={macLink}
onValueChange={setMacLink}
/>
<Input
label="WebGL Link"
placeholder="https://example.com/game-webgl"
value={webGLLink}
onValueChange={setWebGLLink}
/>
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
{Array.isArray(downloadLinks) &&
downloadLinks.map((link, index) => (
<div key={link.id} className="flex gap-2">
<Input
className="flex-grow"
placeholder="https://example.com/download"
value={link.url}
onValueChange={(value) => {
const newLinks = [...downloadLinks];
newLinks[index].url = value;
setDownloadLinks(newLinks);
}}
onBlur={() => {
if (!urlRegex.test(downloadLinks[index].url)) {
toast.error("Please enter a valid URL starting with http:// or https://");
if (!downloadLinks[index].url.startsWith("http://") && !downloadLinks[index].url.startsWith("https://")) {
const newUrl = "https://" + downloadLinks[index].url;
const newLinks = [...downloadLinks];
newLinks[index].url = newUrl;
setDownloadLinks(newLinks);
const input = document.querySelector<HTMLInputElement>(
`#download-link-${index}`
);
if (input) {
input.value = newUrl;
}
}
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
} <h3 className="text-lg font-bold mb-4">Game Metrics</h3>
}} <div className="grid grid-cols-2 gap-4">
/> <div className="bg-white dark:bg-gray-900 p-3 rounded-lg">
<Select <p className="text-sm text-gray-600 dark:text-gray-400">Views</p>
className="w-96" <p className="text-2xl font-bold">0</p>
defaultSelectedKeys={["Windows"]} </div>
aria-label="Select platform" // Add this to fix accessibility warning <div className="bg-white dark:bg-gray-900 p-3 rounded-lg">
onSelectionChange={(value) => { <p className="text-sm text-gray-600 dark:text-gray-400">Downloads</p>
const newLinks = [...downloadLinks]; <p className="text-2xl font-bold">0</p>
newLinks[index].platform = value as unknown as PlatformType; </div>
setDownloadLinks(newLinks); <div className="bg-white dark:bg-gray-900 p-3 rounded-lg">
}} <p className="text-sm text-gray-600 dark:text-gray-400">Rating</p>
> <p className="text-2xl font-bold">N/A</p>
<SelectItem key="Windows" value="Windows"> </div>
Windows <div className="bg-white dark:bg-gray-900 p-3 rounded-lg">
</SelectItem> <p className="text-sm text-gray-600 dark:text-gray-400">Comments</p>
<SelectItem key="MacOS" value="MacOS"> <p className="text-2xl font-bold">0</p>
MacOS
</SelectItem>
<SelectItem key="Linux" value="Linux">
Linux
</SelectItem>
<SelectItem key="Web" value="Web">
Web
</SelectItem>
<SelectItem key="Mobile" value="Mobile">
Mobile
</SelectItem>
<SelectItem key="Other" value="Other">
Other
</SelectItem>
</Select>
<Button
color="danger"
variant="light"
onPress={() => {
setDownloadLinks(downloadLinks.filter((l) => l.id !== link.id));
}}
>
×
</Button>
</div>
))}
</div> </div>
<Button
color="primary"
variant="solid"
onPress={() => {
setDownloadLinks([
...downloadLinks,
{
id: Date.now(),
url: "",
platform: "Windows",
},
]);
}}
>
Add Download Link
</Button>
</div> </div>
</div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button color="primary" type="submit"> <Button color="primary" type="submit">
{waitingPost ? ( {waitingPost ? (
<LoaderCircle className="animate-spin" size={16} /> <LoaderCircle className="animate-spin" size={16} />
) : ( ) : (
<p>{editGame ? "Update" : "Create"}</p> <p>Create</p>
)} )}
</Button> </Button>
</div> </div>
</div> </div>
</Form> </Form>
{!isMobile && ( <div className="flex flex-col gap-4 px-8 items-end">
<div className="flex flex-col gap-4 px-8 items-end">
<Timers /> <Timers />
<Streams /> <Streams />
</div> </div>
)}
</div> </div>
); );
} }

View file

@ -43,7 +43,6 @@ export default function CreatePostPage() {
const { theme } = useTheme(); const { theme } = useTheme();
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<UserType>();
const [sticky, setSticky] = useState(false); const [sticky, setSticky] = useState(false);
const [isMobile, setIsMobile] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
@ -109,16 +108,6 @@ export default function CreatePostPage() {
load(); load();
}, []); }, []);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const styles: StylesConfig< const styles: StylesConfig<
{ {
value: string; value: string;
@ -318,12 +307,10 @@ export default function CreatePostPage() {
</Button> </Button>
</div> </div>
</Form> </Form>
{!isMobile && ( <div className="flex flex-col gap-4 px-8 items-end">
<div className="flex flex-col gap-4 px-8 items-end">
<Timers /> <Timers />
<Streams /> <Streams />
</div> </div>
)}
</div> </div>
); );
} }

View file

@ -1,163 +0,0 @@
"use client";
import { use } from 'react';
import { useState, useEffect } from 'react';
import { getCookie } from '@/helpers/cookie';
import Link from 'next/link';
import { Button } from '@nextui-org/react';
import { useRouter } from 'next/navigation';
import { GameType } from '@/types/GameType';
import { UserType } from '@/types/UserType';
import { DownloadLinkType } from '@/types/DownloadLinkType';
export default function GamePage({ params }: { params: Promise<{ gameSlug: string }> }) {
const resolvedParams = use(params);
const gameSlug = resolvedParams.gameSlug;
const [game, setGame] = useState<GameType | null>(null);
const [user, setUser] = useState<UserType | null>(null);
const router = useRouter();
useEffect(() => {
const fetchGameAndUser = async () => {
// Fetch the game data
const gameResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/games/${gameSlug}`
: `http://localhost:3005/api/v1/games/${gameSlug}`,
{
headers: { authorization: `Bearer ${getCookie("token")}` },
credentials: "include",
}
);
if (gameResponse.ok) {
const gameData = await gameResponse.json();
const filteredContributors = gameData.contributors.filter(
(contributor: UserType) => contributor.id !== gameData.author.id
);
const updatedGameData = {
...gameData,
contributors: filteredContributors,
};
setGame(updatedGameData);
}
// Fetch the logged-in user data
if (getCookie("token")) {
const userResponse = 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")}` },
credentials: "include",
}
);
if (userResponse.ok) {
const userData = await userResponse.json();
setUser(userData);
}
}
};
fetchGameAndUser();
}, [gameSlug]);
if (!game) return <div>Loading...</div>;
// Check if the logged-in user is the creator or a contributor
const isEditable =
user &&
(user.id === game.author.id ||
game.contributors.some((contributor: UserType) => contributor.id === user.id));
return (
<div className="max-w-4xl mx-auto px-4 py-8">
{/* Game Name and Edit Button */}
<div className="flex items-center justify-between mb-4">
<h1 className="text-4xl font-bold">{game.name}</h1>
{isEditable && (
<Button
color="primary"
variant="solid"
onPress={() => router.push(`/create-game`)}
>
Edit
</Button>
)}
</div>
{/* Authors */}
<div className="mb-8">
<p className="text-gray-600">
Created by{' '}
<Link href={`/users/${game.author.slug}`} className="text-blue-500 hover:underline">
{game.author.name}
</Link>
{game.contributors.length > 0 && (
<>
{' '}with{' '}
{game.contributors.map((contributor: UserType, index: number) => (
<span key={contributor.id}>
<Link href={`/users/${contributor.slug}`} className="text-blue-500 hover:underline">
{contributor.name}
</Link>
{index < game.contributors.length - 1 ? ', ' : ''}
</span>
))}
</>
)}
</p>
</div>
<div className="mb-8">
<h2 className="text-2xl font-semibold mb-4">About</h2>
<div className="prose-neutral prose-lg" dangerouslySetInnerHTML={{ __html: game.description ?? '' }} />
</div>
<div className="mb-8">
<h2 className="text-2xl font-semibold mb-4">Downloads</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{game.downloadLinks.map((link: DownloadLinkType) => (
<Button
key={link.id}
as="a"
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="w-full"
color="primary"
variant="bordered"
>
Download for {link.platform}
</Button>
))}
</div>
</div>
{/* Game Metrics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">Views</p>
<p className="text-2xl font-bold">0</p>
</div>
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">Downloads</p>
<p className="text-2xl font-bold">0</p>
</div>
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">Rating</p>
<p className="text-2xl font-bold">N/A</p>
</div>
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
<p className="text-sm text-gray-600 dark:text-gray-400">Comments</p>
<p className="text-2xl font-bold">0</p>
</div>
</div>
</div>
);
}

View file

@ -14,9 +14,9 @@ export default function JamHeader() {
const fetchData = async () => { const fetchData = async () => {
const jamData = await getCurrentJam(); const jamData = await getCurrentJam();
setActiveJamResponse(jamData); setActiveJamResponse(jamData);
console.log(jamData);
// If we're in Jamming phase, fetch top themes and pick the first one // If we're in Jamming phase, fetch top themes and pick the first one
if ((jamData?.phase === "Jamming" || jamData?.phase === "Rating") && jamData.jam) { if (jamData?.phase === "Jamming" && jamData.jam) {
try { try {
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"

View file

@ -17,60 +17,57 @@ import { getCurrentJam } from "@/helpers/jam";
import { JamType } from "@/types/JamType"; import { JamType } from "@/types/JamType";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import MobileNavbarUser from "./MobileNavbarUser"; import MobileNavbarUser from "./MobileNavbarUser";
import ThemeToggle from "../theme-toggle";
export default function MobileNavbar() { export default function MobileNavbar() {
const pathname = usePathname(); const pathname = usePathname();
const [jam, setJam] = useState<JamType | null>(); const [jam, setJam] = useState<JamType | null>();
const [isInJam, setIsInJam] = useState<boolean>(); const [isInJam, setIsInJam] = useState<boolean>();
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<UserType>();
useEffect(() => { useEffect(() => {
loadUser(); loadUser();
async function loadUser() { async function loadUser() {
const jamResponse = await getCurrentJam(); const currentJamResponse = await getCurrentJam();
const currentJam = jamResponse?.jam; const currentJam = currentJamResponse?.jam;
setJam(currentJam); setJam(currentJam);
if (!hasCookie("token")) { if (!hasCookie("token")) {
setUser(undefined); setUser(undefined);
return; 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")}` },
credentials: "include",
}
);
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(user);
} else {
setUser(undefined);
}
} }
}, [pathname]);
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")}` },
credentials: "include",
}
);
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(user);
} else {
setUser(undefined);
}
}
}, [pathname]);
return ( return (
<NavbarBase maxWidth="2xl" className="bg-[#222] p-1" isBordered height={80}> <NavbarBase maxWidth="2xl" className="bg-[#222] p-1" isBordered height={80}>
{/* Left side navbar items */}
<NavbarContent justify="start" className="gap-10"> <NavbarContent justify="start" className="gap-10">
<NavbarBrand className="flex-grow-0"> <NavbarBrand className="flex-grow-0">
<Link <Link
@ -88,9 +85,9 @@ export default function MobileNavbar() {
</Link> </Link>
</NavbarBrand> </NavbarBrand>
</NavbarContent> </NavbarContent>
{/* Right side navbar items */}
<NavbarContent justify="end" className="gap-4"> <NavbarContent justify="end" className="gap-4">
<ThemeToggle />
{!user && ( {!user && (
<NavbarButtonLink icon={<LogInIcon />} name="Log In" href="/login" /> <NavbarButtonLink icon={<LogInIcon />} name="Log In" href="/login" />
)} )}

View file

@ -1,5 +1,3 @@
"use client";
import { import {
Avatar, Avatar,
Dropdown, Dropdown,
@ -9,9 +7,9 @@ import {
NavbarItem, NavbarItem,
} from "@nextui-org/react"; } from "@nextui-org/react";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import { JamType } from "@/types/JamType";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { getCurrentJam, joinJam } from "@/helpers/jam"; import { getCurrentJam, joinJam } from "@/helpers/jam";
import { JamType } from "@/types/JamType";
import { Dispatch, SetStateAction } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
interface NavbarUserProps { interface NavbarUserProps {
@ -21,27 +19,12 @@ interface NavbarUserProps {
isInJam?: boolean; isInJam?: boolean;
} }
export default function MobileNavbarUser({ export default async function MobileNavbarUser({
user, user,
jam, jam,
setIsInJam, setIsInJam,
isInJam, isInJam,
}: NavbarUserProps) { }: NavbarUserProps) {
const [currentJam, setCurrentJam] = useState<JamType | null>(null);
useEffect(() => {
const fetchCurrentJam = async () => {
try {
const response = await getCurrentJam();
setCurrentJam(response?.jam || null);
} catch (error) {
console.error("Error fetching current jam:", error);
}
};
fetchCurrentJam();
}, []);
return ( return (
<NavbarItem> <NavbarItem>
<Dropdown> <Dropdown>
@ -55,7 +38,7 @@ export default function MobileNavbarUser({
/> />
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu> <DropdownMenu>
{jam && currentJam && isInJam ? ( {jam && (await getCurrentJam())?.jam && isInJam ? (
<DropdownItem <DropdownItem
key="create-game" key="create-game"
href="/create-game" href="/create-game"
@ -64,18 +47,20 @@ export default function MobileNavbarUser({
Create Game Create Game
</DropdownItem> </DropdownItem>
) : null} ) : null}
{jam && currentJam && !isInJam ? ( {jam && (await getCurrentJam())?.jam && !isInJam ? (
<DropdownItem <DropdownItem
key="join-event" key="join-event"
className="text-black" className="text-black"
onPress={async () => { onPress={async () => {
try { try {
if (!currentJam) { const currentJam = await getCurrentJam();
if (!currentJam || !currentJam.jam) {
toast.error("There is no jam to join"); toast.error("There is no jam to join");
return; return;
} }
if (await joinJam(currentJam.id)) { if (await joinJam(currentJam.jam.id)) {
setIsInJam(true); setIsInJam(true);
} }
} catch (error) { } catch (error) {
@ -120,4 +105,4 @@ export default function MobileNavbarUser({
</Dropdown> </Dropdown>
</NavbarItem> </NavbarItem>
); );
} }

View file

@ -26,7 +26,6 @@ import { usePathname } from "next/navigation";
import { getCookie, hasCookie } from "@/helpers/cookie"; import { getCookie, hasCookie } from "@/helpers/cookie";
import { getCurrentJam, joinJam } from "@/helpers/jam"; import { getCurrentJam, joinJam } from "@/helpers/jam";
import { JamType } from "@/types/JamType"; import { JamType } from "@/types/JamType";
import { GameType } from "@/types/GameType";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import NavbarUser from "./PCNavbarUser"; import NavbarUser from "./PCNavbarUser";
import NavbarButtonAction from "./NavbarButtonAction"; import NavbarButtonAction from "./NavbarButtonAction";
@ -40,7 +39,6 @@ export default function PCNavbar() {
const [isInJam, setIsInJam] = useState<boolean>(); const [isInJam, setIsInJam] = useState<boolean>();
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<UserType>();
const [reduceMotion, setReduceMotion] = useState<boolean>(false); const [reduceMotion, setReduceMotion] = useState<boolean>(false);
const [hasGame, setHasGame] = useState<GameType | null>();
useEffect(() => { useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
@ -62,12 +60,12 @@ export default function PCNavbar() {
const jamResponse = await getCurrentJam(); const jamResponse = await getCurrentJam();
const currentJam = jamResponse?.jam; const currentJam = jamResponse?.jam;
setJam(currentJam); setJam(currentJam);
if (!hasCookie("token")) { if (!hasCookie("token")) {
setUser(undefined); setUser(undefined);
return; return;
} }
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}` ? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
@ -77,41 +75,9 @@ export default function PCNavbar() {
credentials: "include", credentials: "include",
} }
); );
const user = await response.json(); const user = await response.json();
// Check if user has a game in current jam
const gameResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/self/current-game?username=${getCookie("user")}`
: `http://localhost:3005/api/v1/self/current-game?username=${getCookie("user")}`,
{
headers: { authorization: `Bearer ${getCookie("token")}` },
credentials: "include",
}
);
if (gameResponse.ok) {
const gameData = await gameResponse.json();
console.log("Game Data:", gameData); // Log game data
console.log("User Data:", user); // Log user data
if (gameData) {
// Check if the logged-in user is either the creator or a contributor
const isContributor =
gameData.author?.id === user.id || // Check if logged-in user is the author
gameData.contributors?.some((contributor: UserType) => contributor.id === user.id); // Check if logged-in user is a contributor
console.log("Is Contributor:", isContributor); // Log whether the user is a contributor
if (isContributor) {
setHasGame(gameData); // Set the game data for "My Game"
} else {
setHasGame(null); // No game associated with this user
}
}
}
if ( if (
currentJam && currentJam &&
user.jams.filter((jam: JamType) => jam.id == currentJam.id).length > 0 user.jams.filter((jam: JamType) => jam.id == currentJam.id).length > 0
@ -120,7 +86,7 @@ export default function PCNavbar() {
} else { } else {
setIsInJam(false); setIsInJam(false);
} }
if (response.status == 200) { if (response.status == 200) {
setUser(user); setUser(user);
} else { } else {
@ -128,7 +94,6 @@ export default function PCNavbar() {
} }
} }
}, [pathname]); }, [pathname]);
return ( return (
<NavbarBase <NavbarBase
@ -161,15 +126,15 @@ export default function PCNavbar() {
<NavbarLink href="/games" name="Games" /> <NavbarLink href="/games" name="Games" />
</NavbarContent> </NavbarContent>
{/* Right side navbar items */}
<NavbarContent justify="end" className="gap-4"> <NavbarContent justify="end" className="gap-4">
<NavbarSearchbar /> <NavbarSearchbar />
{user && <Divider orientation="vertical" className="h-1/2" />} {user && <Divider orientation="vertical" className="h-1/2" />}
{user && jam && isInJam && ( {user && jam && isInJam && (
<NavbarButtonLink <NavbarButtonLink
icon={<Gamepad2 />} icon={<Gamepad2 />}
name={hasGame ? "My Game" : "Create Game"} name="Create Game"
href={hasGame ? "/games/"+hasGame.slug : "/create-game"} href="/create-game"
/> />
)} )}
{user && jam && !isInJam && ( {user && jam && !isInJam && (

View file

@ -20,7 +20,6 @@ export default function ThemeSlaughter() {
null null
); );
const [phaseLoading, setPhaseLoading] = useState(true); const [phaseLoading, setPhaseLoading] = useState(true);
const [themeLoading, setThemeLoading] = useState<{ [key: number]: boolean }>({});
// Fetch token on the client side // Fetch token on the client side
useEffect(() => { useEffect(() => {
@ -109,10 +108,8 @@ export default function ThemeSlaughter() {
// Handle voting // Handle voting
const handleVote = async (voteType: string) => { const handleVote = async (voteType: string) => {
if (!randomTheme) return; if (!randomTheme) return;
// Set loading for the current random theme setLoading(true);
setThemeLoading((prev) => ({ ...prev, [randomTheme.id]: true }));
try { try {
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
@ -131,7 +128,7 @@ export default function ThemeSlaughter() {
}), }),
} }
); );
if (response.ok) { if (response.ok) {
// Refresh data after voting // Refresh data after voting
fetchRandomTheme(); fetchRandomTheme();
@ -142,8 +139,7 @@ export default function ThemeSlaughter() {
} catch (error) { } catch (error) {
console.error("Error submitting vote:", error); console.error("Error submitting vote:", error);
} finally { } finally {
// Remove loading state for the current random theme setLoading(false);
setThemeLoading((prev) => ({ ...prev, [randomTheme.id]: false }));
} }
}; };
@ -233,78 +229,68 @@ export default function ThemeSlaughter() {
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
{/* Left Side */} {/* Left Side */}
<div className="w-1/2 p-6 bg-gray-100 dark:bg-gray-800 flex flex-col justify-start items-center"> <div className="w-1/2 p-6 bg-gray-100 dark:bg-gray-800 flex flex-col justify-start items-center">
{randomTheme ? ( {randomTheme ? (
<> <>
<h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4"> <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4">
{randomTheme.suggestion} {randomTheme.suggestion}
</h2> </h2>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
onClick={() => handleVote("YES")} onClick={() => handleVote("YES")}
className={`px-6 py-3 font-bold rounded-lg ${ className="px-6 py-3 bg-green-500 text-white font-bold rounded-lg hover:bg-green-600"
themeLoading[randomTheme?.id || -1] disabled={loading}
? "bg-gray-400 text-white cursor-not-allowed" >
: "bg-green-500 text-white hover:bg-green-600" YES
}`} </button>
disabled={themeLoading[randomTheme?.id || -1]} <button
> onClick={() => handleVote("NO")}
YES className="px-6 py-3 bg-red-500 text-white font-bold rounded-lg hover:bg-red-600"
</button> disabled={loading}
<button >
onClick={() => handleVote("NO")} NO
className={`px-6 py-3 font-bold rounded-lg ${ </button>
themeLoading[randomTheme?.id || -1] <button
? "bg-gray-400 text-white cursor-not-allowed" onClick={() => handleVote("SKIP")}
: "bg-red-500 text-white hover:bg-red-600" className="px-6 py-3 bg-gray-500 text-white font-bold rounded-lg hover:bg-gray-600"
}`} disabled={loading}
disabled={themeLoading[randomTheme?.id || -1]} >
> SKIP
NO </button>
</button> </div>
<button </>
onClick={() => handleVote("SKIP")} ) : (
className={`px-6 py-3 font-bold rounded-lg ${ <p className="text-gray-600 dark:text-gray-400">
themeLoading[randomTheme?.id || -1] No themes available.
? "bg-gray-400 text-white cursor-not-allowed" </p>
: "bg-gray-500 text-white hover:bg-gray-600" )}
}`} </div>
disabled={themeLoading[randomTheme?.id || -1]}
>
SKIP
</button>
</div>
</>
) : (
<p className="text-gray-600 dark:text-gray-400">No themes available.</p>
)}
</div>
{/* Right Side */} {/* Right Side */}
<div className="w-1/2 p-6 bg-white dark:bg-gray-900 overflow-y-auto"> <div className="w-1/2 p-6 bg-white dark:bg-gray-900 overflow-y-auto">
<h3 className="text-xl font-bold text-gray-800 dark:text-white mb-4"> <h3 className="text-xl font-bold text-gray-800 dark:text-white mb-4">
Your Votes Your Votes
</h3> </h3>
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
{votedThemes.map((theme) => ( {votedThemes.map((theme) => (
<div <div
key={theme.id} key={theme.id}
onClick={() => handleResetVote(theme.id)} onClick={() => handleResetVote(theme.id)}
className={`p-4 rounded-lg cursor-pointer ${ className={`p-4 rounded-lg cursor-pointer ${
theme.slaughterScore > 0 theme.slaughterScore > 0
? "bg-green-500 text-white" ? "bg-green-500 text-white"
: theme.slaughterScore < 0 : theme.slaughterScore < 0
? "bg-red-500 text-white" ? "bg-red-500 text-white"
: "bg-gray-300 text-black" : "bg-gray-300 text-black"
}`} }`}
> >
{theme.suggestion} {theme.suggestion}
</div> </div>
))} ))}
</div>
</div> </div>
</div> </div>
</div>
); );
return <></>; return <></>;
} }

View file

@ -96,12 +96,7 @@ export default function VotingPage() {
// Handle voting // Handle voting
const handleVote = async (themeId: number, votingScore: number) => { const handleVote = async (themeId: number, votingScore: number) => {
setThemes((prevThemes) => setLoading(true);
prevThemes.map((theme) =>
theme.id === themeId ? { ...theme, loading: true } : theme
)
);
try { try {
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
@ -117,30 +112,21 @@ export default function VotingPage() {
body: JSON.stringify({ suggestionId: themeId, votingScore }), body: JSON.stringify({ suggestionId: themeId, votingScore }),
} }
); );
if (response.ok) { if (response.ok) {
// Just update the local state instead of re-fetching all themes
setThemes((prevThemes) => setThemes((prevThemes) =>
prevThemes.map((theme) => prevThemes.map((theme) =>
theme.id === themeId theme.id === themeId ? { ...theme, votingScore } : theme
? { ...theme, votingScore, loading: false }
: theme
) )
); );
} else { } else {
console.error("Failed to submit vote."); console.error("Failed to submit vote.");
setThemes((prevThemes) =>
prevThemes.map((theme) =>
theme.id === themeId ? { ...theme, loading: false } : theme
)
);
} }
} catch (error) { } catch (error) {
console.error("Error submitting vote:", error); console.error("Error submitting vote:", error);
setThemes((prevThemes) => } finally {
prevThemes.map((theme) => setLoading(false);
theme.id === themeId ? { ...theme, loading: false } : theme
)
);
} }
}; };

View file

@ -23,55 +23,24 @@ export default function Timers() {
fetchCurrentJamPhase(); fetchCurrentJamPhase();
}, []); }, []);
if (activeJamResponse && activeJamResponse.jam) {
return (
if(activeJamResponse && activeJamResponse.jam) <div className="text-[#333] dark:text-white transition-color duration-250">
{ <Timer
const startTimeUTC = new Date(activeJamResponse.jam.startTime).toISOString(); name="Jam Start"
console.log(startTimeUTC); targetDate={new Date(activeJamResponse.jam.startTime)}
/>
if (activeJamResponse.phase == "Suggestion" || activeJamResponse.phase == "Survival" || activeJamResponse.phase == "Voting") { <Spacer y={8} />
return ( <p>Site under construction</p>
<div className="text-[#333] dark:text-white transition-color duration-250"> </div>
<Timer );
name="Jam starts in" } else {
targetDate={new Date(activeJamResponse.jam.startTime) } return (
/> <div className="text-[#333] dark:text-white transition-color duration-250">
<Spacer y={8} /> No upcoming jams
<p>Site under construction</p> <Spacer y={8} />
</div> <p>Site under construction</p>
); </div>
} else if (activeJamResponse.phase == "Jamming") { );
return (
<div className="text-[#333] dark:text-white transition-color duration-250">
<Timer
name="Jam ends in"
targetDate={ new Date(new Date(activeJamResponse.jam.startTime).getTime() + (activeJamResponse.jam.jammingHours * 60 * 60 * 1000))}
/>
<Spacer y={8} />
<p>Site under construction</p>
</div>
);
} else if (activeJamResponse.phase == "Rating") {
return (
<div className="text-[#333] dark:text-white transition-color duration-250">
<Timer
name="Rating ends in"
targetDate={new Date(new Date(activeJamResponse.jam.startTime).getTime() + (activeJamResponse.jam.jammingHours * 60 * 60 * 1000) + (activeJamResponse.jam.ratingHours * 60 * 60 * 1000))}
/>
<Spacer y={8} />
<p>Site under construction</p>
</div>
);
} else {
return (
<div className="text-[#333] dark:text-white transition-color duration-250">
No upcoming jams
<Spacer y={8} />
<p>Site under construction</p>
</div>
);
}
} }
} }

View file

@ -25,8 +25,10 @@ export async function getCurrentJam(): Promise<ActiveJamResponse | null> {
: "http://localhost:3005/api/v1/jams/active" : "http://localhost:3005/api/v1/jams/active"
); );
// Parse JSON response
const data = await response.json(); const data = await response.json();
// Return the phase and jam details
return { return {
phase: data.phase, phase: data.phase,
jam: data.futureJam, jam: data.futureJam,

View file

@ -1,6 +0,0 @@
export type PlatformType = "Windows" | "MacOS" | "Linux" | "Web" | "Mobile" | "Other";
export interface DownloadLinkType {
id: number;
url: string;
platform: PlatformType;
}

View file

@ -1,17 +0,0 @@
import { DownloadLinkType } from "./DownloadLinkType";
import { UserType } from "./UserType";
export interface GameType {
id: number;
slug: string,
name: string;
authorId: number;
author: UserType;
description?: string;
thumbnail?: string;
createdAt: Date;
updatedAt: Date;
downloadLinks: DownloadLinkType[];
contributors: UserType[];
}