Link support buttons to CrowdRepublic campaign and add VK.

Pricing buy buttons open the live crowdfunding page; hero and footer get VK links; remove unused modal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-06 20:54:10 +08:00
parent cb245bcb7d
commit e476e10b05
9 changed files with 37 additions and 207 deletions
+11 -2
View File
@@ -1,4 +1,4 @@
import { SITE_LINKS } from "../../data/siteContent";
import { SITE_LINKS, VK_URL } from "../../data/siteContent";
import { useLocale } from "../../i18n/LocaleProvider";
import { useEulaModal } from "../../providers/EulaModalProvider";
@@ -10,6 +10,7 @@ const FOOTER_LINK_KEYS = [
"faq",
"enterKey",
"support",
"vk",
] as const;
const FOOTER_HREFS: Record<(typeof FOOTER_LINK_KEYS)[number], string> = {
@@ -18,6 +19,7 @@ const FOOTER_HREFS: Record<(typeof FOOTER_LINK_KEYS)[number], string> = {
faq: SITE_LINKS.faq,
enterKey: SITE_LINKS.enterKey,
support: SITE_LINKS.support,
vk: VK_URL,
};
export function Footer() {
@@ -30,7 +32,14 @@ export function Footer() {
<p className={styles.copy}>© TTRPGPlayer</p>
<nav className={styles.links} aria-label={copy.footer.aria}>
{FOOTER_LINK_KEYS.map((key) => (
<a key={key} className={styles.link} href={FOOTER_HREFS[key]}>
<a
key={key}
className={styles.link}
href={FOOTER_HREFS[key]}
{...(key === "vk"
? { target: "_blank", rel: "noopener noreferrer" }
: undefined)}
>
{copy.footer.links[key]}
</a>
))}
+4 -1
View File
@@ -1,4 +1,4 @@
import { HERO_HIGHLIGHT_IDS, SITE_LINKS } from "../../data/siteContent";
import { HERO_HIGHLIGHT_IDS, SITE_LINKS, VK_URL } from "../../data/siteContent";
import { useLocale } from "../../i18n/LocaleProvider";
import { Button } from "../ui/Button";
import { HeroMockup } from "./HeroMockup";
@@ -28,6 +28,9 @@ export function Hero() {
<Button href={SITE_LINKS.pricing} variant="secondary">
{copy.cta.buyLicense}
</Button>
<Button href={VK_URL} variant="ghost" newTab>
{copy.cta.vk}
</Button>
</div>
<p className={styles.platforms}>{copy.hero.platformsLine}</p>
+7 -4
View File
@@ -1,6 +1,6 @@
import type { ReactNode } from "react";
import { useCrowdfundingModal } from "../../providers/CrowdfundingModalProvider";
import { CROWDFUNDING_URL } from "../../data/siteContent";
import { Button } from "./Button";
type BuyButtonVariant = "primary" | "secondary" | "ghost" | "gold";
@@ -16,10 +16,13 @@ export function BuyButton({
variant = "secondary",
className,
}: BuyButtonProps) {
const { openModal } = useCrowdfundingModal();
return (
<Button variant={variant} className={className} onClick={openModal}>
<Button
href={CROWDFUNDING_URL}
variant={variant}
className={className}
newTab
>
{children}
</Button>
);
+4 -1
View File
@@ -1,4 +1,7 @@
export const CROWDFUNDING_URL = "https://crowdrepublic.ru/" as const;
export const CROWDFUNDING_URL =
"https://crowdrepublic.ru/projects/1073012" as const;
export const VK_URL = "https://vk.com/ttrpgplayer" as const;
export const SITE_LINKS = {
features: "#features",
+4 -12
View File
@@ -28,6 +28,7 @@ const ru = {
downloadFree: "Скачать бесплатно",
buyLicense: "Поддержать проект",
downloadForOs: "Скачать для моей ОС",
vk: "ВК",
},
hero: {
eyebrow: "Инструмент для мастеров · Платформа для историй",
@@ -245,12 +246,6 @@ const ru = {
ctaBanner: {
title: "Готовы вести следующую сессию по-новому?",
},
crowdfundingModal: {
title: "Скоро старт краудфандинга",
messageBefore: "Скоро начнётся краудфандинговая кампания на ",
messageAfter: ", следите за новостями.",
close: "Понятно",
},
eulaModal: {
title: "Лицензионное соглашение",
localeNote: "",
@@ -264,6 +259,7 @@ const ru = {
faq: "FAQ",
enterKey: "Выбрать ключ",
support: "Поддержка",
vk: "ВКонтакте",
eula: "Лицензия",
},
},
@@ -302,6 +298,7 @@ const en: SiteCopy = {
downloadFree: "Download free",
buyLicense: "Support the project",
downloadForOs: "Download for my OS",
vk: "VK",
},
hero: {
eyebrow: "A tool for GMs · A platform for stories",
@@ -517,12 +514,6 @@ 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",
},
eulaModal: {
title: "End User License Agreement",
localeNote: "The binding legal text below is in Russian.",
@@ -536,6 +527,7 @@ const en: SiteCopy = {
faq: "FAQ",
enterKey: "Choose key",
support: "Support",
vk: "VK",
eula: "EULA",
},
},
+3 -6
View File
@@ -3,7 +3,6 @@ import { createRoot } from "react-dom/client";
import { App } from "./App";
import { LocaleProvider } from "./i18n/LocaleProvider";
import { CrowdfundingModalProvider } from "./providers/CrowdfundingModalProvider";
import { EulaModalProvider } from "./providers/EulaModalProvider";
import { ExchangeRateProvider } from "./providers/ExchangeRateProvider";
import "./styles/global.css";
@@ -17,11 +16,9 @@ createRoot(rootEl).render(
<StrictMode>
<LocaleProvider>
<ExchangeRateProvider>
<CrowdfundingModalProvider>
<EulaModalProvider>
<App />
</EulaModalProvider>
</CrowdfundingModalProvider>
<EulaModalProvider>
<App />
</EulaModalProvider>
</ExchangeRateProvider>
</LocaleProvider>
</StrictMode>,
@@ -1,72 +0,0 @@
.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);
}
-107
View File
@@ -1,107 +0,0 @@
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<CrowdfundingModalContextValue | null>(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 (
<CrowdfundingModalContext.Provider value={{ openModal }}>
{children}
{open ? (
<div
className={styles.overlay}
onClick={(event) => {
if (event.target === event.currentTarget) closeModal();
}}
role="presentation"
>
<div
className={styles.dialog}
role="dialog"
aria-modal="true"
aria-labelledby="crowdfunding-modal-title"
>
<h2 id="crowdfunding-modal-title" className={styles.title}>
{copy.crowdfundingModal.title}
</h2>
<p className={styles.message}>
{copy.crowdfundingModal.messageBefore}
<a
href={CROWDFUNDING_URL}
target="_blank"
rel="noopener noreferrer"
className={styles.link}
>
{CROWDFUNDING_URL}
</a>
{copy.crowdfundingModal.messageAfter}
</p>
<button
type="button"
className={styles.closeBtn}
onClick={closeModal}
>
{copy.crowdfundingModal.close}
</button>
</div>
</div>
) : null}
</CrowdfundingModalContext.Provider>
);
}
+4 -2
View File
@@ -14,6 +14,7 @@ import {
REQUIRED_SECTION_IDS,
SITE_LINKS,
UPDATE_FEED_BASE,
VK_URL,
} from "./data/siteContent";
import {
SCREENSHOT_SRC,
@@ -69,9 +70,10 @@ describe("siteContent", () => {
);
});
it("wires download anchor and CrowdRepublic crowdfunding url", () => {
it("wires download anchor, crowdfunding, and VK urls", () => {
assert.equal(SITE_LINKS.download, "#platforms");
assert.equal(CROWDFUNDING_URL, "https://crowdrepublic.ru/");
assert.equal(CROWDFUNDING_URL, "https://crowdrepublic.ru/projects/1073012");
assert.equal(VK_URL, "https://vk.com/ttrpgplayer");
});
it("maps each feature variant to a screenshot asset", () => {