From fa7d537ef70523395cf6cf93fcd7370a234ceb16 Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Wed, 1 Jul 2026 15:32:26 +0800 Subject: [PATCH] Open buy buttons in CrowdRepublic crowdfunding modal. Replace donate.stream links with a modal announcing the upcoming campaign on CrowdRepublic, with RU/EN copy and the old buy URL commented out. Co-authored-by: Cursor --- src/components/layout/Footer.module.css | 8 ++ src/components/layout/Footer.tsx | 36 ++++--- src/components/layout/Header.tsx | 10 +- src/components/sections/CtaBanner.tsx | 5 +- src/components/sections/Hero.tsx | 5 +- src/components/sections/Pricing.tsx | 11 +- src/components/ui/BuyButton.tsx | 26 +++++ src/data/siteContent.ts | 4 +- src/i18n/messages.ts | 12 +++ src/main.tsx | 5 +- src/providers/CrowdfundingModal.module.css | 72 +++++++++++++ src/providers/CrowdfundingModalProvider.tsx | 107 ++++++++++++++++++++ src/siteContent.test.ts | 5 +- 13 files changed, 267 insertions(+), 39 deletions(-) create mode 100644 src/components/ui/BuyButton.tsx create mode 100644 src/providers/CrowdfundingModal.module.css create mode 100644 src/providers/CrowdfundingModalProvider.tsx diff --git a/src/components/layout/Footer.module.css b/src/components/layout/Footer.module.css index 4c731ac..ba3e995 100644 --- a/src/components/layout/Footer.module.css +++ b/src/components/layout/Footer.module.css @@ -34,3 +34,11 @@ .link:hover { color: var(--color-gold); } + +.linkButton { + border: none; + background: none; + padding: 0; + font: inherit; + cursor: pointer; +} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index d0eb1d9..1ca6a81 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,5 +1,6 @@ import { SITE_LINKS } from "../../data/siteContent"; import { useLocale } from "../../i18n/LocaleProvider"; +import { useCrowdfundingModal } from "../../providers/CrowdfundingModalProvider"; import styles from "./Footer.module.css"; @@ -11,9 +12,11 @@ const FOOTER_LINK_KEYS = [ "support", ] as const; -const FOOTER_HREFS: Record<(typeof FOOTER_LINK_KEYS)[number], string> = { +const FOOTER_HREFS: Record< + Exclude<(typeof FOOTER_LINK_KEYS)[number], "buy">, + string +> = { download: SITE_LINKS.download, - buy: SITE_LINKS.buy, faq: SITE_LINKS.faq, enterKey: SITE_LINKS.enterKey, support: SITE_LINKS.support, @@ -21,24 +24,29 @@ const FOOTER_HREFS: Record<(typeof FOOTER_LINK_KEYS)[number], string> = { export function Footer() { const { copy } = useLocale(); + const { openModal } = useCrowdfundingModal(); return ( diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 63000c5..86f957b 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,6 +1,7 @@ import { NAV_HREFS, NAV_LINK_KEYS, SITE_LINKS } from "../../data/siteContent"; import { useLocale } from "../../i18n/LocaleProvider"; import { Button } from "../ui/Button"; +import { BuyButton } from "../ui/BuyButton"; import { LanguageSwitcher } from "./LanguageSwitcher"; import styles from "./Header.module.css"; @@ -39,14 +40,9 @@ export function Header() { > {copy.cta.download} - + diff --git a/src/components/sections/CtaBanner.tsx b/src/components/sections/CtaBanner.tsx index 7a802c6..06ef91c 100644 --- a/src/components/sections/CtaBanner.tsx +++ b/src/components/sections/CtaBanner.tsx @@ -1,6 +1,7 @@ import { SITE_LINKS } from "../../data/siteContent"; import { useLocale } from "../../i18n/LocaleProvider"; import { Button } from "../ui/Button"; +import { BuyButton } from "../ui/BuyButton"; import styles from "./CtaBanner.module.css"; @@ -13,9 +14,7 @@ export function CtaBanner() {

{copy.ctaBanner.title}

- + {copy.cta.buyLicense}
diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx index b4fe398..de3c576 100644 --- a/src/components/sections/Hero.tsx +++ b/src/components/sections/Hero.tsx @@ -1,6 +1,7 @@ import { HERO_HIGHLIGHT_IDS, SITE_LINKS } from "../../data/siteContent"; import { useLocale } from "../../i18n/LocaleProvider"; import { Button } from "../ui/Button"; +import { BuyButton } from "../ui/BuyButton"; import { HeroMockup } from "./HeroMockup"; import styles from "./Hero.module.css"; @@ -25,9 +26,7 @@ export function Hero() {
- + {copy.cta.buyLicense}

{copy.hero.platformsLine}

diff --git a/src/components/sections/Pricing.tsx b/src/components/sections/Pricing.tsx index be8f22a..c005bba 100644 --- a/src/components/sections/Pricing.tsx +++ b/src/components/sections/Pricing.tsx @@ -1,14 +1,13 @@ import { PRICING_TIER_IDS, RECOMMENDED_PRICING_TIER, - SITE_LINKS, } from "../../data/siteContent"; import { PRICING_TIER_PRICES_RUB } from "../../data/pricing"; import { useLocale } from "../../i18n/LocaleProvider"; import { formatRubPrice, formatUsdPrice } from "../../lib/exchangeRate"; import { useExchangeRate } from "../../providers/ExchangeRateProvider"; import { Section } from "../layout/Section"; -import { Button } from "../ui/Button"; +import { BuyButton } from "../ui/BuyButton"; import styles from "./Pricing.module.css"; @@ -61,13 +60,9 @@ export function Pricing() {
  • {feature}
  • ))} - + ); })} diff --git a/src/components/ui/BuyButton.tsx b/src/components/ui/BuyButton.tsx new file mode 100644 index 0000000..9952824 --- /dev/null +++ b/src/components/ui/BuyButton.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from "react"; + +import { useCrowdfundingModal } from "../../providers/CrowdfundingModalProvider"; +import { Button } from "./Button"; + +type BuyButtonVariant = "primary" | "secondary" | "ghost" | "gold"; + +type BuyButtonProps = { + children: ReactNode; + variant?: BuyButtonVariant; + className?: string; +}; + +export function BuyButton({ + children, + variant = "secondary", + className, +}: BuyButtonProps) { + const { openModal } = useCrowdfundingModal(); + + return ( + + ); +} diff --git a/src/data/siteContent.ts b/src/data/siteContent.ts index ca0b650..fefe4ee 100644 --- a/src/data/siteContent.ts +++ b/src/data/siteContent.ts @@ -1,3 +1,5 @@ +export const CROWDFUNDING_URL = "https://crowdrepublic.ru/" as const; + export const SITE_LINKS = { features: "#features", howItWorks: "#how-it-works", @@ -5,7 +7,7 @@ export const SITE_LINKS = { pricing: "#pricing", faq: "#faq", download: "#platforms", - buy: "https://donate.stream/ttrpgplayer", + // buy: "https://donate.stream/ttrpgplayer", enterKey: "#pricing", support: "mailto:player.ttrpg@gmail.com", } as const; diff --git a/src/i18n/messages.ts b/src/i18n/messages.ts index 7456ac8..7598268 100644 --- a/src/i18n/messages.ts +++ b/src/i18n/messages.ts @@ -245,6 +245,12 @@ const ru = { ctaBanner: { title: "Готовы вести следующую сессию по-новому?", }, + crowdfundingModal: { + title: "Скоро старт краудфандинга", + messageBefore: "Скоро начнётся краудфандинговая кампания на ", + messageAfter: ", следите за новостями.", + close: "Понятно", + }, footer: { aria: "Подвал", links: { @@ -505,6 +511,12 @@ const en: SiteCopy = { ctaBanner: { title: "Ready to run your next session differently?", }, + crowdfundingModal: { + title: "Crowdfunding coming soon", + messageBefore: "A crowdfunding campaign will start soon on ", + messageAfter: " — stay tuned for updates.", + close: "Got it", + }, footer: { aria: "Footer", links: { diff --git a/src/main.tsx b/src/main.tsx index 0cac578..3dac449 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import { App } from "./App"; import { LocaleProvider } from "./i18n/LocaleProvider"; +import { CrowdfundingModalProvider } from "./providers/CrowdfundingModalProvider"; import { ExchangeRateProvider } from "./providers/ExchangeRateProvider"; import "./styles/global.css"; @@ -15,7 +16,9 @@ createRoot(rootEl).render( - + + + , diff --git a/src/providers/CrowdfundingModal.module.css b/src/providers/CrowdfundingModal.module.css new file mode 100644 index 0000000..8e71d1b --- /dev/null +++ b/src/providers/CrowdfundingModal.module.css @@ -0,0 +1,72 @@ +.overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(4, 3, 8, 0.78); + backdrop-filter: blur(6px); +} + +.dialog { + width: min(100%, 480px); + padding: 32px 28px 28px; + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-lg); + background: var(--color-bg-elevated); + box-shadow: + var(--shadow-glow), + 0 24px 48px rgba(0, 0, 0, 0.45); +} + +.title { + margin: 0 0 16px; + font-size: 22px; + font-weight: 700; + line-height: 1.3; +} + +.message { + margin: 0 0 24px; + font-size: 16px; + line-height: 1.65; + color: var(--color-text-muted); +} + +.link { + color: var(--color-gold-bright); + text-decoration: underline; + text-decoration-color: var(--color-gold-dim); + text-underline-offset: 3px; + word-break: break-all; +} + +.link:hover { + color: var(--color-gold); +} + +.closeBtn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 12px 22px; + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); + background: transparent; + color: var(--color-gold-bright); + font: inherit; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: + background 0.2s ease, + transform 0.15s ease; +} + +.closeBtn:hover { + background: rgba(200, 161, 102, 0.08); + transform: translateY(-1px); +} diff --git a/src/providers/CrowdfundingModalProvider.tsx b/src/providers/CrowdfundingModalProvider.tsx new file mode 100644 index 0000000..54d4656 --- /dev/null +++ b/src/providers/CrowdfundingModalProvider.tsx @@ -0,0 +1,107 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; + +import { CROWDFUNDING_URL } from "../data/siteContent"; +import { useLocale } from "../i18n/LocaleProvider"; + +import styles from "./CrowdfundingModal.module.css"; + +type CrowdfundingModalContextValue = { + openModal: () => void; +}; + +const CrowdfundingModalContext = + createContext(null); + +export function useCrowdfundingModal(): CrowdfundingModalContextValue { + const ctx = useContext(CrowdfundingModalContext); + if (!ctx) { + throw new Error( + "useCrowdfundingModal must be used within CrowdfundingModalProvider", + ); + } + return ctx; +} + +export function CrowdfundingModalProvider({ + children, +}: { + children: ReactNode; +}) { + const [open, setOpen] = useState(false); + const { copy } = useLocale(); + + const openModal = useCallback(() => { + setOpen(true); + }, []); + const closeModal = useCallback(() => { + setOpen(false); + }, []); + + useEffect(() => { + if (!open) return; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") closeModal(); + }; + + document.addEventListener("keydown", onKeyDown); + document.body.style.overflow = "hidden"; + + return () => { + document.removeEventListener("keydown", onKeyDown); + document.body.style.overflow = ""; + }; + }, [open, closeModal]); + + return ( + + {children} + {open ? ( +
    { + if (event.target === event.currentTarget) closeModal(); + }} + role="presentation" + > +
    +

    + {copy.crowdfundingModal.title} +

    +

    + {copy.crowdfundingModal.messageBefore} + + {CROWDFUNDING_URL} + + {copy.crowdfundingModal.messageAfter} +

    + +
    +
    + ) : null} +
    + ); +} diff --git a/src/siteContent.test.ts b/src/siteContent.test.ts index a5b8eef..4ea9563 100644 --- a/src/siteContent.test.ts +++ b/src/siteContent.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { + CROWDFUNDING_URL, DOWNLOAD_ARTIFACTS, DOWNLOAD_LINKS, FAQ_IDS, @@ -68,9 +69,9 @@ describe("siteContent", () => { ); }); - it("wires download anchor and external buy link", () => { + it("wires download anchor and CrowdRepublic crowdfunding url", () => { assert.equal(SITE_LINKS.download, "#platforms"); - assert.equal(SITE_LINKS.buy, "https://donate.stream/ttrpgplayer"); + assert.equal(CROWDFUNDING_URL, "https://crowdrepublic.ru/"); }); it("maps each feature variant to a screenshot asset", () => {