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:
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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,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>
|
||||
</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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user