diff --git a/_data/pages/home.yml b/_data/pages/home.yml index 0a120d6df7..16c053a349 100644 --- a/_data/pages/home.yml +++ b/_data/pages/home.yml @@ -52,6 +52,9 @@ blocks: operates as a Validity-rollup (or ZK-rollup): it "bundles" many transactions and "rolls" them to Ethereum as a single transaction.' columns: "4" + - title: "Starknet newsletter. " + type: newsletter_popup + description: "Sign up to receive the latest news and updates from the Starknet ecosystem. " - type: flex_layout heading_variant: h3 base: 1 diff --git a/_data/seo/newsletter.yml b/_data/seo/newsletter.yml new file mode 100644 index 0000000000..9c95505f4d --- /dev/null +++ b/_data/seo/newsletter.yml @@ -0,0 +1,5 @@ +title: Starknet Newsletter ✨🗞️ +subtitle: Stay up to date with the latest news and updates from the Starknet ecosystem. +description: Sign up to receive the latest news and updates from the Starknet ecosystem. + We'll send you emails with the latest news, updates, and resources + from the Starknet ecosystem. diff --git a/public/robots.txt b/public/robots.txt index e4fe782829..d80444785f 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -7,3 +7,5 @@ Disallow: /fr/* Disallow: /ja/* Disallow: /ko/* Disallow: /pt/* +Disallow: /*/subscribe-newsletter +Disallow: /subscribe-newsletter diff --git a/workspaces/cms-config/src/blocks.ts b/workspaces/cms-config/src/blocks.ts index aac44961db..1a98203e15 100644 --- a/workspaces/cms-config/src/blocks.ts +++ b/workspaces/cms-config/src/blocks.ts @@ -721,8 +721,25 @@ export const blocks = [ fields: videoChapterFields } ] - }, - { + }, { + name: "newsletter_popup", + label: "Newsletter Popup", + widget: "object", + fields: [ + { + crowdin: true, + label: 'Title', + name: 'title', + widget: 'string' + }, + { + crowdin: true, + label: 'Description', + name: 'description', + widget: 'string' + }, + ] + }, { name: "ordered_block", label: "Ordered Block", widget: "object", diff --git a/workspaces/cms-config/src/collections/seo.ts b/workspaces/cms-config/src/collections/seo.ts index c62eee9de7..91e160d823 100644 --- a/workspaces/cms-config/src/collections/seo.ts +++ b/workspaces/cms-config/src/collections/seo.ts @@ -149,6 +149,31 @@ export const SEOCollectionConfig = { crowdin: true, }, ], + }, { + label: "Starknet newsletter", + name: "newsletter", + file: `_data/seo/newsletter.yml`, + crowdin: true, + fields: [ + { + label: "Title", + name: "title", + widget: "string", + crowdin: true, + }, + { + label: "Sub Title", + name: "subtitle", + widget: "string", + crowdin: true, + }, + { + label: "Description", + name: "description", + widget: "string", + crowdin: true, + } + ], }, ], } satisfies CmsCollection; diff --git a/workspaces/cms-data/src/pages.ts b/workspaces/cms-data/src/pages.ts index e075ded407..2dd60c14a1 100644 --- a/workspaces/cms-data/src/pages.ts +++ b/workspaces/cms-data/src/pages.ts @@ -160,6 +160,12 @@ export interface VideoSectionBlock { readonly 'eth-settlement': ChapterInfo; } +export interface NewsletterBlock { + readonly type: "newsletter_popup"; + readonly title: string; + readonly description: string; +} + export type HeadingVariant = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; export type Block = @@ -175,7 +181,8 @@ export type Block = | OrderedBlock | ListCardItemsBlock | AmbassadorsListBlock - | VideoSectionBlock; + | VideoSectionBlock + | NewsletterBlock; export interface Container { readonly type: "container"; diff --git a/workspaces/cms-data/src/seo/index.ts b/workspaces/cms-data/src/seo/index.ts index 2b0f0f39bb..cdc0b0f809 100644 --- a/workspaces/cms-data/src/seo/index.ts +++ b/workspaces/cms-data/src/seo/index.ts @@ -21,6 +21,11 @@ export interface SEOTexts { subtitle: string; description: string; }; + newsletter: { + title: string; + subtitle: string; + description: string; + }; search: { search: string; cancel: string; diff --git a/workspaces/cms-scripts/src/index.ts b/workspaces/cms-scripts/src/index.ts index 8c83c41c20..6f07c48d3a 100644 --- a/workspaces/cms-scripts/src/index.ts +++ b/workspaces/cms-scripts/src/index.ts @@ -87,6 +87,7 @@ const createSharedData = async () => { "jobs", "search", "language", + "newsletter" ]; for (const locale of locales) { @@ -160,6 +161,7 @@ const simpleFiles = [ await getSimpleFiles("seo", "footer", true), await getSimpleFiles("seo", "home", true), await getSimpleFiles("seo", "language", true), + await getSimpleFiles("seo", "newsletter", true), await getSimpleFiles("seo", "search", true), ]; @@ -278,4 +280,4 @@ const redirects = await yaml("_data/settings/redirects.yml"); await write(`workspaces/website/redirects.json`, redirects); await createRoadmapDetails() await createAnnouncementDetails() -await createSharedData() \ No newline at end of file +await createSharedData() diff --git a/workspaces/website/src/blocks/Block.tsx b/workspaces/website/src/blocks/Block.tsx index e2c915d567..636e3da7d3 100644 --- a/workspaces/website/src/blocks/Block.tsx +++ b/workspaces/website/src/blocks/Block.tsx @@ -19,13 +19,17 @@ import { useAsync } from "react-streaming"; import { usePageContext } from "src/renderer/PageContextProvider"; import { HeadingContainer } from "./HeadingContainer"; import VideoSectionBlock from "./VideoSectionBlock"; +import { NewsletterCard } from "@ui/Card/NewsletterCard"; interface Props { readonly block: TopLevelBlock; + env: { + CLOUDFLARE_RECAPTCHA_KEY: string; + } readonly locale: string; } -export function Block({ block, locale }: Props): JSX.Element | null { +export function Block({ block, env, locale }: Props): JSX.Element | null { if (block.type === "basic_card") { return ; } else if (block.type === "container") { @@ -33,6 +37,7 @@ export function Block({ block, locale }: Props): JSX.Element | null { {block.blocks.map((block, i) => ( ; + } else if (block.type === "newsletter_popup") { + return ; } else if (block.type === "markdown") { return ; } else if (block.type === "ambassadors_list") { @@ -66,6 +73,7 @@ export function Block({ block, locale }: Props): JSX.Element | null { > {block.blocks.map((block, i) => ( {block.blocks.map((block, i) => ( {block.blocks.map((block, i) => ( void; }; type titleVariantType = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; @@ -54,11 +55,12 @@ export const ImageIconCard = ({ withIllustration = false, variant = "image_icon_link_card", columns = 4, - orientation = "left" + orientation = "left", + onClick }: Props) => { - const { href = 'test', label } = getComputedLinkData(locale, link ?? { - custom_title: 'dsa', - custom_internal_link: 'asd' + const { href , label } = getComputedLinkData(locale, link ?? { + custom_title: '', + custom_internal_link: '' }); let titleVariant; let descriptionVariant; @@ -101,9 +103,11 @@ export const ImageIconCard = ({ descriptionVariant = size === "large" ? "body" : "cardBody"; linkVariant = size === "large" ? "cardLink" : "smallCardLink"; } + return ( {description}{" "} - {variant === "community_card" && ( + {variant === "community_card" && href !== '#' && ( { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + setIsOpen(true)} + orientation={'left'} + size={'large'} + title={title} + variant={'community_card'} + locale={locale} + withIllustration={false} + /> + + ); +}; diff --git a/workspaces/website/src/pages/(components)/CMSPage.tsx b/workspaces/website/src/pages/(components)/CMSPage.tsx index 2c46f905d9..48635175f4 100644 --- a/workspaces/website/src/pages/(components)/CMSPage.tsx +++ b/workspaces/website/src/pages/(components)/CMSPage.tsx @@ -17,17 +17,21 @@ import { blocksToTOC } from "./TableOfContents/blocksToTOC"; type CMSPageProps = { data: PageType; + env: { + CLOUDFLARE_RECAPTCHA_KEY: string; + } locale: string; }; export default function CMSPage({ data, + env, locale, }: CMSPageProps) { const date = data?.gitlog?.date; return ( {data.breadcrumbs && @@ -87,6 +91,7 @@ export default function CMSPage({ {data.blocks?.map((block, i) => { return ( (null); + const captchaRef = useRef(null); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setFormState('submitting'); + + try { + const token = captchaRef.current?.getResponse(); + + await axios.post(`/api/newsletter-subscribe?${qs.stringify({ + email: (event.target as any)[0].value, + token, + })}`) + + captchaRef.current?.reset(); + setFormState('success'); + } catch (error: any) { + setFormState(null); + captchaRef.current?.reset(); + + const toastErrorConfig = { + duration: 1500, + isClosable: true, + status: 'error' as 'error', + }; + + if(error.response.data?.title === 'Invalid Captcha') { + toast({ + description: 'We\'re having trouble verifying you\'re a human. Please try again.', + ...toastErrorConfig + }); + + return; + } + + if(error.response.data?.title === 'Member Exists') { + toast({ + description: 'You are already subscribed to the newsletter.', + ...toastErrorConfig + }); + + return; + } + + toast({ + description: 'There was an issue subscribing you to the newsletter.', + ...toastErrorConfig + }); + } + } + + return ( + <> + {!hideHeader && ( + <> + + Starknet Dev News ✨🗞️ + + + + Receive notifications on Starknet version updates and network status. + + + )} + + {formState !== 'success' ? ( +
+ + + Email + + + + + + + + + + ) : ( +
+ + Thank you and may the STARK be with you ✨🗞️ + +
+ )} + + ) +}; diff --git a/workspaces/website/src/pages/(components)/roadmap/RoadmapSubscribeForm.tsx b/workspaces/website/src/pages/(components)/roadmap/RoadmapSubscribeForm.tsx index 4be498e7c3..f4c69ad63a 100644 --- a/workspaces/website/src/pages/(components)/roadmap/RoadmapSubscribeForm.tsx +++ b/workspaces/website/src/pages/(components)/roadmap/RoadmapSubscribeForm.tsx @@ -2,25 +2,15 @@ * Module dependencies. */ -import { Button } from '@ui/Button'; import { - FormControl, - FormLabel, - Input, Modal, ModalOverlay, ModalContent, ModalCloseButton, Container, - useToast, } from '@chakra-ui/react'; -import { Heading } from '@ui/Typography/Heading'; -import { Text } from '@ui/Typography/Text'; -import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile' -import { useRef, useState } from 'react'; -import axios from 'axios'; -import qs from 'qs'; +import { NewsletterForm } from './NewsletterForm'; /** * `RoadmapSubscribeForm` props. @@ -43,63 +33,9 @@ function RoadmapSubscribeForm({ isOpen, setIsOpen, }: RoadmapSubscribeFormProps) { - const toast = useToast(); - const [formState, setFormState] = useState<'submitting' | 'success' | null>(null); - const captchaRef = useRef(null); - const handleClose = () => { setIsOpen(false); - setFormState(null); }; - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setFormState('submitting'); - - try { - const token = captchaRef.current?.getResponse(); - - await axios.post(`/api/newsletter-subscribe?${qs.stringify({ - email: (event.target as any)[0].value, - token, - })}`) - - captchaRef.current?.reset(); - setFormState('success'); - } catch (error: any) { - setFormState(null); - captchaRef.current?.reset(); - - const toastErrorConfig = { - duration: 1500, - isClosable: true, - status: 'error' as 'error', - }; - - if(error.response.data?.title === 'Invalid Captcha') { - toast({ - description: 'We\'re having trouble verifying you\'re a human. Please try again.', - ...toastErrorConfig - }); - - return; - } - - if(error.response.data?.title === 'Member Exists') { - toast({ - description: 'You are already subscribed to the newsletter.', - ...toastErrorConfig - }); - - return; - } - - toast({ - description: 'There was an issue subscribing you to the newsletter.', - ...toastErrorConfig - }); - } - } return ( @@ -114,67 +50,7 @@ function RoadmapSubscribeForm({ - {formState !== 'success' ? ( - <> - - Starknet Dev News ✨🗞️ - - - - Receive notifications on Starknet version updates and network status. - - -
- - - Email - - - - - - - - - - - ) : ( -
- - Thank you and may the STARK be with you ✨🗞️ - -
- )} +
diff --git a/workspaces/website/src/pages/PagePage.tsx b/workspaces/website/src/pages/PagePage.tsx index 6004b995c4..14009d5d62 100644 --- a/workspaces/website/src/pages/PagePage.tsx +++ b/workspaces/website/src/pages/PagePage.tsx @@ -4,16 +4,19 @@ import CMSPage from "./(components)/CMSPage"; export interface Props { readonly data: PageType; + env: { + CLOUDFLARE_RECAPTCHA_KEY: string; + } } -export default function Page({ - data, -}: Props): JSX.Element { +export default function Page(props: Props): JSX.Element { const { locale } = usePageContext(); + const { data, env } = props; return ( ); diff --git a/workspaces/website/src/pages/pages.page.server.tsx b/workspaces/website/src/pages/pages.page.server.tsx index cac605417a..5c0cc6fc79 100644 --- a/workspaces/website/src/pages/pages.page.server.tsx +++ b/workspaces/website/src/pages/pages.page.server.tsx @@ -8,10 +8,16 @@ export async function onBeforeRender(pageContext: PageContextServer) { const locale = pageContext.locale ?? defaultLocale; const slug = pageContext.routeParams["*"] || "home"; const data = await getPageBySlug(slug, locale, pageContext.context); + const env = { + CLOUDFLARE_RECAPTCHA_KEY: import.meta.env.VITE_CLOUDFLARE_RECAPTCHA_KEY, + }; return { pageContext: { - pageProps: { data } satisfies Props, + pageProps: { + data, + env + } satisfies Props, documentProps: { title: slug == "home" ? undefined : data.title, } satisfies DocumentProps, diff --git a/workspaces/website/src/pages/pages.page.tsx b/workspaces/website/src/pages/pages.page.tsx index c214a470d0..27fa859cb5 100644 --- a/workspaces/website/src/pages/pages.page.tsx +++ b/workspaces/website/src/pages/pages.page.tsx @@ -1,10 +1,10 @@ import PagePage, { Props } from "src/pages/PagePage"; import NotFound from "@ui/NotFound/NotFound"; -export function Page({ data }: Props): JSX.Element { +export function Page({ data, env }: Props): JSX.Element { if (data == null) { return ; } - return ; + return ; } diff --git a/workspaces/website/src/pages/subscribe-newsletter/NewsletterPage.tsx b/workspaces/website/src/pages/subscribe-newsletter/NewsletterPage.tsx new file mode 100644 index 0000000000..161536e5ad --- /dev/null +++ b/workspaces/website/src/pages/subscribe-newsletter/NewsletterPage.tsx @@ -0,0 +1,41 @@ + +/** + * Module dependencies. + */ + +import { Box } from "@chakra-ui/react"; +import { PageLayout } from "@ui/Layout/PageLayout"; +import { SEOTexts } from "@starknet-io/cms-data/src/seo"; +import { NewsletterForm } from "../(components)/roadmap/NewsletterForm"; + +/** + * `Props` type. + */ + +export interface Props { + readonly env: { + readonly CLOUDFLARE_RECAPTCHA_KEY: string; + }; + readonly seo: SEOTexts['newsletter']; +} + +/** + * Export `NewsletterPage` component. + */ + +export function NewsletterPage({ env, seo }: Props) { + return ( + + +
+ } + /> + ) +}; diff --git a/workspaces/website/src/pages/subscribe-newsletter/index.page.server.ts b/workspaces/website/src/pages/subscribe-newsletter/index.page.server.ts new file mode 100644 index 0000000000..f3e9647526 --- /dev/null +++ b/workspaces/website/src/pages/subscribe-newsletter/index.page.server.ts @@ -0,0 +1,33 @@ +/** + * Module dependencies + */ + +import { PageContextServer } from "src/renderer/types"; +import { Props } from "src/pages/subscribe-newsletter/NewsletterPage"; +import { getDefaultPageContext } from "src/renderer/helpers"; + +/** + * Export `onBeforeRender` function. + */ + +export async function onBeforeRender(pageContext: PageContextServer) { + const defaultPageContext = await getDefaultPageContext(pageContext); + const { locale } = defaultPageContext; + + const pageProps: Props = { + seo: defaultPageContext.seo.newsletter, + env: { + CLOUDFLARE_RECAPTCHA_KEY: import.meta.env.VITE_CLOUDFLARE_RECAPTCHA_KEY + }, + params: { + locale, + }, + }; + + return { + pageContext: { + ...defaultPageContext, + pageProps, + }, + }; +} diff --git a/workspaces/website/src/pages/subscribe-newsletter/index.page.tsx b/workspaces/website/src/pages/subscribe-newsletter/index.page.tsx new file mode 100644 index 0000000000..ecceb592dd --- /dev/null +++ b/workspaces/website/src/pages/subscribe-newsletter/index.page.tsx @@ -0,0 +1,20 @@ +/** + * Module dependencies. + */ + +import { DocumentProps } from "src/renderer/types"; + +/** + * Export `Page` component + */ + +export { NewsletterPage as Page } from "src/pages/subscribe-newsletter/NewsletterPage"; + +/** + * Export `documentProps`. + */ + +export const documentProps = { + title: "Starknet Newsletter", + description: "Subscribe to the Starknet newsletter to stay up to date with the latest news and developments from the Starknet team." +} satisfies DocumentProps