From 31e55e7c3c15dd44e03fb7167643056b59d5703a Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Sat, 13 Dec 2025 14:08:25 -1000 Subject: [PATCH] Add share modal to hack page --- next.config.ts | 14 + src/app/hack/[slug]/page.tsx | 13 +- src/components/Hack/HackOptionsMenu.tsx | 24 -- src/components/Hack/HackShareButton.tsx | 33 ++ src/components/Hack/ShareModal.tsx | 398 ++++++++++++++++++++++++ 5 files changed, 457 insertions(+), 25 deletions(-) create mode 100644 src/components/Hack/HackShareButton.tsx create mode 100644 src/components/Hack/ShareModal.tsx diff --git a/next.config.ts b/next.config.ts index 9712f9d..223de60 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,8 +10,22 @@ const nextConfig: NextConfig = { port: '54321', pathname: '/storage/v1/**', }, + { + protocol: 'https', + hostname: 'euma9sxl9y.ufs.sh', + pathname: '/f/**', + } ], }, + redirects: async () => { + return [ + { + source: '/img/badge-dark.png', + destination: 'https://euma9sxl9y.ufs.sh/f/zxX0gGr1fg9cnaso9bhNGy79goWrVw5OTBFjMdbiXvASpLRE', + permanent: false, + }, + ]; + }, turbopack: { resolveExtensions: ['.mdx', '.md', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'], rules: { diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx index 29c72d6..f718b62 100644 --- a/src/app/hack/[slug]/page.tsx +++ b/src/app/hack/[slug]/page.tsx @@ -12,6 +12,7 @@ import PokeCommunityIcon from "@/components/Icons/PokeCommunityIcon"; import { createClient, createServiceClient } from "@/utils/supabase/server"; import HackOptionsMenu from "@/components/Hack/HackOptionsMenu"; import DownloadsBadge from "@/components/Hack/DownloadsBadge"; +import HackShareButton from "@/components/Hack/HackShareButton"; import type { CreativeWork, WithContext } from "schema-dts"; import serialize from "serialize-javascript"; import { headers } from "next/headers"; @@ -339,6 +340,11 @@ export default async function HackDetail({ params }: HackDetailProps) { {patchVersion || "Pre-release"} + {!isArchive && ( +
+ +
+ )}

By {isArchive ? (hack.original_author || "Unknown") : author}

{hack.summary}

@@ -352,7 +358,12 @@ export default async function HackDetail({ params }: HackDetailProps) { ))}
- {!isArchive && } + {!isArchive && ( +
+ +
+ )} + {isAdmin && !hack.approved && ( - { - try { - const url = window.location.href; - const title = document?.title || "Check this out"; - if (navigator.share) { - await navigator.share({ title, url }); - } else { - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(url); - alert("Link copied to clipboard"); - } - } - } catch (e) { - // Ignore if user cancels share; otherwise log - if (!(e instanceof Error) || e.name !== "AbortError") { - console.error(e); - } - } - }} - className="block w-full px-3 py-2 text-left text-sm data-focus:bg-black/5 dark:data-focus:bg-white/10"> - Share - { diff --git a/src/components/Hack/HackShareButton.tsx b/src/components/Hack/HackShareButton.tsx new file mode 100644 index 0000000..5a52894 --- /dev/null +++ b/src/components/Hack/HackShareButton.tsx @@ -0,0 +1,33 @@ +"use client"; + +import React, { useState } from "react"; +import { FiShare2 } from "react-icons/fi"; +import ShareModal from "@/components/Hack/ShareModal"; + +interface HackShareButtonProps { + title: string; + url: string; + author: string | null; +} + +export default function HackShareButton({ title, url, author }: HackShareButtonProps) { + const [showShareModal, setShowShareModal] = useState(false); + + return ( + <> + + {showShareModal && ( + setShowShareModal(false)} /> + )} + + ); +} diff --git a/src/components/Hack/ShareModal.tsx b/src/components/Hack/ShareModal.tsx new file mode 100644 index 0000000..a9cb5db --- /dev/null +++ b/src/components/Hack/ShareModal.tsx @@ -0,0 +1,398 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { FiX, FiCheck, FiCopy, FiMail, FiShare2, FiArrowLeft } from "react-icons/fi"; +import { FaXTwitter, FaReddit, FaFacebook } from "react-icons/fa6"; +import { FaInfoCircle } from "react-icons/fa"; +import { PiBracketsSquareBold, PiBracketsAngleBold } from "react-icons/pi"; + +const BANNER_IMAGE_URL = "/img/badge-dark.png"; +const BANNER_IMAGE_WIDTH = 190; +const BANNER_IMAGE_HEIGHT = 60; +const BANNER_IMAGE_FULL_URL = `${process.env.NEXT_PUBLIC_SITE_URL}/img/badge-dark.png`; + +interface ShareModalProps { + title: string; + url: string; + author: string | null; + onClose: () => void; +} + +const ShareModal: React.FC = ({ title, url, author, onClose }) => { + const [urlCopied, setUrlCopied] = useState(false); + const [codePreview, setCodePreview] = useState<{ type: string; code: string; label: string } | null>(null); + const [codeCopied, setCodeCopied] = useState(false); + const hasNavigatorShare = typeof navigator !== "undefined" && navigator.share; + + useEffect(() => { + const html = document.documentElement; + const body = document.body; + const previousHtmlOverflow = html.style.overflow; + const previousBodyOverflow = body.style.overflow; + const previousBodyPaddingRight = body.style.paddingRight; + const scrollBarWidth = window.innerWidth - html.clientWidth; + + html.style.overflow = "hidden"; + body.style.overflow = "hidden"; + if (scrollBarWidth > 0) { + body.style.paddingRight = `${scrollBarWidth}px`; + } + + return () => { + html.style.overflow = previousHtmlOverflow; + body.style.overflow = previousBodyOverflow; + body.style.paddingRight = previousBodyPaddingRight; + }; + }, []); + + const copyUrl = async () => { + try { + await navigator.clipboard.writeText(url); + setUrlCopied(true); + setTimeout(() => setUrlCopied(false), 2000); + } catch (e) { + console.error("Failed to copy URL:", e); + } + }; + + const copyCode = async () => { + if (!codePreview) return; + try { + await navigator.clipboard.writeText(codePreview.code); + setCodeCopied(true); + setTimeout(() => setCodeCopied(false), 2000); + } catch (e) { + console.error("Failed to copy code:", e); + } + }; + + const getCodePreview = (type: string): { code: string; label: string } | null => { + switch (type) { + case "bbcode": + return { code: `[url=${url}][img width=${BANNER_IMAGE_WIDTH} height=${BANNER_IMAGE_HEIGHT}]${BANNER_IMAGE_FULL_URL}[/img][/url]`, label: "BBCode" }; + case "html": + return { code: `Download now at hackdex.app`, label: "HTML" }; + default: + return null; + } + }; + + const handleShare = async (type: string) => { + // Show preview for code formats + if (type === "bbcode" || type === "html") { + const preview = getCodePreview(type); + if (preview) { + setCodePreview({ type, ...preview }); + return; + } + } + + const socialTitle = author ? `Romhack: ${title} by ${author}` : `Romhack: ${title}`; + + switch (type) { + case "other": { + try { + await navigator.share({ + title: socialTitle, + url, + }); + } catch (e) { + // Ignore if user cancels share + if (!(e instanceof Error) || e.name !== "AbortError") { + console.error(e); + } + } + break; + } + case "reddit": { + const redditUrl = `https://www.reddit.com/submit?url=${encodeURIComponent(url)}&title=${encodeURIComponent(socialTitle)}`; + window.open(redditUrl, "_blank", "width=1024,height=768"); + break; + } + case "twitter": { + const twitterUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(socialTitle)}`; + window.open(twitterUrl, "_blank", "width=1024,height=768"); + break; + } + case "email": { + const subject = encodeURIComponent(`Check out ${title}`); + const body = encodeURIComponent(`I found this ROM hack that you might like: ${url}`); + window.location.href = `mailto:?subject=${subject}&body=${body}`; + break; + } + case "facebook": { + const facebookUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`; + window.open(facebookUrl, "_blank", "width=1024,height=768"); + break; + } + } + }; + + const SocialIconButton = ({ + type, + icon: Icon, + label, + }: { + type: string; + icon: React.ComponentType<{ size?: number; className?: string }>; + label: string; + }) => { + return ( + + ); + }; + + + // Show code preview modal if codePreview is set + if (codePreview) { + return ( +
+
+
+ + +
+ {/* Header */} +
+ +

{codePreview.label} Code

+

+ Copy the code below to share this hack with the badge. +

+
+ + {/* Badge Preview */} +
+

Preview:

+
+ Download now at hackdex.app +
+
+ + {/* Code Section */} +
+
+

Code

+
+
+