Compare commits

..

No commits in common. "d8894cd0eb79caba2f829439d551b63d3107f6c1" and "6791e364018668df860bbb277ca8668cfd34851a" have entirely different histories.

10 changed files with 745 additions and 1217 deletions

View file

@ -1,446 +1,9 @@
"use client"; "use client";
import Editor from "@/components/editor";
import { getCookie, hasCookie } from "@/helpers/cookie";
import { Avatar, Button, Form, Input, Spacer } from "@nextui-org/react";
import { LoaderCircle } from "lucide-react";
import { redirect } from "next/navigation";
import { ReactNode, useEffect, useState } from "react";
import { toast } from "react-toastify";
import sanitizeHtml from "sanitize-html";
import Select, { MultiValue, StylesConfig } from "react-select";
import { useTheme } from "next-themes";
import Timers from "@/components/timers";
import Streams from "@/components/streams";
export default function CreateGamePage() { export default function CreateGamePage() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [errors, setErrors] = useState({});
const [waitingPost, setWaitingPost] = useState(false);
const [selectedTags, setSelectedTags] = useState<MultiValue<{
value: string;
label: ReactNode;
isFixed: boolean;
}> | null>(null);
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 }>>([]);
// Add this function to handle author search
const handleAuthorSearch = async (query: string) => {
if (query.length < 2) return;
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/users/search?q=${query}`
: `http://localhost:3005/api/v1/users/search?q=${query}`,
{
headers: { authorization: `Bearer ${getCookie("token")}` },
credentials: "include",
}
);
if (response.ok) {
const data = await response.json();
setSearchResults(data);
}
};
useEffect(() => {
setMounted(true);
const load = async () => {
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();
const tagResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/tags`
: `http://localhost:3005/api/v1/tags`
);
if (tagResponse.ok) {
const newoptions: {
value: string;
label: ReactNode;
id: number;
isFixed: boolean;
}[] = [];
for (const tag of await tagResponse.json()) {
if (tag.modOnly && !user.mod) {
continue;
}
newoptions.push({
value: tag.name,
id: tag.id,
label: (
<div className="flex gap-2 items-center">
{tag.icon && (
<Avatar
className="w-6 h-6 min-w-6 min-h-6"
size="sm"
src={tag.icon}
classNames={{ base: "bg-transparent" }}
/>
)}
<p>
{tag.name}
{tag.modOnly ? " (Mod Only)" : ""}
</p>
</div>
),
isFixed: tag.alwaysAdded,
});
}
setOptions(newoptions);
setSelectedTags(newoptions.filter((tag) => tag.isFixed));
}
};
load();
}, []);
const styles: StylesConfig<
{
value: string;
label: ReactNode;
isFixed: boolean;
},
true
> = {
multiValue: (base, state) => {
return {
...base,
backgroundColor: state.data.isFixed
? theme == "dark"
? "#222"
: "#ddd"
: theme == "dark"
? "#444"
: "#eee",
};
},
multiValueLabel: (base, state) => {
return {
...base,
color: state.data.isFixed
? theme == "dark"
? "#ddd"
: "#222"
: theme == "dark"
? "#fff"
: "#444",
fontWeight: state.data.isFixed ? "normal" : "bold",
paddingRight: state.data.isFixed ? "8px" : "2px",
};
},
multiValueRemove: (base, state) => {
return {
...base,
display: state.data.isFixed ? "none" : "flex",
color: theme == "dark" ? "#ddd" : "#222",
};
},
control: (styles) => ({
...styles,
backgroundColor: theme == "dark" ? "#181818" : "#fff",
minWidth: "300px",
}),
menu: (styles) => ({
...styles,
backgroundColor: theme == "dark" ? "#181818" : "#fff",
color: theme == "dark" ? "#fff" : "#444",
}),
option: (styles, { isFocused }) => ({
...styles,
backgroundColor: isFocused
? theme == "dark"
? "#333"
: "#ddd"
: undefined,
}),
};
return ( return (
<div className="static flex items-top mt-20 justify-center top-0 left-0"> <div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
<Form <p>Game creation coming soon</p>
className="w-full max-w-2xl flex flex-col gap-4"
validationErrors={errors}
onSubmit={async (e) => {
e.preventDefault();
if (!title && !content) {
setErrors({
title: "Please enter a valid title",
content: "Please enter valid content",
});
toast.error("Please enter valid content");
return;
}
if (!title) {
setErrors({ title: "Please enter a valid title" });
return;
}
if (!content) {
setErrors({ content: "Please enter valid content" });
toast.error("Please enter valid content");
return;
}
if (!hasCookie("token")) {
setErrors({ content: "You are not logged in" });
return;
}
const sanitizedHtml = sanitizeHtml(content);
setWaitingPost(true);
const tags = [];
if (selectedTags) {
for (const tag of selectedTags) {
tags.push(
options?.filter((option) => option.value == tag.value)[0].id
);
}
}
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/post"
: "http://localhost:3005/api/v1/post",
{
body: JSON.stringify({
title: title,
content: sanitizedHtml,
username: getCookie("user"),
tags,
gameSlug,
thumbnailUrl,
windowsLink,
linuxLink,
macLink,
webGLLink,
authors: selectedAuthors.map(author => author.id)
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie("token")}`,
},
credentials: "include",
}
);
if (response.status == 401) {
setErrors({ content: "Invalid user" });
setWaitingPost(false);
return;
}
if (response.ok) {
toast.success("Successfully created post");
setWaitingPost(false);
redirect("/");
} else {
toast.error("An error occured");
setWaitingPost(false);
}
}}
>
<Input
isRequired
label="Game Name"
labelPlacement="outside"
name="title"
placeholder="Enter your game name"
type="text"
value={title}
onValueChange={setTitle}
/>
<Input
label="Game Slug"
labelPlacement="outside"
placeholder="your-game-name"
value={gameSlug}
onValueChange={setGameSlug}
description="This will be used in the URL: d2jam.com/games/your-game-name"
/>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Add Authors</label>
<Input
placeholder="Search users..."
value={authorSearch}
onValueChange={(value) => {
setAuthorSearch(value);
handleAuthorSearch(value);
}}
/>
{searchResults.length > 0 && (
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-2">
{searchResults.map((user) => (
<div
key={user.id}
className="flex justify-between items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer"
onClick={() => {
setSelectedAuthors([...selectedAuthors, user]);
setSearchResults([]);
setAuthorSearch("");
}}
>
<span>{user.name}</span>
</div>
))}
</div>
)}
<div className="flex flex-wrap gap-2 mt-2">
{selectedAuthors.map((author) => (
<div
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"
>
<span>{author.name}</span>
<button
onClick={() => setSelectedAuthors(selectedAuthors.filter(a => a.id !== author.id))}
className="text-sm hover:text-red-500"
>
×
</button>
</div>
))}
</div> </div>
</div>
<label className="text-sm font-medium">Game Description</label>
<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 />
<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
label="Windows Download"
placeholder="https://example.com/game-windows.zip"
value={windowsLink}
onValueChange={setWindowsLink}
/>
<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="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">
<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-white dark:bg-gray-900 p-3 rounded-lg">
<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-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>
</div>
<div className="bg-white dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-400">Comments</p>
<p className="text-2xl font-bold">0</p>
</div>
</div>
</div>
<div className="flex gap-2">
<Button color="primary" type="submit">
{waitingPost ? (
<LoaderCircle className="animate-spin" size={16} />
) : (
<p>Create</p>
)}
</Button>
</div>
</div>
</Form>
<div className="flex flex-col gap-4 px-8 items-end">
<Timers />
<Streams />
</div>
</div>
); );
} }

View file

@ -17,8 +17,6 @@ import { toast } from "react-toastify";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import Select, { MultiValue, StylesConfig } from "react-select"; import Select, { MultiValue, StylesConfig } from "react-select";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import Timers from "@/components/timers";
import Streams from "@/components/streams";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
export default function CreatePostPage() { export default function CreatePostPage() {
@ -170,7 +168,7 @@ export default function CreatePostPage() {
}; };
return ( return (
<div className="absolute flex items-top mt-40 justify-center top-0 left-0 w-screen h-screen"> <div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
<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}
@ -307,10 +305,6 @@ export default function CreatePostPage() {
</Button> </Button>
</div> </div>
</Form> </Form>
<div className="flex flex-col gap-4 px-8 items-end"> </div>
<Timers />
<Streams />
</div>
</div>
); );
} }

View file

@ -40,12 +40,11 @@ import { useTheme } from "next-themes";
type EditorProps = { type EditorProps = {
content: string; content: string;
setContent: (content: string) => void; setContent: (content: string) => void;
gameEditor?: boolean;
}; };
const limit = 32767; const limit = 32767;
export default function Editor({ content, setContent,gameEditor }: EditorProps) { export default function Editor({ content, setContent }: EditorProps) {
const { theme } = useTheme(); const { theme } = useTheme();
const editor = useEditor({ const editor = useEditor({
@ -98,11 +97,7 @@ export default function Editor({ content, setContent,gameEditor }: EditorProps)
editorProps: { editorProps: {
attributes: { attributes: {
class: class:
"prose dark:prose-invert " + "prose dark:prose-invert min-h-[150px] max-h-[400px] overflow-y-auto cursor-text rounded-md border p-5 focus-within:outline-none focus-within:border-blue-500 !duration-250 !ease-linear !transition-all",
(gameEditor
? "min-h-[600px] max-h-[600px]"
: "min-h-[150px] max-h-[400px]") +
" overflow-y-auto cursor-text rounded-md border p-5 focus-within:outline-none focus-within:border-blue-500 !duration-250 !ease-linear !transition-all",
}, },
}, },
}); });

View file

@ -111,10 +111,11 @@ export default function JamHeader() {
</div> </div>
</div> </div>
{/* Phase-Specific Display */}
{activeJamResponse?.phase === "Suggestion" && ( {activeJamResponse?.phase === "Suggestion" && (
<div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x"> <div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x">
<a <a
href="/theme-suggestions" // href="/theme-suggestions"
className="text-blue-300 dark:text-blue-500 hover:underline font-semibold" className="text-blue-300 dark:text-blue-500 hover:underline font-semibold"
> >
Go to Theme Suggestion Go to Theme Suggestion
@ -125,7 +126,7 @@ export default function JamHeader() {
{activeJamResponse?.phase === "Survival" && ( {activeJamResponse?.phase === "Survival" && (
<div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x"> <div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x">
<a <a
href="/theme-slaughter" // href="/theme-slaughter"
className="text-blue-300 dark:text-blue-500 hover:underline font-semibold" className="text-blue-300 dark:text-blue-500 hover:underline font-semibold"
> >
Go to Theme Survival Go to Theme Survival
@ -136,7 +137,7 @@ export default function JamHeader() {
{activeJamResponse?.phase === "Voting" && ( {activeJamResponse?.phase === "Voting" && (
<div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x"> <div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x">
<a <a
href="/theme-voting" // href="/theme-voting"
className="text-blue-300 dark:text-blue-500 hover:underline font-semibold" className="text-blue-300 dark:text-blue-500 hover:underline font-semibold"
> >
Go to Theme Voting Go to Theme Voting

View file

@ -1,296 +1,286 @@
"use client"; "use client";
import React, { useState, useEffect, useCallback } from "react"; // import React, { useState, useEffect, useCallback } from "react";
import { getCookie } from "@/helpers/cookie"; // import { getCookie } from "@/helpers/cookie";
import { // import {
getCurrentJam, // getCurrentJam,
hasJoinedCurrentJam, // hasJoinedCurrentJam,
ActiveJamResponse, // ActiveJamResponse,
joinJam // } from "@/helpers/jam";
} from "@/helpers/jam";
import {ThemeType} from "@/types/ThemeType";
export default function ThemeSlaughter() { export default function ThemeSlaughter() {
const [randomTheme, setRandomTheme] = useState<ThemeType | null>(null); // const [randomTheme, setRandomTheme] = useState(null);
const [votedThemes, setVotedThemes] = useState<ThemeType[]>([]); // const [votedThemes, setVotedThemes] = useState([]);
const [loading, setLoading] = useState(false); // const [loading, setLoading] = useState(false);
const [token, setToken] = useState<string | null>(null); // const [token, setToken] = useState<string | null>(null);
const [hasJoined, setHasJoined] = useState<boolean>(false); // const [hasJoined, setHasJoined] = useState<boolean>(false);
const [activeJamResponse, setActiveJam] = useState<ActiveJamResponse | null>( // const [activeJamResponse, setActiveJam] = useState<ActiveJamResponse | null>(
null // null
); // );
const [phaseLoading, setPhaseLoading] = useState(true); // const [phaseLoading, setPhaseLoading] = useState(true);
// Fetch token on the client side // // Fetch token on the client side
useEffect(() => { // useEffect(() => {
const fetchedToken = getCookie("token"); // const fetchedToken = getCookie("token");
setToken(fetchedToken); // setToken(fetchedToken);
}, []); // }, []);
// Fetch the current jam phase using helpers/jam // // Fetch the current jam phase using helpers/jam
useEffect(() => { // useEffect(() => {
const fetchCurrentJamPhase = async () => { // const fetchCurrentJamPhase = async () => {
try { // try {
const activeJam = await getCurrentJam(); // const activeJam = await getCurrentJam();
setActiveJam(activeJam); // Set active jam details // setActiveJam(activeJam); // Set active jam details
} catch (error) { // } catch (error) {
console.error("Error fetching current jam:", error); // console.error("Error fetching current jam:", error);
} finally { // } finally {
setPhaseLoading(false); // Stop loading when phase is fetched // setPhaseLoading(false); // Stop loading when phase is fetched
} // }
}; // };
fetchCurrentJamPhase(); // fetchCurrentJamPhase();
}, []); // }, []);
// Fetch a random theme // // Fetch a random theme
const fetchRandomTheme = useCallback(async () => { // const fetchRandomTheme = useCallback(async () => {
if (!token) return; // Wait until token is available // if (!token) return; // Wait until token is available
if (!activeJamResponse) return; // if (!activeJamResponse) return;
if ( // if (
activeJamResponse && // activeJamResponse &&
activeJamResponse.jam && // activeJamResponse.jam &&
activeJamResponse.phase != "Survival" // activeJamResponse.phase != "Survival"
) { // ) {
return ( // return (
<div> // <div>
<h1>It&apos;s not Theme Survival phase.</h1> // <h1>It&apos;s not Theme Survival phase.</h1>
</div> // </div>
); // );
} // }
try { // try {
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/themes/random" // ? "https://d2jam.com/api/v1/themes/random"
: "http://localhost:3005/api/v1/themes/random", // : "http://localhost:3005/api/v1/themes/random",
{ // {
headers: { Authorization: `Bearer ${token}` }, // headers: { Authorization: `Bearer ${token}` },
credentials: "include", // credentials: "include",
} // }
); // );
if (response.ok) { // if (response.ok) {
const data = await response.json(); // const data = await response.json();
setRandomTheme(data); // setRandomTheme(data);
} else { // } else {
console.error("Failed to fetch random theme."); // console.error("Failed to fetch random theme.");
} // }
} catch (error) { // } catch (error) {
console.error("Error fetching random theme:", error); // console.error("Error fetching random theme:", error);
} // }
}, [activeJamResponse, token]); // }, [activeJamResponse, token]);
// Fetch voted themes // // Fetch voted themes
const fetchVotedThemes = useCallback(async () => { // const fetchVotedThemes = useCallback(async () => {
if (!token) return; // Wait until token is available // if (!token) return; // Wait until token is available
try { // try {
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/themes/voteSlaughter" // ? "https://d2jam.com/api/v1/themes/voteSlaughter"
: "http://localhost:3005/api/v1/themes/voteSlaughter", // : "http://localhost:3005/api/v1/themes/voteSlaughter",
{ // {
headers: { Authorization: `Bearer ${token}` }, // headers: { Authorization: `Bearer ${token}` },
credentials: "include", // credentials: "include",
} // }
); // );
if (response.ok) { // if (response.ok) {
const data = await response.json(); // const data = await response.json();
setVotedThemes(data); // setVotedThemes(data);
} else { // } else {
console.error("Failed to fetch voted themes."); // console.error("Failed to fetch voted themes.");
} // }
} catch (error) { // } catch (error) {
console.error("Error fetching voted themes:", error); // console.error("Error fetching voted themes:", error);
} // }
}, [token]); // }, [token]);
// Handle voting // // Handle voting
const handleVote = async (voteType: string) => { // const handleVote = async (voteType: string) => {
if (!randomTheme) return; // if (!randomTheme) return;
setLoading(true); // setLoading(true);
try { // try {
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/themes/voteSlaughter" // ? "https://d2jam.com/api/v1/themes/voteSlaughter"
: "http://localhost:3005/api/v1/themes/voteSlaughter", // : "http://localhost:3005/api/v1/themes/voteSlaughter",
{ // {
method: "POST", // method: "POST",
headers: { // headers: {
"Content-Type": "application/json", // "Content-Type": "application/json",
Authorization: `Bearer ${token}`, // Authorization: `Bearer ${token}`,
}, // },
credentials: "include", // credentials: "include",
body: JSON.stringify({ // body: JSON.stringify({
suggestionId: randomTheme.id, // suggestionId: randomTheme.id,
voteType, // voteType,
}), // }),
} // }
); // );
if (response.ok) { // if (response.ok) {
// Refresh data after voting // // Refresh data after voting
fetchRandomTheme(); // fetchRandomTheme();
fetchVotedThemes(); // fetchVotedThemes();
} else { // } else {
console.error("Failed to submit vote."); // console.error("Failed to submit vote.");
} // }
} catch (error) { // } catch (error) {
console.error("Error submitting vote:", error); // console.error("Error submitting vote:", error);
} finally { // } finally {
setLoading(false); // setLoading(false);
} // }
}; // };
// Handle resetting a vote from the grid // // Handle resetting a vote from the grid
const handleResetVote = async (themeId: number) => { // const handleResetVote = async (themeId: number) => {
try { // try {
const theme = votedThemes.find((theme) => theme.id === themeId); // setRandomTheme(votedThemes.find((theme) => theme.id === themeId));
if (theme) { // setVotedThemes((prev) =>
setRandomTheme(theme); // prev.map((theme) =>
setVotedThemes((prev) => // theme.id === themeId ? { ...theme, slaughterScore: 0 } : theme
prev.map((t) => // )
t.id === themeId ? { ...t, slaughterScore: 0 } : t // );
) // } catch (error) {
); // console.error("Error resetting vote:", error);
} // }
} catch (error) { // };
console.error("Error resetting vote:", error);
}
};
useEffect(() => { // useEffect(() => {
if (token && activeJamResponse?.phase === "Survival") { // if (token && activeJamResponse?.phase === "Survival") {
fetchRandomTheme(); // fetchRandomTheme();
fetchVotedThemes(); // fetchVotedThemes();
} // }
}, [token, activeJamResponse, fetchRandomTheme, fetchVotedThemes]); // }, [token, activeJamResponse, fetchRandomTheme, fetchVotedThemes]);
useEffect(() => { // useEffect(() => {
const init = async () => { // const init = async () => {
const joined = await hasJoinedCurrentJam(); // const joined = await hasJoinedCurrentJam();
setHasJoined(joined); // setHasJoined(joined);
setLoading(false); // setLoading(false);
}; // };
init(); // init();
}, []); // }, []);
if (phaseLoading || loading) { // if (phaseLoading || loading) {
return <div>Loading...</div>; // return <div>Loading...</div>;
} // }
// if (!hasJoined) {
// return (
// <div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen">
// <h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
// Join the Jam First
// </h1>
// <p className="text-gray-600 dark:text-gray-400">
// You need to join the current jam before you can join Theme Survival.
// </p>
// <button
// onClick={() => joinJam(activeJamResponse?.jam?.id)}
// className="mt-4 px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
// >
// Join Jam
// </button>
// </div>
// );
// }
// Render message if not in Theme Slaughter phase // // Render message if not in Theme Slaughter phase
if (activeJamResponse?.phase !== "Survival") { // if (activeJamResponse?.phase !== "Survival") {
return ( // return (
<div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen"> // <div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen">
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6"> // <h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
Not in Theme Slaughter Phase // Not in Theme Slaughter Phase
</h1> // </h1>
<p className="text-gray-600 dark:text-gray-400"> // <p className="text-gray-600 dark:text-gray-400">
The current phase is{" "} // The current phase is{" "}
<strong>{activeJamResponse?.phase || "Unknown"}</strong>. Please come // <strong>{activeJamResponse?.phase || "Unknown"}</strong>. Please come
back during the Theme Slaughter phase. // back during the Theme Slaughter phase.
</p> // </p>
</div> // </div>
); // );
} // }
const loggedIn = getCookie("token"); // const loggedIn = getCookie("token");
if (!loggedIn) {
return <div>Sign in to be able to join the Theme Survival</div>;
}
if (!hasJoined) { // if (!loggedIn) {
return ( // return <div>Sign in to be able to join the Theme Survival</div>;
<div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen"> // }
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
Join the Jam First
</h1>
<p className="text-gray-600 dark:text-gray-400">
You need to join the current jam before you can join Theme Survival.
</p>
<button
onClick={() => {
if (activeJamResponse?.jam?.id !== undefined) {
joinJam(activeJamResponse.jam.id);
}
}}
className="mt-4 px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
Join Jam
</button>
</div>
);
}
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 bg-green-500 text-white font-bold rounded-lg hover:bg-green-600" // className="px-6 py-3 bg-green-500 text-white font-bold rounded-lg hover:bg-green-600"
disabled={loading} // disabled={loading}
> // >
YES // YES
</button> // </button>
<button // <button
onClick={() => handleVote("NO")} // onClick={() => handleVote("NO")}
className="px-6 py-3 bg-red-500 text-white font-bold rounded-lg hover:bg-red-600" // className="px-6 py-3 bg-red-500 text-white font-bold rounded-lg hover:bg-red-600"
disabled={loading} // disabled={loading}
> // >
NO // NO
</button> // </button>
<button // <button
onClick={() => handleVote("SKIP")} // onClick={() => handleVote("SKIP")}
className="px-6 py-3 bg-gray-500 text-white font-bold rounded-lg hover:bg-gray-600" // className="px-6 py-3 bg-gray-500 text-white font-bold rounded-lg hover:bg-gray-600"
disabled={loading} // disabled={loading}
> // >
SKIP // SKIP
</button> // </button>
</div> // </div>
</> // </>
) : ( // ) : (
<p className="text-gray-600 dark:text-gray-400"> // <p className="text-gray-600 dark:text-gray-400">
No themes available. // No themes available.
</p> // </p>
)} // )}
</div> // </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

@ -1,287 +1,282 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; // import React, { useState, useEffect } from "react";
import { getCookie } from "@/helpers/cookie"; // import { getCookie } from "@/helpers/cookie";
import { // import {
getCurrentJam, // getCurrentJam,
hasJoinedCurrentJam, // hasJoinedCurrentJam,
ActiveJamResponse, // ActiveJamResponse,
} from "@/helpers/jam"; // } from "@/helpers/jam";
import { ThemeType } from "@/types/ThemeType";
import { joinJam } from "@/helpers/jam";
export default function ThemeSuggestions() { export default function ThemeSuggestions() {
const [suggestion, setSuggestion] = useState(""); // const [suggestion, setSuggestion] = useState("");
const [loading, setLoading] = useState(false); // const [loading, setLoading] = useState(false);
const [successMessage, setSuccessMessage] = useState(""); // const [successMessage, setSuccessMessage] = useState("");
const [errorMessage, setErrorMessage] = useState(""); // const [errorMessage, setErrorMessage] = useState("");
const [userSuggestions, setUserSuggestions] = useState<ThemeType[]>([]); // const [userSuggestions, setUserSuggestions] = useState([]);
const [themeLimit, setThemeLimit] = useState(0); // const [themeLimit, setThemeLimit] = useState(0);
const [hasJoined, setHasJoined] = useState<boolean>(false); // const [hasJoined, setHasJoined] = useState<boolean>(false);
const [activeJamResponse, setActiveJamResponse] = // const [activeJamResponse, setActiveJamResponse] =
useState<ActiveJamResponse | null>(null); // useState<ActiveJamResponse | null>(null);
const [phaseLoading, setPhaseLoading] = useState(true); // Loading state for fetching phase // const [phaseLoading, setPhaseLoading] = useState(true); // Loading state for fetching phase
// Fetch the current jam phase using helpers/jam // // Fetch the current jam phase using helpers/jam
useEffect(() => { // useEffect(() => {
const fetchCurrentJamPhase = async () => { // const fetchCurrentJamPhase = async () => {
try { // try {
const activeJam = await getCurrentJam(); // const activeJam = await getCurrentJam();
setActiveJamResponse(activeJam); // Set active jam details // setActiveJamResponse(activeJam); // Set active jam details
if (activeJam?.jam) { // if (activeJam?.jam) {
setThemeLimit(activeJam.jam.themePerUser || Infinity); // Set theme limit // setThemeLimit(activeJam.jam.themePerUser || Infinity); // Set theme limit
} // }
} catch (error) { // } catch (error) {
console.error("Error fetching current jam:", error); // console.error("Error fetching current jam:", error);
} finally { // } finally {
setPhaseLoading(false); // Stop loading when phase is fetched // setPhaseLoading(false); // Stop loading when phase is fetched
} // }
}; // };
fetchCurrentJamPhase(); // fetchCurrentJamPhase();
}, []); // }, []);
// Fetch all suggestions for the logged-in user // // Fetch all suggestions for the logged-in user
const fetchSuggestions = async () => { // const fetchSuggestions = async () => {
try { // try {
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/themes/suggestion" // ? "https://d2jam.com/api/v1/themes/suggestion"
: "http://localhost:3005/api/v1/themes/suggestion", // : "http://localhost:3005/api/v1/themes/suggestion",
{ // {
headers: { Authorization: `Bearer ${getCookie("token")}` }, // headers: { Authorization: `Bearer ${getCookie("token")}` },
credentials: "include", // credentials: "include",
} // }
); // );
if (response.ok) { // if (response.ok) {
const data = await response.json(); // const data = await response.json();
setUserSuggestions(data); // setUserSuggestions(data);
} // }
} catch (error) { // } catch (error) {
console.error("Error fetching suggestions:", error); // console.error("Error fetching suggestions:", error);
} // }
}; // };
// Fetch suggestions only when phase is "Suggestion" // // Fetch suggestions only when phase is "Suggestion"
useEffect(() => { // useEffect(() => {
if (activeJamResponse?.phase === "Suggestion") { // if (activeJamResponse?.phase === "Suggestion") {
fetchSuggestions(); // fetchSuggestions();
} // }
}, [activeJamResponse]); // }, [activeJamResponse]);
// Handle form submission to add a new suggestion // // Handle form submission to add a new suggestion
const handleSubmit = async (e: React.FormEvent) => { // const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); // e.preventDefault();
setLoading(true); // setLoading(true);
setSuccessMessage(""); // setSuccessMessage("");
setErrorMessage(""); // setErrorMessage("");
if (!suggestion.trim()) { // if (!suggestion.trim()) {
setErrorMessage("Suggestion cannot be empty."); // setErrorMessage("Suggestion cannot be empty.");
setLoading(false); // setLoading(false);
return; // return;
} // }
try { // try {
const token = getCookie("token"); // const token = getCookie("token");
if (!token) { // if (!token) {
throw new Error("User is not authenticated. Please log in."); // throw new Error("User is not authenticated. Please log in.");
} // }
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/themes/suggestion" // ? "https://d2jam.com/api/v1/themes/suggestion"
: "http://localhost:3005/api/v1/themes/suggestion", // : "http://localhost:3005/api/v1/themes/suggestion",
{ // {
method: "POST", // method: "POST",
headers: { // headers: {
"Content-Type": "application/json", // "Content-Type": "application/json",
Authorization: `Bearer ${token}`, // Authorization: `Bearer ${token}`,
}, // },
credentials: "include", // credentials: "include",
body: JSON.stringify({ suggestionText: suggestion }), // body: JSON.stringify({ suggestionText: suggestion }),
} // }
); // );
if (!response.ok) { // if (!response.ok) {
const errorData = await response.json(); // const errorData = await response.json();
throw new Error(errorData.error || "Failed to submit suggestion."); // throw new Error(errorData.error || "Failed to submit suggestion.");
} // }
setSuccessMessage("Suggestion added successfully!"); // setSuccessMessage("Suggestion added successfully!");
setSuggestion(""); // Clear input field // setSuggestion(""); // Clear input field
fetchSuggestions(); // Refresh suggestions list // fetchSuggestions(); // Refresh suggestions list
} catch (error) { // } catch (error) {
if (error instanceof Error) { // if (error instanceof Error) {
console.error("Error submitting suggestion:", error.message); // console.error("Error submitting suggestion:", error.message);
setErrorMessage(error.message || "An unexpected error occurred."); // setErrorMessage(error.message || "An unexpected error occurred.");
} else { // } else {
console.error("Unknown error:", error); // console.error("Unknown error:", error);
setErrorMessage("An unexpected error occurred."); // setErrorMessage("An unexpected error occurred.");
} // }
} finally { // } finally {
setLoading(false); // setLoading(false);
} // }
}; // };
// Handle deleting a suggestion // // Handle deleting a suggestion
const handleDelete = async (id: number) => { // const handleDelete = async (id: number) => {
try { // try {
const token = getCookie("token"); // const token = getCookie("token");
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/themes/suggestion/${id}` // ? `https://d2jam.com/api/v1/themes/suggestion/${id}`
: `http://localhost:3005/api/v1/themes/suggestion/${id}`, // : `http://localhost:3005/api/v1/themes/suggestion/${id}`,
{ // {
method: "DELETE", // method: "DELETE",
headers: { Authorization: `Bearer ${token}` }, // headers: { Authorization: `Bearer ${token}` },
credentials: "include", // credentials: "include",
} // }
); // );
if (!response.ok) { // if (!response.ok) {
throw new Error("Failed to delete suggestion."); // throw new Error("Failed to delete suggestion.");
} // }
fetchSuggestions(); // Refresh suggestions list // fetchSuggestions(); // Refresh suggestions list
} catch (error) { // } catch (error) {
console.error("Error deleting suggestion:", error); // console.error("Error deleting suggestion:", error);
} // }
}; // };
useEffect(() => { // useEffect(() => {
const init = async () => { // const init = async () => {
const joined = await hasJoinedCurrentJam(); // const joined = await hasJoinedCurrentJam();
setHasJoined(joined); // setHasJoined(joined);
setLoading(false); // setLoading(false);
}; // };
init(); // init();
}, []); // }, []);
// Render loading state while fetching phase // // Render loading state while fetching phase
if (phaseLoading || loading) { // if (phaseLoading || loading) {
return <div>Loading...</div>; // return <div>Loading...</div>;
} // }
const token = getCookie("token"); // if (!hasJoined) {
// return (
// <div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen">
// <h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
// Join the Jam First
// </h1>
// <p className="text-gray-600 dark:text-gray-400">
// You need to join the current jam before you can suggest themes.
// </p>
// <button
// onClick={() => joinJam(activeJamResponse?.jam?.id)}
// className="mt-4 px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
// >
// Join Jam
// </button>
// </div>
// );
// }
if (!token) { // const token = getCookie("token");
return <div>Sign in to be able to suggest themes</div>;
}
if (!hasJoined) { // if (!token) {
return ( // return <div>Sign in to be able to suggest themes</div>;
<div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen"> // }
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
Join the Jam First
</h1>
<p className="text-gray-600 dark:text-gray-400">
You need to join the current jam before you can suggest themes.
</p>
<button
onClick={() => {
if (activeJamResponse?.jam?.id !== undefined) {
joinJam(activeJamResponse.jam.id);
}
}}
className="mt-4 px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
Join Jam
</button>
</div>
);
}
// Render message if not in Suggestion phase // // Render message if not in Suggestion phase
if (activeJamResponse?.phase !== "Suggestion") { // if (activeJamResponse?.phase !== "Suggestion") {
return ( // return (
<div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen"> // <div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen">
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6"> // <h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
Not in Suggestion Phase // Not in Suggestion Phase
</h1> // </h1>
<p className="text-gray-600 dark:text-gray-400"> // <p className="text-gray-600 dark:text-gray-400">
The current phase is{" "} // The current phase is{" "}
<strong>{activeJamResponse?.phase || "Unknown"}</strong>. Please come // <strong>{activeJamResponse?.phase || "Unknown"}</strong>. Please come
back during the Suggestion phase. // back during the Suggestion phase.
</p> // </p>
</div> // </div>
); // );
} // }
return ( // return (
<div className="max-w-md mx-auto mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md"> // <div className="max-w-md mx-auto mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<h2 className="text-xl font-bold text-gray-800 dark:text-white mb-4"> // <h2 className="text-xl font-bold text-gray-800 dark:text-white mb-4">
Submit Your Theme Suggestion // Submit Your Theme Suggestion
</h2> // </h2>
{/* Hide form if user has reached their limit */} // {/* Hide form if user has reached their limit */}
{userSuggestions.length < themeLimit ? ( // {userSuggestions.length < themeLimit ? (
<form onSubmit={handleSubmit} className="flex flex-col gap-4"> // <form onSubmit={handleSubmit} className="flex flex-col gap-4">
<textarea // <textarea
className="w-full p-3 border rounded-lg focus:outline-none text-gray-600 focus:ring focus:ring-blue-300 dark:bg-gray-700 dark:text-white" // className="w-full p-3 border rounded-lg focus:outline-none text-gray-600 focus:ring focus:ring-blue-300 dark:bg-gray-700 dark:text-white"
placeholder="Enter your theme suggestion..." // placeholder="Enter your theme suggestion..."
value={suggestion} // value={suggestion}
onChange={(e) => { // onChange={(e) => {
if (e.target.value.length <= 32) { // if (e.target.value.length <= 32) {
setSuggestion(e.target.value); // setSuggestion(e.target.value);
} // }
}} // }}
rows={1} // rows={1}
maxLength={32} // maxLength={32}
></textarea> // ></textarea>
{errorMessage && ( // {errorMessage && (
<p className="text-red-500 text-sm">{errorMessage}</p> // <p className="text-red-500 text-sm">{errorMessage}</p>
)} // )}
{successMessage && ( // {successMessage && (
<p className="text-green-500 text-sm">{successMessage}</p> // <p className="text-green-500 text-sm">{successMessage}</p>
)} // )}
<button // <button
type="submit" // type="submit"
className={`w-full py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-600 focus:outline-none focus:ring focus:ring-blue-300 ${ // className={`w-full py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-600 focus:outline-none focus:ring focus:ring-blue-300 ${
loading ? "opacity-50 cursor-not-allowed" : "" // loading ? "opacity-50 cursor-not-allowed" : ""
}`} // }`}
disabled={loading} // disabled={loading}
> // >
{loading ? "Submitting..." : "Submit Suggestion"} // {loading ? "Submitting..." : "Submit Suggestion"}
</button> // </button>
</form> // </form>
) : ( // ) : (
<p className="text-yellow-500 text-sm"> // <p className="text-yellow-500 text-sm">
You&apos;ve reached your theme suggestion limit for this jam! // You&apos;ve reached your theme suggestion limit for this jam!
</p> // </p>
)} // )}
{/* List of user's suggestions */} // {/* List of user's suggestions */}
<div className="mt-6"> // <div className="mt-6">
<h3 className="text-lg font-semibold text-gray-800 dark:text-white mb-4"> // <h3 className="text-lg font-semibold text-gray-800 dark:text-white mb-4">
Your Suggestions // Your Suggestions
</h3> // </h3>
{userSuggestions.length > 0 ? ( // {userSuggestions.length > 0 ? (
<ul className="space-y-4"> // <ul className="space-y-4">
{userSuggestions.map((suggestion) => ( // {userSuggestions.map((suggestion) => (
<li // <li
key={suggestion.id} // key={suggestion.id}
className="flex justify-between items-center text-gray-400 bg-gray-100 dark:bg-gray-700 p-3 rounded-lg shadow-sm" // className="flex justify-between items-center text-gray-400 bg-gray-100 dark:bg-gray-700 p-3 rounded-lg shadow-sm"
> // >
<span>{suggestion.suggestion}</span> // <span>{suggestion.suggestion}</span>
<button // <button
onClick={() => handleDelete(suggestion.id)} // onClick={() => handleDelete(suggestion.id)}
className="text-red-500 hover:text-red-700 font-semibold" // className="text-red-500 hover:text-red-700 font-semibold"
> // >
Delete // Delete
</button> // </button>
</li> // </li>
))} // ))}
</ul> // </ul>
) : ( // ) : (
<p className="text-gray-600 dark:text-gray-400"> // <p className="text-gray-600 dark:text-gray-400">
You haven&apos;t submitted any suggestions yet. // You haven&apos;t submitted any suggestions yet.
</p> // </p>
)} // )}
</div> // </div>
</div> // </div>
); // );
return <></>;
} }

View file

@ -1,248 +1,245 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; // import React, { useState, useEffect } from "react";
import { getCookie } from "@/helpers/cookie"; // import { getCookie } from "@/helpers/cookie";
import { // import {
getCurrentJam, // getCurrentJam,
hasJoinedCurrentJam, // hasJoinedCurrentJam,
ActiveJamResponse, // ActiveJamResponse,
} from "@/helpers/jam"; // } from "@/helpers/jam";
import { ThemeType } from "@/types/ThemeType";
import { joinJam } from "@/helpers/jam";
interface VoteType {
themeSuggestionId: number;
votingScore: number;
}
export default function VotingPage() { export default function VotingPage() {
const [themes, setThemes] = useState<ThemeType[]>([]); return <></>;
const [loading, setLoading] = useState(false); // const [themes, setThemes] = useState([]);
const [activeJamResponse, setActiveJamResponse] = useState<ActiveJamResponse | null>(null); // const [loading, setLoading] = useState(false);
const [hasJoined, setHasJoined] = useState<boolean>(false); // const [activeJamResponse, setActiveJamResponse] =
const [phaseLoading, setPhaseLoading] = useState(true); // Loading state for fetching phase // useState<ActiveJamResponse | null>(null);
const token = getCookie("token"); // const [hasJoined, setHasJoined] = useState<boolean>(false);
// const [phaseLoading, setPhaseLoading] = useState(true); // Loading state for fetching phase
// const token = getCookie("token");
// Fetch the current jam phase using helpers/jam // // Fetch the current jam phase using helpers/jam
useEffect(() => { // useEffect(() => {
const fetchCurrentJamPhase = async () => { // const fetchCurrentJamPhase = async () => {
try { // try {
const activeJam = await getCurrentJam(); // const activeJam = await getCurrentJam();
setActiveJamResponse(activeJam); // Set active jam details // setActiveJamResponse(activeJam); // Set active jam details
} catch (error) { // } catch (error) {
console.error("Error fetching current jam:", error); // console.error("Error fetching current jam:", error);
} finally { // } finally {
setPhaseLoading(false); // Stop loading when phase is fetched // setPhaseLoading(false); // Stop loading when phase is fetched
} // }
}; // };
fetchCurrentJamPhase(); // fetchCurrentJamPhase();
}, []); // }, []);
// Fetch themes only when phase is "Voting" // // Fetch themes only when phase is "Voting"
useEffect(() => { // useEffect(() => {
// Fetch top N themes with voting scores // // Fetch top N themes with voting scores
async function fetchThemes() { // async function fetchThemes() {
if (!token || !activeJamResponse) return; // if (!token || !activeJamResponse) return;
try { // try {
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/themes/top-themes" // ? "https://d2jam.com/api/v1/themes/top-themes"
: "http://localhost:3005/api/v1/themes/top-themes", // : "http://localhost:3005/api/v1/themes/top-themes",
{ // {
headers: { Authorization: `Bearer ${token}` }, // headers: { Authorization: `Bearer ${token}` },
credentials: "include", // credentials: "include",
} // }
); // );
if (response.ok) { // if (response.ok) {
const themes = await response.json(); // const themes = await response.json();
// console.log("Fetched themes:", themes); // Debug log
// Fetch user's votes for these themes // // Fetch user's votes for these themes
const votesResponse = await fetch( // const votesResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" // process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/themes/votes" // ? "https://d2jam.com/api/v1/themes/votes"
: "http://localhost:3005/api/v1/themes/votes", // : "http://localhost:3005/api/v1/themes/votes",
{ // {
headers: { Authorization: `Bearer ${token}` }, // headers: { Authorization: `Bearer ${token}` },
credentials: "include", // credentials: "include",
} // }
); // );
if (votesResponse.ok) { // if (votesResponse.ok) {
const votes = await votesResponse.json(); // const votes = await votesResponse.json();
// Merge themes with user's votes // console.log("Fetched votes:", votes); // Debug log
const themesWithVotes = themes.map((theme: ThemeType) => {
const vote = votes.find((v: VoteType) => v.themeSuggestionId === theme.id);
return {
...theme,
votingScore: vote ? vote.votingScore : null,
};
});
setThemes(themesWithVotes);
}
} else {
console.error("Failed to fetch themes.");
}
} catch (error) {
console.error("Error fetching themes:", error);
}
}
if (activeJamResponse?.phase === "Voting") { // // Merge themes with user's votes
fetchThemes(); // const themesWithVotes = themes.map((theme) => {
} // const vote = votes.find((v) => v.themeSuggestionId === theme.id);
}, [activeJamResponse, token]); // console.log(`Theme ${theme.id} vote:`, vote); // Debug log
// return {
// ...theme,
// votingScore: vote ? vote.votingScore : null,
// };
// });
// Handle voting // console.log("Themes with votes:", themesWithVotes); // Debug log
const handleVote = async (themeId: number, votingScore: number) => { // setThemes(themesWithVotes);
setLoading(true); // }
try { // } else {
const response = await fetch( // console.error("Failed to fetch themes.");
process.env.NEXT_PUBLIC_MODE === "PROD" // }
? "https://d2jam.com/api/v1/themes/vote" // } catch (error) {
: "http://localhost:3005/api/v1/themes/vote", // console.error("Error fetching themes:", error);
{ // }
method: "POST", // }
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
credentials: "include",
body: JSON.stringify({ suggestionId: themeId, votingScore }),
}
);
if (response.ok) { // if (activeJamResponse?.phase === "Voting") {
// Just update the local state instead of re-fetching all themes // fetchThemes();
setThemes((prevThemes) => // }
prevThemes.map((theme) => // }, [activeJamResponse, token]);
theme.id === themeId ? { ...theme, votingScore } : theme
)
);
} else {
console.error("Failed to submit vote.");
}
} catch (error) {
console.error("Error submitting vote:", error);
} finally {
setLoading(false);
}
};
useEffect(() => { // // Handle voting
const init = async () => { // const handleVote = async (themeId, votingScore) => {
const joined = await hasJoinedCurrentJam(); // setLoading(true);
setHasJoined(joined); // try {
setLoading(false); // const response = await fetch(
}; // process.env.NEXT_PUBLIC_MODE === "PROD"
// ? "https://d2jam.com/api/v1/themes/vote"
// : "http://localhost:3005/api/v1/themes/vote",
// {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// Authorization: `Bearer ${token}`,
// },
// credentials: "include",
// body: JSON.stringify({ suggestionId: themeId, votingScore }),
// }
// );
init(); // if (response.ok) {
}, []); // // Just update the local state instead of re-fetching all themes
// setThemes((prevThemes) =>
// prevThemes.map((theme) =>
// theme.id === themeId ? { ...theme, votingScore } : theme
// )
// );
// } else {
// console.error("Failed to submit vote.");
// }
// } catch (error) {
// console.error("Error submitting vote:", error);
// } finally {
// setLoading(false);
// }
// };
if (phaseLoading || loading) { // useEffect(() => {
return <div>Loading...</div>; // const init = async () => {
} // const joined = await hasJoinedCurrentJam();
// setHasJoined(joined);
// setLoading(false);
// };
if (activeJamResponse?.phase !== "Voting") { // init();
return ( // }, []);
<div className="p-4 bg-gray-100 dark:bg-gray-800 min-h-screen">
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-4">
Not in Voting Phase
</h1>
<p className="text-gray-600 dark:text-gray-400">
The current phase is{" "}
<strong>{activeJamResponse?.phase || "Unknown"}</strong>. Please come
back during the Voting phase.
</p>
</div>
);
}
const loggedIn = getCookie("token"); // if (phaseLoading || loading) {
// return <div>Loading...</div>;
// }
if (!loggedIn) { // if (!hasJoined) {
return <div>Sign in to be able to vote</div>; // return (
} // <div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen">
// <h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
// Join the Jam First
// </h1>
// <p className="text-gray-600 dark:text-gray-400">
// You need to join the current jam before you can vote themes.
// </p>
// <button
// onClick={() => joinJam(activeJamResponse?.jam?.id)}
// className="mt-4 px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
// >
// Join Jam
// </button>
// </div>
// );
// }
if (!hasJoined) { // if (activeJamResponse?.phase !== "Voting") {
return ( // return (
<div className="p-6 bg-gray-100 dark:bg-gray-800 min-h-screen"> // <div className="p-4 bg-gray-100 dark:bg-gray-800 min-h-screen">
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-6"> // <h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-4">
Join the Jam First // Not in Voting Phase
</h1> // </h1>
<p className="text-gray-600 dark:text-gray-400"> // <p className="text-gray-600 dark:text-gray-400">
You need to join the current jam before you can vote themes. // The current phase is{" "}
</p> // <strong>{activeJamResponse?.phase || "Unknown"}</strong>. Please come
<button // back during the Voting phase.
onClick={() => { // </p>
if (activeJamResponse?.jam?.id !== undefined) { // </div>
joinJam(activeJamResponse.jam.id); // );
} // }
}}
className="mt-4 px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
Join Jam
</button>
</div>
);
}
return ( // const loggedIn = getCookie("token");
<div className="p-3 bg-gray-100 dark:bg-gray-800 min-h-screen">
<h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-4">
Voting Phase
</h1>
<div className="space-y-2">
{themes.map((theme) => (
<div
key={theme.id}
className="p-3 bg-white dark:bg-gray-900 rounded-lg shadow-md flex items-center"
>
{/* Voting Buttons */}
<div className="flex gap-1 mr-4">
<button
onClick={() => handleVote(theme.id, -1)}
className={`px-3 py-2 rounded-lg ${
theme.votingScore === -1
? "bg-red-500 text-white"
: "bg-gray-300 text-black hover:bg-red-500 hover:text-white"
}`}
disabled={loading}
>
-1
</button>
<button
onClick={() => handleVote(theme.id, 0)}
className={`px-3 py-2 rounded-lg ${
theme.votingScore === 0
? "bg-gray-500 text-white"
: "bg-gray-300 text-black hover:bg-gray-500 hover:text-white"
}`}
disabled={loading}
>
0
</button>
<button
onClick={() => handleVote(theme.id, +1)}
className={`px-3 py-2 rounded-lg ${
theme.votingScore === +1
? "bg-green-500 text-white"
: "bg-gray-300 text-black hover:bg-green-500 hover:text-white"
}`}
disabled={loading}
>
+1
</button>
</div>
{/* Theme Suggestion */} // if (!loggedIn) {
<div className="text-gray-800 dark:text-white"> // return <div>Sign in to be able to vote</div>;
{theme.suggestion} // }
</div>
</div> // return (
))} // <div className="p-3 bg-gray-100 dark:bg-gray-800 min-h-screen">
</div> // <h1 className="text-2xl font-bold text-gray-800 dark:text-white mb-4">
</div> // Voting Phase
); // </h1>
// <div className="space-y-2">
// {themes.map((theme) => (
// <div
// key={theme.id}
// className="p-3 bg-white dark:bg-gray-900 rounded-lg shadow-md flex items-center"
// >
// {/* Voting Buttons */}
// <div className="flex gap-1 mr-4">
// <button
// onClick={() => handleVote(theme.id, -1)}
// className={`px-3 py-2 rounded-lg ${
// theme.votingScore === -1
// ? "bg-red-500 text-white"
// : "bg-gray-300 text-black hover:bg-red-500 hover:text-white"
// }`}
// disabled={loading}
// >
// -1
// </button>
// <button
// onClick={() => handleVote(theme.id, 0)}
// className={`px-3 py-2 rounded-lg ${
// theme.votingScore === 0
// ? "bg-gray-500 text-white"
// : "bg-gray-300 text-black hover:bg-gray-500 hover:text-white"
// }`}
// disabled={loading}
// >
// 0
// </button>
// <button
// onClick={() => handleVote(theme.id, +1)}
// className={`px-3 py-2 rounded-lg ${
// theme.votingScore === +1
// ? "bg-green-500 text-white"
// : "bg-gray-300 text-black hover:bg-green-500 hover:text-white"
// }`}
// disabled={loading}
// >
// +1
// </button>
// </div>
// {/* Theme Suggestion */}
// <div className="text-gray-800 dark:text-white">
// {theme.suggestion}
// </div>
// </div>
// ))}
// </div>
// </div>
// );
} }

View file

@ -74,8 +74,8 @@ export async function hasJoinedCurrentJam(): Promise<boolean> {
try { try {
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/jams/participation" ? "https://d2jam.com/api/v1/participation"
: "http://localhost:3005/api/v1/jams/participation", : "http://localhost:3005/api/v1/participation",
{ {
credentials: "include", credentials: "include",
headers: { headers: {

View file

@ -9,5 +9,4 @@ export interface JamType {
startTime: Date; startTime: Date;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
themePerUser: number;
} }

View file

@ -1,6 +0,0 @@
export interface ThemeType {
id: number;
suggestion: string;
slaughterScore: number;
votingScore: number;
}