mirror of
https://github.com/Ategon/Jamjar.git
synced 2025-02-12 06:16:21 +00:00
Allow images in posts
This commit is contained in:
parent
333bb8b387
commit
21bb6b7188
9 changed files with 435 additions and 326 deletions
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -59,6 +59,7 @@
|
|||
"react-select": "^5.9.0",
|
||||
"react-toastify": "^11.0.3",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"tiptap-extension-resize-image": "^1.2.1",
|
||||
"tiptap-markdown": "^0.8.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -12654,6 +12655,17 @@
|
|||
"@popperjs/core": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiptap-extension-resize-image": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tiptap-extension-resize-image/-/tiptap-extension-resize-image-1.2.1.tgz",
|
||||
"integrity": "sha512-SLMAujDa+0LN/6Iv2HtU4Uk0BL6LMh4b/r85frpdnjFDW2i6pIOfTVG8jzJQ8T1EgYHNn2YG1U2HoVAGuwLc3Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.0.0",
|
||||
"@tiptap/extension-image": "^2.0.0",
|
||||
"@tiptap/pm": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiptap-markdown": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.8.10.tgz",
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
"react-select": "^5.9.0",
|
||||
"react-toastify": "^11.0.3",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"tiptap-extension-resize-image": "^1.2.1",
|
||||
"tiptap-markdown": "^0.8.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -2,25 +2,21 @@
|
|||
|
||||
import Editor from "@/components/editor";
|
||||
import { getCookie } from "@/helpers/cookie";
|
||||
import { Button, Form, Input, Spacer } from "@nextui-org/react";
|
||||
import { Button, Form, Input, Spacer } from "@nextui-org/react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { Select, SelectItem } from "@nextui-org/react";
|
||||
import Timers from "@/components/timers";
|
||||
import Streams from "@/components/streams";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from "next/navigation";
|
||||
import { GameType } from "@/types/GameType";
|
||||
import { PlatformType, DownloadLinkType } from "@/types/DownloadLinkType";
|
||||
|
||||
|
||||
|
||||
|
||||
import { sanitize } from "@/helpers/sanitize";
|
||||
|
||||
export default function CreateGamePage() {
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
|
@ -53,14 +49,14 @@ export default function CreateGamePage() {
|
|||
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
|
||||
.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}`
|
||||
|
@ -70,7 +66,7 @@ export default function CreateGamePage() {
|
|||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSearchResults(data);
|
||||
|
@ -88,7 +84,7 @@ export default function CreateGamePage() {
|
|||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
|
||||
const load = async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
|
@ -101,7 +97,7 @@ export default function CreateGamePage() {
|
|||
);
|
||||
const localuser = await response.json();
|
||||
setUser(localuser);
|
||||
|
||||
|
||||
/*
|
||||
const tagResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
|
@ -150,15 +146,18 @@ export default function CreateGamePage() {
|
|||
*/
|
||||
};
|
||||
load();
|
||||
},[]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const checkExistingGame = async () => {
|
||||
|
||||
const response = 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")}`,
|
||||
? `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",
|
||||
|
@ -177,340 +176,350 @@ export default function CreateGamePage() {
|
|||
setThumbnailUrl(gameData.thumbnail);
|
||||
setDownloadLinks(gameData.downloadLinks);
|
||||
setGame(gameData);
|
||||
const uniqueAuthors = [gameData.author, ...gameData.contributors]
|
||||
.filter((author, index, self) =>
|
||||
const uniqueAuthors = [
|
||||
gameData.author,
|
||||
...gameData.contributors,
|
||||
].filter(
|
||||
(author, index, self) =>
|
||||
index === self.findIndex((a) => a.id === author.id)
|
||||
);
|
||||
);
|
||||
setSelectedAuthors(uniqueAuthors);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
setSelectedAuthors(user ? [user] : []);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
} else {
|
||||
setEditGame(false);
|
||||
setTitle("");
|
||||
setGameSlug("");
|
||||
setContent("");
|
||||
setEditorKey((prev) => prev + 1);
|
||||
setEditorKey((prev) => prev + 1);
|
||||
setThumbnailUrl("");
|
||||
setDownloadLinks([]);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
if (mounted && user) {
|
||||
|
||||
if (mounted && user) {
|
||||
checkExistingGame();
|
||||
}
|
||||
|
||||
},[user,mounted]);
|
||||
|
||||
}, [user, mounted]);
|
||||
|
||||
return (
|
||||
<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();
|
||||
<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;
|
||||
}
|
||||
|
||||
const userSlug = getCookie("user"); // Retrieve user slug from cookies
|
||||
if (!userSlug) {
|
||||
toast.error("You are not logged in.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
try {
|
||||
const requestMethod = editGame ? "PUT" : "POST";
|
||||
const endpoint = editGame ? `/games/${prevSlug}` : "/games/create";
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1${endpoint}`
|
||||
: `http://localhost:3005/api/v1${endpoint}`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
name: title,
|
||||
slug: gameSlug,
|
||||
description: sanitizedHtml,
|
||||
thumbnail: thumbnailUrl,
|
||||
downloadLinks: downloadLinks.map((link) => ({
|
||||
url: link.url,
|
||||
platform: link.platform,
|
||||
})),
|
||||
userSlug,
|
||||
contributors: selectedAuthors.map((author) => author.id),
|
||||
}),
|
||||
method: requestMethod,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie("token")}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
setErrors({ content: "Invalid user" });
|
||||
setWaitingPost(false);
|
||||
if (!title && !content) {
|
||||
setErrors({
|
||||
title: "Please enter a valid title",
|
||||
content: "Please enter valid content",
|
||||
});
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(gameSlug ? "Game updated successfully!" : "Game created successfully!");
|
||||
setWaitingPost(false);
|
||||
router.push(`/games/${gameSlug || sanitizeSlug(title)}`);
|
||||
} else {
|
||||
const error = await response.text();
|
||||
toast.error(error || "Failed to create game");
|
||||
setWaitingPost(false);
|
||||
if (!title) {
|
||||
setErrors({ title: "Please enter a valid title" });
|
||||
return;
|
||||
}
|
||||
} 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
|
||||
isRequired
|
||||
label="Game Name"
|
||||
labelPlacement="outside"
|
||||
name="title"
|
||||
placeholder="Enter your game name"
|
||||
type="text"
|
||||
value={title}
|
||||
onValueChange={(value) => {
|
||||
setTitle(value);
|
||||
if (!isSlugManuallyEdited) {
|
||||
setGameSlug(sanitizeSlug(value));
|
||||
|
||||
if (!content) {
|
||||
setErrors({ content: "Please enter valid content" });
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Game Slug"
|
||||
labelPlacement="outside"
|
||||
placeholder="your-game-name"
|
||||
value={gameSlug}
|
||||
onValueChange={(value) => {
|
||||
setGameSlug(sanitizeSlug(value));
|
||||
setIsSlugManuallyEdited(true);
|
||||
}}
|
||||
description="This will be used in the URL: d2jam.com/games/your-game-name"
|
||||
/>
|
||||
const userSlug = getCookie("user"); // Retrieve user slug from cookies
|
||||
if (!userSlug) {
|
||||
toast.error("You are not logged in.");
|
||||
return;
|
||||
}
|
||||
|
||||
<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={() => {
|
||||
if (!selectedAuthors.some(a => a.id === user.id)) {
|
||||
setSelectedAuthors([...selectedAuthors, user]);
|
||||
const sanitizedHtml = sanitize(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
try {
|
||||
const requestMethod = editGame ? "PUT" : "POST";
|
||||
const endpoint = editGame ? `/games/${prevSlug}` : "/games/create";
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1${endpoint}`
|
||||
: `http://localhost:3005/api/v1${endpoint}`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
name: title,
|
||||
slug: gameSlug,
|
||||
description: sanitizedHtml,
|
||||
thumbnail: thumbnailUrl,
|
||||
downloadLinks: downloadLinks.map((link) => ({
|
||||
url: link.url,
|
||||
platform: link.platform,
|
||||
})),
|
||||
userSlug,
|
||||
contributors: selectedAuthors.map((author) => author.id),
|
||||
}),
|
||||
method: requestMethod,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie("token")}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
setSearchResults([]);
|
||||
setAuthorSearch("");
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
setErrors({ content: "Invalid user" });
|
||||
setWaitingPost(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(
|
||||
gameSlug
|
||||
? "Game updated successfully!"
|
||||
: "Game created successfully!"
|
||||
);
|
||||
setWaitingPost(false);
|
||||
router.push(`/games/${gameSlug || sanitizeSlug(title)}`);
|
||||
} else {
|
||||
const error = await response.text();
|
||||
toast.error(error || "Failed to create game");
|
||||
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
|
||||
isRequired
|
||||
label="Game Name"
|
||||
labelPlacement="outside"
|
||||
name="title"
|
||||
placeholder="Enter your game name"
|
||||
type="text"
|
||||
value={title}
|
||||
onValueChange={(value) => {
|
||||
setTitle(value);
|
||||
if (!isSlugManuallyEdited) {
|
||||
setGameSlug(sanitizeSlug(value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Game Slug"
|
||||
labelPlacement="outside"
|
||||
placeholder="your-game-name"
|
||||
value={gameSlug}
|
||||
onValueChange={(value) => {
|
||||
setGameSlug(sanitizeSlug(value));
|
||||
setIsSlugManuallyEdited(true);
|
||||
}}
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<span>{user.name}</span>
|
||||
/>
|
||||
{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={() => {
|
||||
if (!selectedAuthors.some((a) => a.id === user.id)) {
|
||||
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>
|
||||
{((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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
{((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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<label className="text-sm font-medium">Game Description</label>
|
||||
<Editor key={editorKey} content={content} setContent={setContent} gameEditor />
|
||||
</div>
|
||||
<label className="text-sm font-medium">Game Description</label>
|
||||
<Editor
|
||||
key={editorKey}
|
||||
content={content}
|
||||
setContent={setContent}
|
||||
gameEditor
|
||||
/>
|
||||
|
||||
<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}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Thumbnail URL"
|
||||
labelPlacement="outside"
|
||||
placeholder="https://example.com/thumbnail.png"
|
||||
value={thumbnailUrl}
|
||||
onValueChange={setThumbnailUrl}
|
||||
/>
|
||||
<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://"
|
||||
);
|
||||
|
||||
<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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
className="w-96"
|
||||
defaultSelectedKeys={["Windows"]}
|
||||
aria-label="Select platform" // Add this to fix accessibility warning
|
||||
onSelectionChange={(value) => {
|
||||
const newLinks = [...downloadLinks];
|
||||
newLinks[index].platform = value as unknown as PlatformType;
|
||||
setDownloadLinks(newLinks);
|
||||
}}
|
||||
>
|
||||
<SelectItem key="Windows" value="Windows">
|
||||
Windows
|
||||
</SelectItem>
|
||||
<SelectItem key="MacOS" value="MacOS">
|
||||
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>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
className="w-96"
|
||||
defaultSelectedKeys={["Windows"]}
|
||||
aria-label="Select platform" // Add this to fix accessibility warning
|
||||
onSelectionChange={(value) => {
|
||||
const newLinks = [...downloadLinks];
|
||||
newLinks[index].platform =
|
||||
value as unknown as PlatformType;
|
||||
setDownloadLinks(newLinks);
|
||||
}}
|
||||
>
|
||||
<SelectItem key="Windows" value="Windows">
|
||||
Windows
|
||||
</SelectItem>
|
||||
<SelectItem key="MacOS" value="MacOS">
|
||||
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>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={() => {
|
||||
setDownloadLinks([
|
||||
...downloadLinks,
|
||||
{
|
||||
id: Date.now(),
|
||||
url: "",
|
||||
platform: "Windows",
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
Add Download Link
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button color="primary" type="submit">
|
||||
{waitingPost ? (
|
||||
<LoaderCircle className="animate-spin" size={16} />
|
||||
) : (
|
||||
<p>{editGame ? "Update" : "Create"}</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button color="primary" type="submit">
|
||||
{waitingPost ? (
|
||||
<LoaderCircle className="animate-spin" size={16} />
|
||||
) : (
|
||||
<p>{editGame ? "Update" : "Create"}</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col gap-4 px-8 items-end">
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ 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";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { sanitize } from "@/helpers/sanitize";
|
||||
|
||||
export default function CreatePostPage() {
|
||||
const [title, setTitle] = useState("");
|
||||
|
@ -118,7 +118,6 @@ export default function CreatePostPage() {
|
|||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
|
||||
const styles: StylesConfig<
|
||||
{
|
||||
value: string;
|
||||
|
@ -213,7 +212,7 @@ export default function CreatePostPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
const sanitizedHtml = sanitize(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
const tags = [];
|
||||
|
@ -320,10 +319,10 @@ export default function CreatePostPage() {
|
|||
</Form>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col gap-4 px-8 items-end">
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -37,8 +37,8 @@ import { redirect, useParams } from "next/navigation";
|
|||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import Editor from "@/components/editor";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import CommentCard from "@/components/posts/CommentCard";
|
||||
import { sanitize } from "@/helpers/sanitize";
|
||||
|
||||
export default function PostPage() {
|
||||
const [post, setPost] = useState<PostType>();
|
||||
|
@ -579,7 +579,7 @@ export default function PostPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
const sanitizedHtml = sanitize(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
const response = await fetch(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import Editor from "@/components/editor";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { Avatar, Button, Form, Input, Spacer } from "@nextui-org/react";
|
||||
|
@ -10,6 +9,7 @@ import { useEffect, useState } from "react";
|
|||
import { toast } from "react-toastify";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { sanitize } from "@/helpers/sanitize";
|
||||
|
||||
export default function UserPage() {
|
||||
const [user, setUser] = useState<UserType>();
|
||||
|
@ -71,7 +71,7 @@ export default function UserPage() {
|
|||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const sanitizedBio = sanitizeHtml(bio);
|
||||
const sanitizedBio = sanitize(bio);
|
||||
|
||||
if (!name) {
|
||||
toast.error("You need to enter a name");
|
||||
|
|
|
@ -37,6 +37,9 @@ import CodeBlock from "@tiptap/extension-code-block";
|
|||
import { Spacer } from "@nextui-org/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import ImageResize from "tiptap-extension-resize-image";
|
||||
import { toast } from "react-toastify";
|
||||
import { getCookie } from "@/helpers/cookie";
|
||||
|
||||
type EditorProps = {
|
||||
content: string;
|
||||
|
@ -95,6 +98,7 @@ export default function Editor({
|
|||
Youtube,
|
||||
CodeBlock,
|
||||
Link,
|
||||
ImageResize,
|
||||
],
|
||||
content: content,
|
||||
immediatelyRender: false,
|
||||
|
@ -110,6 +114,69 @@ export default function Editor({
|
|||
: "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",
|
||||
},
|
||||
handleDrop: (view, event, slice, moved) => {
|
||||
if (
|
||||
!moved &&
|
||||
event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files[0]
|
||||
) {
|
||||
const file = event.dataTransfer.files[0];
|
||||
const filesize = parseInt((file.size / 1024 / 1024).toFixed(4));
|
||||
|
||||
if (file.type !== "image/jpeg" && file.type !== "image/png") {
|
||||
toast.error("Invalid file format");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(filesize);
|
||||
|
||||
if (filesize > 8) {
|
||||
toast.error("Image is too big");
|
||||
return false;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("upload", event.dataTransfer.files[0]);
|
||||
|
||||
fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/image"
|
||||
: "http://localhost:3005/api/v1/image",
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
authorization: `Bearer ${getCookie("token")}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
).then((response) => {
|
||||
if (response.ok) {
|
||||
response.json().then((data) => {
|
||||
toast.success(data.message);
|
||||
const { schema } = view.state;
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
if (!coordinates) {
|
||||
toast.error("Error getting coordinates");
|
||||
return;
|
||||
}
|
||||
|
||||
const node = schema.nodes.image.create({ src: data.data });
|
||||
const transaction = view.state.tr.insert(coordinates.pos, node);
|
||||
return view.dispatch(transaction);
|
||||
});
|
||||
} else {
|
||||
toast.error("Failed to upload image");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ import { useState } from "react";
|
|||
import Editor from "../editor";
|
||||
import { toast } from "react-toastify";
|
||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import LikeButton from "./LikeButton";
|
||||
import { sanitize } from "@/helpers/sanitize";
|
||||
|
||||
export default function CommentCard({ comment }: { comment: CommentType }) {
|
||||
const [creatingReply, setCreatingReply] = useState<boolean>(false);
|
||||
|
@ -90,7 +90,7 @@ export default function CommentCard({ comment }: { comment: CommentType }) {
|
|||
return;
|
||||
}
|
||||
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
const sanitizedHtml = sanitize(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
const response = await fetch(
|
||||
|
|
21
src/helpers/sanitize.ts
Normal file
21
src/helpers/sanitize.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
export function sanitize(content: string) {
|
||||
return sanitizeHtml(content, {
|
||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
|
||||
allowedAttributes: {
|
||||
img: ["src", "style"],
|
||||
p: ["style"],
|
||||
},
|
||||
allowedStyles: {
|
||||
img: {
|
||||
width: [/^\d+(px|%)?|auto$/],
|
||||
height: [/^\d+(px|%)?|auto$/],
|
||||
margin: [/^\d+(px|%)?|auto$/],
|
||||
},
|
||||
p: {
|
||||
"text-align": [/^right|left|center$/],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue