import FsLightbox from 'fslightbox-react' import { nip19 } from 'nostr-tools' import { useEffect, useMemo, useRef, useState } from 'react' import { Link as ReactRouterLink, useLoaderData, useNavigation, useParams, useSubmit } from 'react-router-dom' import { toast } from 'react-toastify' import { BlogCard } from '../../components/BlogCard' import { ProfileSection } from '../../components/ProfileSection' import { useAppSelector, useBodyScrollDisable } from '../../hooks' import { getGamePageRoute, getModsEditPageRoute } from '../../routes' import '../../styles/comments.css' import '../../styles/downloads.css' import '../../styles/innerPage.css' import '../../styles/popup.css' import '../../styles/post.css' import '../../styles/reactions.css' import '../../styles/styles.css' import '../../styles/tabs.css' import '../../styles/tags.css' import '../../styles/write.css' import { DownloadUrl, ModPageLoaderResult } from '../../types' import { capitalizeEachWord, copyTextToClipboard, downloadFile, getFilenameFromUrl } from '../../utils' import { Comments } from '../../components/comment' import { PublishDetails } from 'components/Internal/PublishDetails' import { Interactions } from 'components/Internal/Interactions' import { ReportPopup } from 'components/ReportPopup' import { Spinner } from 'components/Spinner' import { RouterLoadingSpinner } from 'components/LoadingSpinner' import { OriginalAuthor } from 'components/OriginalAuthor' import DOMPurify from 'dompurify' import TurndownService from 'turndown' import { Viewer } from 'components/Markdown/Viewer' const MOD_REPORT_REASONS = [ { label: 'Actually CP', key: 'actuallyCP' }, { label: 'Spam', key: 'spam' }, { label: 'Scam', key: 'scam' }, { label: 'Not a game mod', key: 'notAGameMod' }, { label: 'Stolen game mod', key: 'stolenGameMod' }, { label: `Repost of a game mod`, key: 'repost' }, { label: `Wasn't tagged NSFW`, key: 'wasntTaggedNSFW' }, { label: 'Other reason', key: 'otherReason' } ] export const ModPage = () => { const { mod } = useLoaderData() as ModPageLoaderResult // We can get author right away from naddr, no need to wait for mod data const { naddr } = useParams() let author = mod?.author if (naddr && !author) { try { const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) author = decoded.data.pubkey } catch (error) { // Silently ignore - we will get author eventually from mods } } const [commentCount, setCommentCount] = useState(0) const oldDownloadListRef = useRef(null) const handleViewOldLinks = () => { if (oldDownloadListRef.current) { // Toggle styles if (oldDownloadListRef.current.style.height === '0px') { // Enable styles oldDownloadListRef.current.style.padding = '' oldDownloadListRef.current.style.height = '' oldDownloadListRef.current.style.border = '' } else { // Disable styles oldDownloadListRef.current.style.padding = '0' oldDownloadListRef.current.style.height = '0' oldDownloadListRef.current.style.border = 'unset' } } } return ( <>
{mod ? ( <>

Mod Download

{mod.downloadUrls.length > 0 && (
)} {mod.downloadUrls.length > 1 && ( <>
{mod.downloadUrls .slice(1) .map((download, index) => ( ))}
)}
) : ( )}
{typeof author !== 'undefined' && ( )}
) } const Game = () => { const { naddr } = useParams() const navigation = useNavigation() const { mod, isAddedToNSFW, isBlocked, isRepost } = useLoaderData() as ModPageLoaderResult const userState = useAppSelector((state) => state.user) const [showReportPopUp, setShowReportPopUp] = useState() useBodyScrollDisable(!!showReportPopUp) const submit = useSubmit() const handleBlock = () => { if (navigation.state === 'idle') { submit( { intent: 'block', value: !isBlocked }, { method: 'post', encType: 'application/json' } ) } } const handleNSFW = () => { if (navigation.state === 'idle') { submit( { intent: 'nsfw', value: !isAddedToNSFW }, { method: 'post', encType: 'application/json' } ) } } const handleRepost = () => { if (navigation.state === 'idle') { submit( { intent: 'repost', value: !isRepost }, { method: 'post', encType: 'application/json' } ) } } const isAdmin = userState.user?.npub && userState.user.npub === import.meta.env.VITE_REPORTING_NPUB const game = mod?.game || '' const gameRoute = getGamePageRoute(game) const editRoute = getModsEditPageRoute(naddr ? naddr : '') return ( <>

Mod for:  {game}

{!!showReportPopUp && ( setShowReportPopUp(undefined)} /> )} ) } type BodyProps = { featuredImageUrl: string title: string body: string game: string screenshotsUrls: string[] tags: string[] LTags: string[] nsfw: boolean repost: boolean originalAuthor?: string } const Body = ({ featuredImageUrl, game, title, body, screenshotsUrls, tags, LTags, nsfw, repost, originalAuthor }: BodyProps) => { const COLLAPSED_MAX_SIZE = 250 const postBodyRef = useRef(null) const viewFullPostBtnRef = useRef(null) const markdown = useMemo(() => { const sanitized = DOMPurify.sanitize(body) const turndown = new TurndownService() turndown.keep(['sup', 'sub']) return turndown.turndown(sanitized) }, [body]) const [lightBoxController, setLightBoxController] = useState({ toggler: false, slide: 1 }) const openLightBoxOnSlide = (slide: number) => { setLightBoxController((prev) => ({ toggler: !prev.toggler, slide })) } useEffect(() => { if (postBodyRef.current) { if (postBodyRef.current.scrollHeight <= COLLAPSED_MAX_SIZE) { viewFullPost() } } }, []) const viewFullPost = () => { if (postBodyRef.current && viewFullPostBtnRef.current) { postBodyRef.current.style.maxHeight = 'unset' postBodyRef.current.style.padding = 'unset' viewFullPostBtnRef.current.style.display = 'none' } } return ( <>

{title}

Read Full

{screenshotsUrls.map((url, index) => ( openLightBoxOnSlide(index + 1)} /> ))}
{nsfw && (

NSFW

)} {repost && (

REPOST {originalAuthor && originalAuthor !== '' && ( <> . Original Author:{' '} )}

)} {tags.map((tag, index) => ( {tag} ))} {LTags.length > 0 && (
{LTags.map((hierarchy) => { const hierarchicalCategories = hierarchy.split(`:`) const categories = hierarchicalCategories .map((c, i) => { const partialHierarchy = hierarchicalCategories .slice(0, i + 1) .join(':') return (

{capitalizeEachWord(c)}

) }) .reduce((prev, curr, i) => [ prev,

>

, curr ]) return (
{categories}
) })}
)}
) } const Download = ({ url, hash, signatureKey, malwareScanLink, modVersion, customNote }: DownloadUrl) => { const [showAuthDetails, setShowAuthDetails] = useState(false) const handleDownload = () => { // Get the filename from the URL const filename = getFilenameFromUrl(url) downloadFile(url, filename) } return (
{/*temporarily commented out the WoT rating for download links within a mod post

Ratings (WIP):

420

420

420

4,200

4,200

4,200

*/}

setShowAuthDetails((prev) => !prev)} > Authentication Details

{showAuthDetails && (

Download URL

{url}

SHA-256 hash

{hash}

Signature from

{signatureKey}

Scan

{malwareScanLink}

Mod Version

{modVersion}

Note

{customNote}

)}
) } const DisplayModAuthorBlogs = () => { const { latest } = useLoaderData() as ModPageLoaderResult if (!latest?.length) return null return (

Creator's Blog Posts

{latest?.map((b) => ( ))}
) }