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 <cursoragent@cursor.com>
This commit is contained in:
@@ -34,3 +34,11 @@
|
|||||||
.link:hover {
|
.link:hover {
|
||||||
color: var(--color-gold);
|
color: var(--color-gold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.linkButton {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SITE_LINKS } from "../../data/siteContent";
|
import { SITE_LINKS } from "../../data/siteContent";
|
||||||
import { useLocale } from "../../i18n/LocaleProvider";
|
import { useLocale } from "../../i18n/LocaleProvider";
|
||||||
|
import { useCrowdfundingModal } from "../../providers/CrowdfundingModalProvider";
|
||||||
|
|
||||||
import styles from "./Footer.module.css";
|
import styles from "./Footer.module.css";
|
||||||
|
|
||||||
@@ -11,9 +12,11 @@ const FOOTER_LINK_KEYS = [
|
|||||||
"support",
|
"support",
|
||||||
] as const;
|
] 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,
|
download: SITE_LINKS.download,
|
||||||
buy: SITE_LINKS.buy,
|
|
||||||
faq: SITE_LINKS.faq,
|
faq: SITE_LINKS.faq,
|
||||||
enterKey: SITE_LINKS.enterKey,
|
enterKey: SITE_LINKS.enterKey,
|
||||||
support: SITE_LINKS.support,
|
support: SITE_LINKS.support,
|
||||||
@@ -21,24 +24,29 @@ const FOOTER_HREFS: Record<(typeof FOOTER_LINK_KEYS)[number], string> = {
|
|||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const { copy } = useLocale();
|
const { copy } = useLocale();
|
||||||
|
const { openModal } = useCrowdfundingModal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
<div className={styles.inner}>
|
<div className={styles.inner}>
|
||||||
<p className={styles.copy}>© TTRPGPlayer</p>
|
<p className={styles.copy}>© TTRPGPlayer</p>
|
||||||
<nav className={styles.links} aria-label={copy.footer.aria}>
|
<nav className={styles.links} aria-label={copy.footer.aria}>
|
||||||
{FOOTER_LINK_KEYS.map((key) => (
|
{FOOTER_LINK_KEYS.map((key) =>
|
||||||
<a
|
key === "buy" ? (
|
||||||
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
className={styles.link}
|
type="button"
|
||||||
href={FOOTER_HREFS[key]}
|
className={`${styles.link} ${styles.linkButton}`}
|
||||||
{...(key === "buy"
|
onClick={openModal}
|
||||||
? { target: "_blank", rel: "noopener noreferrer" }
|
|
||||||
: undefined)}
|
|
||||||
>
|
>
|
||||||
{copy.footer.links[key]}
|
{copy.footer.links[key]}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a key={key} className={styles.link} href={FOOTER_HREFS[key]}>
|
||||||
|
{copy.footer.links[key]}
|
||||||
</a>
|
</a>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NAV_HREFS, NAV_LINK_KEYS, SITE_LINKS } from "../../data/siteContent";
|
import { NAV_HREFS, NAV_LINK_KEYS, SITE_LINKS } from "../../data/siteContent";
|
||||||
import { useLocale } from "../../i18n/LocaleProvider";
|
import { useLocale } from "../../i18n/LocaleProvider";
|
||||||
import { Button } from "../ui/Button";
|
import { Button } from "../ui/Button";
|
||||||
|
import { BuyButton } from "../ui/BuyButton";
|
||||||
import { LanguageSwitcher } from "./LanguageSwitcher";
|
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||||
|
|
||||||
import styles from "./Header.module.css";
|
import styles from "./Header.module.css";
|
||||||
@@ -39,14 +40,9 @@ export function Header() {
|
|||||||
>
|
>
|
||||||
{copy.cta.download}
|
{copy.cta.download}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<BuyButton variant="secondary" className={styles.actionBtn}>
|
||||||
href={SITE_LINKS.buy}
|
|
||||||
variant="secondary"
|
|
||||||
className={styles.actionBtn}
|
|
||||||
newTab
|
|
||||||
>
|
|
||||||
{copy.cta.buy}
|
{copy.cta.buy}
|
||||||
</Button>
|
</BuyButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SITE_LINKS } from "../../data/siteContent";
|
import { SITE_LINKS } from "../../data/siteContent";
|
||||||
import { useLocale } from "../../i18n/LocaleProvider";
|
import { useLocale } from "../../i18n/LocaleProvider";
|
||||||
import { Button } from "../ui/Button";
|
import { Button } from "../ui/Button";
|
||||||
|
import { BuyButton } from "../ui/BuyButton";
|
||||||
|
|
||||||
import styles from "./CtaBanner.module.css";
|
import styles from "./CtaBanner.module.css";
|
||||||
|
|
||||||
@@ -13,9 +14,7 @@ export function CtaBanner() {
|
|||||||
<h2 className={styles.title}>{copy.ctaBanner.title}</h2>
|
<h2 className={styles.title}>{copy.ctaBanner.title}</h2>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Button href={SITE_LINKS.download}>{copy.cta.download}</Button>
|
<Button href={SITE_LINKS.download}>{copy.cta.download}</Button>
|
||||||
<Button href={SITE_LINKS.buy} variant="secondary" newTab>
|
<BuyButton variant="secondary">{copy.cta.buyLicense}</BuyButton>
|
||||||
{copy.cta.buyLicense}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HERO_HIGHLIGHT_IDS, SITE_LINKS } from "../../data/siteContent";
|
import { HERO_HIGHLIGHT_IDS, SITE_LINKS } from "../../data/siteContent";
|
||||||
import { useLocale } from "../../i18n/LocaleProvider";
|
import { useLocale } from "../../i18n/LocaleProvider";
|
||||||
import { Button } from "../ui/Button";
|
import { Button } from "../ui/Button";
|
||||||
|
import { BuyButton } from "../ui/BuyButton";
|
||||||
import { HeroMockup } from "./HeroMockup";
|
import { HeroMockup } from "./HeroMockup";
|
||||||
|
|
||||||
import styles from "./Hero.module.css";
|
import styles from "./Hero.module.css";
|
||||||
@@ -25,9 +26,7 @@ export function Hero() {
|
|||||||
|
|
||||||
<div className={styles.cta}>
|
<div className={styles.cta}>
|
||||||
<Button href={SITE_LINKS.download}>{copy.cta.downloadFree}</Button>
|
<Button href={SITE_LINKS.download}>{copy.cta.downloadFree}</Button>
|
||||||
<Button href={SITE_LINKS.buy} variant="secondary" newTab>
|
<BuyButton variant="secondary">{copy.cta.buyLicense}</BuyButton>
|
||||||
{copy.cta.buyLicense}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className={styles.platforms}>{copy.hero.platformsLine}</p>
|
<p className={styles.platforms}>{copy.hero.platformsLine}</p>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
PRICING_TIER_IDS,
|
PRICING_TIER_IDS,
|
||||||
RECOMMENDED_PRICING_TIER,
|
RECOMMENDED_PRICING_TIER,
|
||||||
SITE_LINKS,
|
|
||||||
} from "../../data/siteContent";
|
} from "../../data/siteContent";
|
||||||
import { PRICING_TIER_PRICES_RUB } from "../../data/pricing";
|
import { PRICING_TIER_PRICES_RUB } from "../../data/pricing";
|
||||||
import { useLocale } from "../../i18n/LocaleProvider";
|
import { useLocale } from "../../i18n/LocaleProvider";
|
||||||
import { formatRubPrice, formatUsdPrice } from "../../lib/exchangeRate";
|
import { formatRubPrice, formatUsdPrice } from "../../lib/exchangeRate";
|
||||||
import { useExchangeRate } from "../../providers/ExchangeRateProvider";
|
import { useExchangeRate } from "../../providers/ExchangeRateProvider";
|
||||||
import { Section } from "../layout/Section";
|
import { Section } from "../layout/Section";
|
||||||
import { Button } from "../ui/Button";
|
import { BuyButton } from "../ui/BuyButton";
|
||||||
|
|
||||||
import styles from "./Pricing.module.css";
|
import styles from "./Pricing.module.css";
|
||||||
|
|
||||||
@@ -61,13 +60,9 @@ export function Pricing() {
|
|||||||
<li key={feature}>{feature}</li>
|
<li key={feature}>{feature}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<Button
|
<BuyButton variant={recommended ? "gold" : "secondary"}>
|
||||||
href={SITE_LINKS.buy}
|
|
||||||
variant={recommended ? "gold" : "secondary"}
|
|
||||||
newTab
|
|
||||||
>
|
|
||||||
{copy.cta.buy}
|
{copy.cta.buy}
|
||||||
</Button>
|
</BuyButton>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Button variant={variant} className={className} onClick={openModal}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export const CROWDFUNDING_URL = "https://crowdrepublic.ru/" as const;
|
||||||
|
|
||||||
export const SITE_LINKS = {
|
export const SITE_LINKS = {
|
||||||
features: "#features",
|
features: "#features",
|
||||||
howItWorks: "#how-it-works",
|
howItWorks: "#how-it-works",
|
||||||
@@ -5,7 +7,7 @@ export const SITE_LINKS = {
|
|||||||
pricing: "#pricing",
|
pricing: "#pricing",
|
||||||
faq: "#faq",
|
faq: "#faq",
|
||||||
download: "#platforms",
|
download: "#platforms",
|
||||||
buy: "https://donate.stream/ttrpgplayer",
|
// buy: "https://donate.stream/ttrpgplayer",
|
||||||
enterKey: "#pricing",
|
enterKey: "#pricing",
|
||||||
support: "mailto:player.ttrpg@gmail.com",
|
support: "mailto:player.ttrpg@gmail.com",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -245,6 +245,12 @@ const ru = {
|
|||||||
ctaBanner: {
|
ctaBanner: {
|
||||||
title: "Готовы вести следующую сессию по-новому?",
|
title: "Готовы вести следующую сессию по-новому?",
|
||||||
},
|
},
|
||||||
|
crowdfundingModal: {
|
||||||
|
title: "Скоро старт краудфандинга",
|
||||||
|
messageBefore: "Скоро начнётся краудфандинговая кампания на ",
|
||||||
|
messageAfter: ", следите за новостями.",
|
||||||
|
close: "Понятно",
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
aria: "Подвал",
|
aria: "Подвал",
|
||||||
links: {
|
links: {
|
||||||
@@ -505,6 +511,12 @@ const en: SiteCopy = {
|
|||||||
ctaBanner: {
|
ctaBanner: {
|
||||||
title: "Ready to run your next session differently?",
|
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: {
|
footer: {
|
||||||
aria: "Footer",
|
aria: "Footer",
|
||||||
links: {
|
links: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
|
|||||||
|
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
import { LocaleProvider } from "./i18n/LocaleProvider";
|
import { LocaleProvider } from "./i18n/LocaleProvider";
|
||||||
|
import { CrowdfundingModalProvider } from "./providers/CrowdfundingModalProvider";
|
||||||
import { ExchangeRateProvider } from "./providers/ExchangeRateProvider";
|
import { ExchangeRateProvider } from "./providers/ExchangeRateProvider";
|
||||||
import "./styles/global.css";
|
import "./styles/global.css";
|
||||||
|
|
||||||
@@ -15,7 +16,9 @@ createRoot(rootEl).render(
|
|||||||
<StrictMode>
|
<StrictMode>
|
||||||
<LocaleProvider>
|
<LocaleProvider>
|
||||||
<ExchangeRateProvider>
|
<ExchangeRateProvider>
|
||||||
|
<CrowdfundingModalProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</CrowdfundingModalProvider>
|
||||||
</ExchangeRateProvider>
|
</ExchangeRateProvider>
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CROWDFUNDING_URL,
|
||||||
DOWNLOAD_ARTIFACTS,
|
DOWNLOAD_ARTIFACTS,
|
||||||
DOWNLOAD_LINKS,
|
DOWNLOAD_LINKS,
|
||||||
FAQ_IDS,
|
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.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", () => {
|
it("maps each feature variant to a screenshot asset", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user