mirror of
https://github.com/Ategon/Jamjar.git
synced 2025-02-12 06:16:21 +00:00
Compare commits
9 commits
3bf841d259
...
87878f270a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
87878f270a | ||
![]() |
a2a1323e01 | ||
![]() |
eb54bd588a | ||
![]() |
7a5cbfdfba | ||
![]() |
922d89424b | ||
![]() |
21bb6b7188 | ||
![]() |
333bb8b387 | ||
![]() |
f8b2ca69d9 | ||
![]() |
8939141f76 |
12 changed files with 703 additions and 346 deletions
|
@ -7,6 +7,14 @@ const nextConfig: NextConfig = {
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "**",
|
hostname: "**",
|
||||||
},
|
},
|
||||||
|
...(process.env.NEXT_PUBLIC_MODE === "DEV"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
protocol: "http" as "http",
|
||||||
|
hostname: "localhost",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
36
package-lock.json
generated
36
package-lock.json
generated
|
@ -25,6 +25,7 @@
|
||||||
"@tiptap/extension-horizontal-rule": "^2.11.2",
|
"@tiptap/extension-horizontal-rule": "^2.11.2",
|
||||||
"@tiptap/extension-image": "^2.11.2",
|
"@tiptap/extension-image": "^2.11.2",
|
||||||
"@tiptap/extension-italic": "^2.11.2",
|
"@tiptap/extension-italic": "^2.11.2",
|
||||||
|
"@tiptap/extension-link": "^2.11.5",
|
||||||
"@tiptap/extension-list-item": "^2.11.2",
|
"@tiptap/extension-list-item": "^2.11.2",
|
||||||
"@tiptap/extension-ordered-list": "^2.11.2",
|
"@tiptap/extension-ordered-list": "^2.11.2",
|
||||||
"@tiptap/extension-paragraph": "^2.11.2",
|
"@tiptap/extension-paragraph": "^2.11.2",
|
||||||
|
@ -58,6 +59,7 @@
|
||||||
"react-select": "^5.9.0",
|
"react-select": "^5.9.0",
|
||||||
"react-toastify": "^11.0.3",
|
"react-toastify": "^11.0.3",
|
||||||
"sanitize-html": "^2.14.0",
|
"sanitize-html": "^2.14.0",
|
||||||
|
"tiptap-extension-resize-image": "^1.2.1",
|
||||||
"tiptap-markdown": "^0.8.10"
|
"tiptap-markdown": "^0.8.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -4361,6 +4363,23 @@
|
||||||
"@tiptap/core": "^2.7.0"
|
"@tiptap/core": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "2.11.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.11.5.tgz",
|
||||||
|
"integrity": "sha512-4Iu/aPzevbYpe50xDI0ZkqRa6nkZ9eF270Ue2qaF3Ab47nehj+9Jl78XXzo8+LTyFMnrETI73TAs1aC/IGySeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^2.7.0",
|
||||||
|
"@tiptap/pm": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-list-item": {
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
"version": "2.11.2",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.11.2.tgz",
|
||||||
|
@ -7980,6 +7999,12 @@
|
||||||
"uc.micro": "^2.0.0"
|
"uc.micro": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
|
@ -12630,6 +12655,17 @@
|
||||||
"@popperjs/core": "^2.9.0"
|
"@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": {
|
"node_modules/tiptap-markdown": {
|
||||||
"version": "0.8.10",
|
"version": "0.8.10",
|
||||||
"resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.8.10.tgz",
|
"resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.8.10.tgz",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"@tiptap/extension-horizontal-rule": "^2.11.2",
|
"@tiptap/extension-horizontal-rule": "^2.11.2",
|
||||||
"@tiptap/extension-image": "^2.11.2",
|
"@tiptap/extension-image": "^2.11.2",
|
||||||
"@tiptap/extension-italic": "^2.11.2",
|
"@tiptap/extension-italic": "^2.11.2",
|
||||||
|
"@tiptap/extension-link": "^2.11.5",
|
||||||
"@tiptap/extension-list-item": "^2.11.2",
|
"@tiptap/extension-list-item": "^2.11.2",
|
||||||
"@tiptap/extension-ordered-list": "^2.11.2",
|
"@tiptap/extension-ordered-list": "^2.11.2",
|
||||||
"@tiptap/extension-paragraph": "^2.11.2",
|
"@tiptap/extension-paragraph": "^2.11.2",
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
"react-select": "^5.9.0",
|
"react-select": "^5.9.0",
|
||||||
"react-toastify": "^11.0.3",
|
"react-toastify": "^11.0.3",
|
||||||
"sanitize-html": "^2.14.0",
|
"sanitize-html": "^2.14.0",
|
||||||
|
"tiptap-extension-resize-image": "^1.2.1",
|
||||||
"tiptap-markdown": "^0.8.10"
|
"tiptap-markdown": "^0.8.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -6,18 +6,15 @@ import { Button, Form, Input, Spacer } from "@nextui-org/react";
|
||||||
import { LoaderCircle } from "lucide-react";
|
import { LoaderCircle } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import sanitizeHtml from "sanitize-html";
|
|
||||||
import { Select, SelectItem } from "@nextui-org/react";
|
import { Select, SelectItem } from "@nextui-org/react";
|
||||||
import Timers from "@/components/timers";
|
import Timers from "@/components/timers";
|
||||||
import Streams from "@/components/streams";
|
import Streams from "@/components/streams";
|
||||||
import { UserType } from "@/types/UserType";
|
import { UserType } from "@/types/UserType";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import { GameType } from "@/types/GameType";
|
import { GameType } from "@/types/GameType";
|
||||||
import { PlatformType, DownloadLinkType } from "@/types/DownloadLinkType";
|
import { PlatformType, DownloadLinkType } from "@/types/DownloadLinkType";
|
||||||
|
import { sanitize } from "@/helpers/sanitize";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function CreateGamePage() {
|
export default function CreateGamePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -39,7 +36,7 @@ export default function CreateGamePage() {
|
||||||
const [gameSlug, setGameSlug] = useState("");
|
const [gameSlug, setGameSlug] = useState("");
|
||||||
const [prevSlug, setPrevGameSlug] = useState("");
|
const [prevSlug, setPrevGameSlug] = useState("");
|
||||||
const [game, setGame] = useState<GameType>();
|
const [game, setGame] = useState<GameType>();
|
||||||
const [thumbnailUrl, setThumbnailUrl] = useState("");
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
|
||||||
const [authorSearch, setAuthorSearch] = useState("");
|
const [authorSearch, setAuthorSearch] = useState("");
|
||||||
const [selectedAuthors, setSelectedAuthors] = useState<Array<UserType>>([]);
|
const [selectedAuthors, setSelectedAuthors] = useState<Array<UserType>>([]);
|
||||||
const [searchResults, setSearchResults] = useState<Array<UserType>>([]);
|
const [searchResults, setSearchResults] = useState<Array<UserType>>([]);
|
||||||
|
@ -53,8 +50,8 @@ export default function CreateGamePage() {
|
||||||
const sanitizeSlug = (value: string): string => {
|
const sanitizeSlug = (value: string): string => {
|
||||||
return value
|
return value
|
||||||
.toLowerCase() // Convert to lowercase
|
.toLowerCase() // Convert to lowercase
|
||||||
.replace(/\s+/g, '-') // Replace whitespace with hyphens
|
.replace(/\s+/g, "-") // Replace whitespace with hyphens
|
||||||
.replace(/[^a-z0-9-]/g, '') // Only allow lowercase letters, numbers, and hyphens
|
.replace(/[^a-z0-9-]/g, "") // Only allow lowercase letters, numbers, and hyphens
|
||||||
.substring(0, 50); // Limit length to 50 characters
|
.substring(0, 50); // Limit length to 50 characters
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -154,11 +151,14 @@ export default function CreateGamePage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkExistingGame = async () => {
|
const checkExistingGame = async () => {
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||||
? `https://d2jam.com/api/v1/self/current-game?username=${getCookie("user")}`
|
? `https://d2jam.com/api/v1/self/current-game?username=${getCookie(
|
||||||
: `http://localhost:3005/api/v1/self/current-game?username=${getCookie("user")}`,
|
"user"
|
||||||
|
)}`
|
||||||
|
: `http://localhost:3005/api/v1/self/current-game?username=${getCookie(
|
||||||
|
"user"
|
||||||
|
)}`,
|
||||||
{
|
{
|
||||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
@ -177,21 +177,18 @@ export default function CreateGamePage() {
|
||||||
setThumbnailUrl(gameData.thumbnail);
|
setThumbnailUrl(gameData.thumbnail);
|
||||||
setDownloadLinks(gameData.downloadLinks);
|
setDownloadLinks(gameData.downloadLinks);
|
||||||
setGame(gameData);
|
setGame(gameData);
|
||||||
const uniqueAuthors = [gameData.author, ...gameData.contributors]
|
const uniqueAuthors = [
|
||||||
.filter((author, index, self) =>
|
gameData.author,
|
||||||
|
...gameData.contributors,
|
||||||
|
].filter(
|
||||||
|
(author, index, self) =>
|
||||||
index === self.findIndex((a) => a.id === author.id)
|
index === self.findIndex((a) => a.id === author.id)
|
||||||
);
|
);
|
||||||
setSelectedAuthors(uniqueAuthors);
|
setSelectedAuthors(uniqueAuthors);
|
||||||
|
} else {
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
setSelectedAuthors(user ? [user] : []);
|
setSelectedAuthors(user ? [user] : []);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
setEditGame(false);
|
setEditGame(false);
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setGameSlug("");
|
setGameSlug("");
|
||||||
|
@ -199,20 +196,16 @@ export default function CreateGamePage() {
|
||||||
setEditorKey((prev) => prev + 1);
|
setEditorKey((prev) => prev + 1);
|
||||||
setThumbnailUrl("");
|
setThumbnailUrl("");
|
||||||
setDownloadLinks([]);
|
setDownloadLinks([]);
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mounted && user) {
|
if (mounted && user) {
|
||||||
checkExistingGame();
|
checkExistingGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [user, mounted]);
|
}, [user, mounted]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="static flex items-top mt-20 justify-center top-0 left-0">
|
<div className="static flex items-top mt-20 justify-center top-0 left-0">
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
className="w-full max-w-2xl flex flex-col gap-4"
|
className="w-full max-w-2xl flex flex-col gap-4"
|
||||||
validationErrors={errors}
|
validationErrors={errors}
|
||||||
|
@ -245,7 +238,7 @@ export default function CreateGamePage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedHtml = sanitizeHtml(content);
|
const sanitizedHtml = sanitize(content);
|
||||||
setWaitingPost(true);
|
setWaitingPost(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -285,7 +278,11 @@ export default function CreateGamePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
toast.success(gameSlug ? "Game updated successfully!" : "Game created successfully!");
|
toast.success(
|
||||||
|
gameSlug
|
||||||
|
? "Game updated successfully!"
|
||||||
|
: "Game created successfully!"
|
||||||
|
);
|
||||||
setWaitingPost(false);
|
setWaitingPost(false);
|
||||||
router.push(`/games/${gameSlug || sanitizeSlug(title)}`);
|
router.push(`/games/${gameSlug || sanitizeSlug(title)}`);
|
||||||
} else {
|
} else {
|
||||||
|
@ -349,7 +346,7 @@ export default function CreateGamePage() {
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="flex justify-between items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer"
|
className="flex justify-between items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!selectedAuthors.some(a => a.id === user.id)) {
|
if (!selectedAuthors.some((a) => a.id === user.id)) {
|
||||||
setSelectedAuthors([...selectedAuthors, user]);
|
setSelectedAuthors([...selectedAuthors, user]);
|
||||||
}
|
}
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
|
@ -368,9 +365,14 @@ export default function CreateGamePage() {
|
||||||
className="bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-100 px-3 py-1 rounded-full flex items-center gap-2"
|
className="bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-100 px-3 py-1 rounded-full flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span>{author.name}</span>
|
<span>{author.name}</span>
|
||||||
{((game && author.id !== game.authorId) || (!game && author.id !== user?.id)) && (
|
{((game && author.id !== game.authorId) ||
|
||||||
|
(!game && author.id !== user?.id)) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedAuthors(selectedAuthors.filter(a => a.id !== author.id))}
|
onClick={() =>
|
||||||
|
setSelectedAuthors(
|
||||||
|
selectedAuthors.filter((a) => a.id !== author.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
className="text-sm hover:text-red-500"
|
className="text-sm hover:text-red-500"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
|
@ -381,23 +383,79 @@ export default function CreateGamePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label className="text-sm font-medium">Game Description</label>
|
<label className="text-sm font-medium">Game Description</label>
|
||||||
<Editor key={editorKey} content={content} setContent={setContent} gameEditor />
|
<Editor
|
||||||
|
key={editorKey}
|
||||||
|
content={content}
|
||||||
|
setContent={setContent}
|
||||||
|
gameEditor
|
||||||
|
/>
|
||||||
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<p>Thumbnail</p>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("upload", file);
|
||||||
|
|
||||||
<Input
|
try {
|
||||||
label="Thumbnail URL"
|
const response = await fetch(
|
||||||
labelPlacement="outside"
|
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||||
placeholder="https://example.com/thumbnail.png"
|
? "https://d2jam.com/api/v1/image"
|
||||||
value={thumbnailUrl}
|
: "http://localhost:3005/api/v1/image",
|
||||||
onValueChange={setThumbnailUrl}
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${getCookie("token")}`,
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setThumbnailUrl(data.data);
|
||||||
|
toast.success(data.message);
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to upload image");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Error uploading image");
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{thumbnailUrl && (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="bg-[#222222] h-28 w-full relative">
|
||||||
|
<Image
|
||||||
|
src={thumbnailUrl}
|
||||||
|
alt={`${title}'s thumbnail`}
|
||||||
|
className="object-cover"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spacer y={3} />
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
size="sm"
|
||||||
|
onPress={() => {
|
||||||
|
setThumbnailUrl(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove Banner Picture
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{Array.isArray(downloadLinks) &&
|
{Array.isArray(downloadLinks) &&
|
||||||
|
@ -414,22 +472,27 @@ export default function CreateGamePage() {
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (!urlRegex.test(downloadLinks[index].url)) {
|
if (!urlRegex.test(downloadLinks[index].url)) {
|
||||||
toast.error("Please enter a valid URL starting with http:// or https://");
|
toast.error(
|
||||||
|
"Please enter a valid URL starting with http:// or https://"
|
||||||
|
);
|
||||||
|
|
||||||
if (!downloadLinks[index].url.startsWith("http://") && !downloadLinks[index].url.startsWith("https://")) {
|
if (
|
||||||
const newUrl = "https://" + downloadLinks[index].url;
|
!downloadLinks[index].url.startsWith("http://") &&
|
||||||
|
!downloadLinks[index].url.startsWith("https://")
|
||||||
|
) {
|
||||||
|
const newUrl =
|
||||||
|
"https://" + downloadLinks[index].url;
|
||||||
const newLinks = [...downloadLinks];
|
const newLinks = [...downloadLinks];
|
||||||
newLinks[index].url = newUrl;
|
newLinks[index].url = newUrl;
|
||||||
setDownloadLinks(newLinks);
|
setDownloadLinks(newLinks);
|
||||||
const input = document.querySelector<HTMLInputElement>(
|
const input =
|
||||||
|
document.querySelector<HTMLInputElement>(
|
||||||
`#download-link-${index}`
|
`#download-link-${index}`
|
||||||
);
|
);
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = newUrl;
|
input.value = newUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -439,7 +502,8 @@ export default function CreateGamePage() {
|
||||||
aria-label="Select platform" // Add this to fix accessibility warning
|
aria-label="Select platform" // Add this to fix accessibility warning
|
||||||
onSelectionChange={(value) => {
|
onSelectionChange={(value) => {
|
||||||
const newLinks = [...downloadLinks];
|
const newLinks = [...downloadLinks];
|
||||||
newLinks[index].platform = value as unknown as PlatformType;
|
newLinks[index].platform =
|
||||||
|
value as unknown as PlatformType;
|
||||||
setDownloadLinks(newLinks);
|
setDownloadLinks(newLinks);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -466,7 +530,9 @@ export default function CreateGamePage() {
|
||||||
color="danger"
|
color="danger"
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setDownloadLinks(downloadLinks.filter((l) => l.id !== link.id));
|
setDownloadLinks(
|
||||||
|
downloadLinks.filter((l) => l.id !== link.id)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
|
@ -510,7 +576,6 @@ export default function CreateGamePage() {
|
||||||
<Streams />
|
<Streams />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,12 @@ import { LoaderCircle } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
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 Timers from "@/components/timers";
|
||||||
import Streams from "@/components/streams";
|
import Streams from "@/components/streams";
|
||||||
import { UserType } from "@/types/UserType";
|
import { UserType } from "@/types/UserType";
|
||||||
|
import { sanitize } from "@/helpers/sanitize";
|
||||||
|
|
||||||
export default function CreatePostPage() {
|
export default function CreatePostPage() {
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
|
@ -118,7 +118,6 @@ export default function CreatePostPage() {
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const styles: StylesConfig<
|
const styles: StylesConfig<
|
||||||
{
|
{
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -213,7 +212,7 @@ export default function CreatePostPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedHtml = sanitizeHtml(content);
|
const sanitizedHtml = sanitize(content);
|
||||||
setWaitingPost(true);
|
setWaitingPost(true);
|
||||||
|
|
||||||
const tags = [];
|
const tags = [];
|
||||||
|
|
|
@ -37,8 +37,8 @@ import { redirect, useParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import Editor from "@/components/editor";
|
import Editor from "@/components/editor";
|
||||||
import sanitizeHtml from "sanitize-html";
|
|
||||||
import CommentCard from "@/components/posts/CommentCard";
|
import CommentCard from "@/components/posts/CommentCard";
|
||||||
|
import { sanitize } from "@/helpers/sanitize";
|
||||||
|
|
||||||
export default function PostPage() {
|
export default function PostPage() {
|
||||||
const [post, setPost] = useState<PostType>();
|
const [post, setPost] = useState<PostType>();
|
||||||
|
@ -579,7 +579,7 @@ export default function PostPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedHtml = sanitizeHtml(content);
|
const sanitizedHtml = sanitize(content);
|
||||||
setWaitingPost(true);
|
setWaitingPost(true);
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Editor from "@/components/editor";
|
import Editor from "@/components/editor";
|
||||||
import sanitizeHtml from "sanitize-html";
|
|
||||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||||
import { UserType } from "@/types/UserType";
|
import { UserType } from "@/types/UserType";
|
||||||
import { Avatar, Button, Form, Input } from "@nextui-org/react";
|
import { Avatar, Button, Form, Input, Spacer } from "@nextui-org/react";
|
||||||
import { redirect, usePathname } from "next/navigation";
|
import { redirect, usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { LoaderCircle } from "lucide-react";
|
import { LoaderCircle } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { sanitize } from "@/helpers/sanitize";
|
||||||
|
|
||||||
export default function UserPage() {
|
export default function UserPage() {
|
||||||
const [user, setUser] = useState<UserType>();
|
const [user, setUser] = useState<UserType>();
|
||||||
const [profilePicture, setProfilePicture] = useState("");
|
const [profilePicture, setProfilePicture] = useState<string | null>(null);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [bannerPicture, setBannerPicture] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
|
const [bannerPicture, setBannerPicture] = useState<string | null>(null);
|
||||||
const [bio, setBio] = useState("");
|
const [bio, setBio] = useState("");
|
||||||
const [errors] = useState({});
|
const [errors] = useState({});
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [waitingSave, setWaitingSave] = useState(false);
|
const [waitingSave, setWaitingSave] = useState(false);
|
||||||
|
const [showEmail, setShowEmail] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUser();
|
loadUser();
|
||||||
|
@ -48,6 +50,7 @@ export default function UserPage() {
|
||||||
setBannerPicture(data.bannerPicture ?? "");
|
setBannerPicture(data.bannerPicture ?? "");
|
||||||
setBio(data.bio ?? "");
|
setBio(data.bio ?? "");
|
||||||
setName(data.name ?? "");
|
setName(data.name ?? "");
|
||||||
|
setEmail(data.email ?? "");
|
||||||
} else {
|
} else {
|
||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
}
|
}
|
||||||
|
@ -71,7 +74,7 @@ export default function UserPage() {
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const sanitizedBio = sanitizeHtml(bio);
|
const sanitizedBio = sanitize(bio);
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
toast.error("You need to enter a name");
|
toast.error("You need to enter a name");
|
||||||
|
@ -92,6 +95,7 @@ export default function UserPage() {
|
||||||
profilePicture: profilePicture,
|
profilePicture: profilePicture,
|
||||||
bannerPicture: bannerPicture,
|
bannerPicture: bannerPicture,
|
||||||
targetUserSlug: user.slug,
|
targetUserSlug: user.slug,
|
||||||
|
email: email,
|
||||||
}),
|
}),
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -104,7 +108,7 @@ export default function UserPage() {
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
toast.success("Changed settings");
|
toast.success("Changed settings");
|
||||||
setUser(await response.json());
|
setUser((await response.json()).data);
|
||||||
setWaitingSave(false);
|
setWaitingSave(false);
|
||||||
} else {
|
} else {
|
||||||
toast.error("Failed to update settings");
|
toast.error("Failed to update settings");
|
||||||
|
@ -124,34 +128,122 @@ export default function UserPage() {
|
||||||
onValueChange={setName}
|
onValueChange={setName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<p>Email</p>
|
||||||
|
{showEmail && (
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
placeholder="Enter an email"
|
||||||
|
type="text"
|
||||||
|
value={email}
|
||||||
|
onValueChange={setEmail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button size="sm" onPress={() => setShowEmail(!showEmail)}>
|
||||||
|
{showEmail ? "Hide Email" : "Show Email"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<p>Bio</p>
|
<p>Bio</p>
|
||||||
<Editor content={bio} setContent={setBio} />
|
<Editor content={bio} setContent={setBio} />
|
||||||
|
|
||||||
<Input
|
<p>Profile Picture</p>
|
||||||
label="Profile Picture"
|
<input
|
||||||
labelPlacement="outside"
|
type="file"
|
||||||
name="profilePicture"
|
accept="image/*"
|
||||||
placeholder="Enter a url to an image"
|
onChange={async (e) => {
|
||||||
type="text"
|
const file = e.target.files?.[0];
|
||||||
value={profilePicture}
|
if (!file) return;
|
||||||
onValueChange={setProfilePicture}
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("upload", file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await 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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setProfilePicture(data.data);
|
||||||
|
toast.success(data.message);
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to upload image");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Error uploading image");
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{profilePicture && <Avatar src={profilePicture} />}
|
{profilePicture && (
|
||||||
|
<div>
|
||||||
|
<Avatar src={profilePicture} />
|
||||||
|
<Spacer y={3} />
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
size="sm"
|
||||||
|
onPress={() => {
|
||||||
|
setProfilePicture(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove Profile Picture
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Input
|
<p>Banner Picture</p>
|
||||||
label="Banner Picture"
|
<input
|
||||||
labelPlacement="outside"
|
type="file"
|
||||||
name="bannerPicture"
|
accept="image/*"
|
||||||
placeholder="Enter a url to an image"
|
onChange={async (e) => {
|
||||||
type="text"
|
const file = e.target.files?.[0];
|
||||||
value={bannerPicture}
|
if (!file) return;
|
||||||
onValueChange={setBannerPicture}
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("upload", file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await 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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setBannerPicture(data.data);
|
||||||
|
toast.success(data.message);
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to upload image");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Error uploading image");
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{bannerPicture &&
|
{bannerPicture && (
|
||||||
bannerPicture.startsWith("https://") &&
|
<div className="w-full">
|
||||||
bannerPicture.length > 8 && (
|
|
||||||
<div className="bg-[#222222] h-28 w-full relative">
|
<div className="bg-[#222222] h-28 w-full relative">
|
||||||
<Image
|
<Image
|
||||||
src={bannerPicture}
|
src={bannerPicture}
|
||||||
|
@ -160,6 +252,17 @@ export default function UserPage() {
|
||||||
fill
|
fill
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Spacer y={3} />
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
size="sm"
|
||||||
|
onPress={() => {
|
||||||
|
setBannerPicture(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove Banner Picture
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|
|
@ -10,6 +10,7 @@ export default function UserPage() {
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [password2, setPassword2] = useState("");
|
const [password2, setPassword2] = useState("");
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute flex items-center 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">
|
||||||
|
@ -66,12 +67,24 @@ export default function UserPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
const regex = /.+@.+\..+/;
|
||||||
|
if (!email.match(regex)) {
|
||||||
|
setErrors({ email: "Invalid email" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||||
? "https://d2jam.com/api/v1/user"
|
? "https://d2jam.com/api/v1/user"
|
||||||
: "http://localhost:3005/api/v1/user",
|
: "http://localhost:3005/api/v1/user",
|
||||||
{
|
{
|
||||||
body: JSON.stringify({ username: username, password: password }),
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
email: email,
|
||||||
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
@ -104,6 +117,16 @@ export default function UserPage() {
|
||||||
onValueChange={setUsername}
|
onValueChange={setUsername}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
labelPlacement="outside"
|
||||||
|
name="email"
|
||||||
|
placeholder="Optional"
|
||||||
|
type="text"
|
||||||
|
value={email}
|
||||||
|
onValueChange={setEmail}
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
isRequired
|
isRequired
|
||||||
label="Password"
|
label="Password"
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
Code,
|
Code,
|
||||||
Highlighter,
|
Highlighter,
|
||||||
Italic,
|
Italic,
|
||||||
|
LinkIcon,
|
||||||
Minus,
|
Minus,
|
||||||
Quote,
|
Quote,
|
||||||
Redo,
|
Redo,
|
||||||
|
@ -28,6 +29,18 @@ type EditorMenuProps = {
|
||||||
export default function EditorMenuBar({ editor }: EditorMenuProps) {
|
export default function EditorMenuBar({ editor }: EditorMenuProps) {
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
|
const addLink = () => {
|
||||||
|
const url = prompt("Enter link URL:");
|
||||||
|
if (url) {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.extendMarkRange("link")
|
||||||
|
.setLink({ href: url })
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{
|
{
|
||||||
icon: <Bold size={20} />,
|
icon: <Bold size={20} />,
|
||||||
|
@ -71,6 +84,12 @@ export default function EditorMenuBar({ editor }: EditorMenuProps) {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
isActive: editor.isActive("superscript"),
|
isActive: editor.isActive("superscript"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <LinkIcon size={20} />,
|
||||||
|
onClick: addLink,
|
||||||
|
disabled: false,
|
||||||
|
isActive: editor.isActive("link"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: <Minus size={20} />,
|
icon: <Minus size={20} />,
|
||||||
onClick: () => editor.chain().focus().setHorizontalRule().run(),
|
onClick: () => editor.chain().focus().setHorizontalRule().run(),
|
||||||
|
|
|
@ -36,6 +36,10 @@ import Youtube from "@tiptap/extension-youtube";
|
||||||
import CodeBlock from "@tiptap/extension-code-block";
|
import CodeBlock from "@tiptap/extension-code-block";
|
||||||
import { Spacer } from "@nextui-org/react";
|
import { Spacer } from "@nextui-org/react";
|
||||||
import { useTheme } from "next-themes";
|
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 = {
|
type EditorProps = {
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -45,7 +49,11 @@ type EditorProps = {
|
||||||
|
|
||||||
const limit = 32767;
|
const limit = 32767;
|
||||||
|
|
||||||
export default function Editor({ content, setContent,gameEditor }: EditorProps) {
|
export default function Editor({
|
||||||
|
content,
|
||||||
|
setContent,
|
||||||
|
gameEditor,
|
||||||
|
}: EditorProps) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
|
@ -89,6 +97,8 @@ export default function Editor({ content, setContent,gameEditor }: EditorProps)
|
||||||
TableCell,
|
TableCell,
|
||||||
Youtube,
|
Youtube,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
|
Link,
|
||||||
|
ImageResize,
|
||||||
],
|
],
|
||||||
content: content,
|
content: content,
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
|
@ -104,6 +114,77 @@ export default function Editor({ content, setContent,gameEditor }: EditorProps)
|
||||||
: "min-h-[150px] max-h-[400px]") +
|
: "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",
|
" 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]
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
const filesize = parseInt((file.size / 1024 / 1024).toFixed(4));
|
||||||
|
|
||||||
|
const allowedTypes = [
|
||||||
|
"image/jpeg", // JPEG images
|
||||||
|
"image/png", // PNG images
|
||||||
|
"image/gif", // GIF images
|
||||||
|
"image/webp", // WebP images
|
||||||
|
"image/svg+xml", // SVG images
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
toast.error("Invalid file format");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Editor from "../editor";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||||
import sanitizeHtml from "sanitize-html";
|
|
||||||
import LikeButton from "./LikeButton";
|
import LikeButton from "./LikeButton";
|
||||||
|
import { sanitize } from "@/helpers/sanitize";
|
||||||
|
|
||||||
export default function CommentCard({ comment }: { comment: CommentType }) {
|
export default function CommentCard({ comment }: { comment: CommentType }) {
|
||||||
const [creatingReply, setCreatingReply] = useState<boolean>(false);
|
const [creatingReply, setCreatingReply] = useState<boolean>(false);
|
||||||
|
@ -90,7 +90,7 @@ export default function CommentCard({ comment }: { comment: CommentType }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedHtml = sanitizeHtml(content);
|
const sanitizedHtml = sanitize(content);
|
||||||
setWaitingPost(true);
|
setWaitingPost(true);
|
||||||
|
|
||||||
const response = await fetch(
|
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