diff --git a/package-lock.json b/package-lock.json index c7fa9db..3a86a28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 9ce4adb..1f2915b 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/app/create-game/page.tsx b/src/app/create-game/page.tsx index 5538504..277d80c 100644 --- a/src/app/create-game/page.tsx +++ b/src/app/create-game/page.tsx @@ -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> ); } diff --git a/src/app/create-post/page.tsx b/src/app/create-post/page.tsx index 8db48d6..68f2b82 100644 --- a/src/app/create-post/page.tsx +++ b/src/app/create-post/page.tsx @@ -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> ); } diff --git a/src/app/p/[slug]/page.tsx b/src/app/p/[slug]/page.tsx index fff860e..b04a97d 100644 --- a/src/app/p/[slug]/page.tsx +++ b/src/app/p/[slug]/page.tsx @@ -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( diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 53c3054..09cb06b 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -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"); diff --git a/src/components/editor/index.tsx b/src/components/editor/index.tsx index 5c1edc0..d69cdc0 100644 --- a/src/components/editor/index.tsx +++ b/src/components/editor/index.tsx @@ -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; + }, }, }); diff --git a/src/components/posts/CommentCard.tsx b/src/components/posts/CommentCard.tsx index ff6b469..e34b001 100644 --- a/src/components/posts/CommentCard.tsx +++ b/src/components/posts/CommentCard.tsx @@ -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( diff --git a/src/helpers/sanitize.ts b/src/helpers/sanitize.ts new file mode 100644 index 0000000..4c0cf8e --- /dev/null +++ b/src/helpers/sanitize.ts @@ -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$/], + }, + }, + }); +}