Auto stash before merge of "main" and "origin/main"

This commit is contained in:
boragenel 2025-01-23 11:10:27 +03:00
parent 6791e36401
commit 74bc8014e9
3 changed files with 454 additions and 7 deletions

View file

@ -1,9 +1,446 @@
"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() {
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([]);
// 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 (
<div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
<p>Game creation coming soon</p>
<div className="static flex items-top mt-20 justify-center top-0 left-0">
<Form
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>
<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,7 +17,8 @@ import { toast } from "react-toastify";
import sanitizeHtml from "sanitize-html";
import Select, { MultiValue, StylesConfig } from "react-select";
import { useTheme } from "next-themes";
import { UserType } from "@/types/UserType";
import Timers from "@/components/timers";
import Streams from "@/components/streams";
export default function CreatePostPage() {
const [title, setTitle] = useState("");
@ -168,7 +169,7 @@ export default function CreatePostPage() {
};
return (
<div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
<div className="absolute flex items-top mt-40 justify-center top-0 left-0 w-screen h-screen">
<Form
className="w-full max-w-2xl flex flex-col gap-4"
validationErrors={errors}
@ -305,6 +306,10 @@ export default function CreatePostPage() {
</Button>
</div>
</Form>
</div>
<div class="flex flex-col gap-4 px-8 items-end">
<Timers />
<Streams />
</div>
</div>
);
}

View file

@ -40,11 +40,12 @@ import { useTheme } from "next-themes";
type EditorProps = {
content: string;
setContent: (content: string) => void;
gameEditor: boolean;
};
const limit = 32767;
export default function Editor({ content, setContent }: EditorProps) {
export default function Editor({ content, setContent,gameEditor }: EditorProps) {
const { theme } = useTheme();
const editor = useEditor({
@ -97,7 +98,11 @@ export default function Editor({ content, setContent }: EditorProps) {
editorProps: {
attributes: {
class:
"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",
"prose dark:prose-invert " +
(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",
},
},
});