Merge pull request #31 from yougotwill/email_subscription

Email subscription
This commit is contained in:
William Grant 2021-09-08 11:24:30 +02:00 committed by GitHub
commit a271a1bf87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 206 additions and 19 deletions

View File

@ -1,5 +1,5 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import React, { LegacyRef, useContext } from 'react';
import { ScreenContext } from '../contexts/screen';
export interface Props {
@ -9,6 +9,8 @@ export interface Props {
disabled?: boolean;
selected?: boolean;
buttonType?: 'submit';
reference?: LegacyRef<HTMLButtonElement>;
onClick?(): any;
children?: string;
className?: string;
@ -27,6 +29,8 @@ export function Button(props: Props) {
type = 'solid',
disabled = false,
selected = false,
buttonType,
reference,
onClick,
children,
className,
@ -90,7 +94,7 @@ export function Button(props: Props) {
'';
return (
<div
<button
className={classNames(
'flex',
'justify-center',
@ -111,8 +115,9 @@ export function Button(props: Props) {
type !== 'text' && ['border-2', 'border-solid', `border-${color}`],
className,
)}
role="button"
tabIndex={-1}
type={buttonType}
ref={reference}
onClick={onClickFn}
>
{prefix && (
@ -126,6 +131,6 @@ export function Button(props: Props) {
{suffix}
</div>
)}
</div>
</button>
);
}

View File

@ -3,12 +3,14 @@ import React, { ReactNode } from 'react';
import { UI } from '../constants';
interface Props {
id?: string;
backgroundColor?: 'primary' | 'secondary' | 'secondary-1';
classes?: string;
children: ReactNode;
}
export function Contained(props: Props) {
const { backgroundColor, children } = props;
const { id, backgroundColor, classes, children } = props;
const containerStyle = {
paddingLeft: `${UI.PAGE_CONTAINED_PADDING_VW}vw`,
@ -20,9 +22,11 @@ export function Contained(props: Props) {
return (
<div
id={id}
className={classNames(
'w-full',
backgroundColor && `bg-${backgroundColor}`,
classes,
)}
>
<div className="relative" style={containerStyle}>

100
components/EmailSignup.tsx Normal file
View File

@ -0,0 +1,100 @@
import { ReactElement, useState, useRef, FormEventHandler } from 'react';
import { useRouter } from 'next/router';
import classNames from 'classnames';
import { Contained } from './Contained';
import { Input } from './Input';
import { Button } from './Button';
export default function EmailSignup(): ReactElement {
const router = useRouter();
const buttonRef = useRef<HTMLButtonElement>(null);
const setButtonText = (value: string) => {
if (null !== buttonRef.current) {
buttonRef.current.innerText = value;
}
};
const [email, setEmail] = useState('');
const handleSubscription: FormEventHandler = async event => {
event.preventDefault();
setButtonText('Subscribing...');
let response;
try {
response = await fetch('/api/email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
} catch (error) {
response = error;
}
switch (response?.status) {
case 201:
setEmail('');
setButtonText('Signed up ✓');
break;
case 400:
default:
setButtonText('Signup failed ✗');
break;
}
};
return (
<Contained
id="signup"
classes={classNames(
'border-2 border-solid border-primary py-6 px-2 mt-6 mb-10',
'tablet:w-4/5 tablet:mx-auto tablet:py-4 tablet:mt-6 tablet:mb-8',
'desktop:py-6',
router.asPath !== '/get-involved' && 'tablet:w-full',
)}
>
<h3
className={classNames(
'text-2xl font-semibold leading-none mb-2',
'tablet:text-3xl',
'desktop:text-4xl desktop:mb-3',
)}
>
You've got mail!
</h3>
<p
className={classNames(
'leading-none mb-6',
'tablet:mb-3 tablet:leading-tight',
'desktop:mb-6 desktop:text-xl',
)}
>
Sign up to our newsletter to keep up to date with everything Oxen.
</p>
<form onSubmit={handleSubscription}>
<Input
type="email"
placeholder="Your Email"
value={email}
onValueChange={value => setEmail(value)}
size={'large'}
border={'primary'}
inputMode={'text'}
className={classNames(
'mb-6 rounded-sm',
'tablet:mb-4',
'desktop:mb-6',
)}
required
/>
<Button
color="primary"
size="medium"
className={classNames('mx-auto', 'tablet:w-40')}
buttonType={'submit'}
reference={buttonRef}
>
Sign up
</Button>
</form>
</Contained>
);
}

View File

@ -54,8 +54,8 @@ export interface InputProps {
// HTMLInputElement Props
autofocus?: boolean;
// required?: boolean;
// autofocus?: boolean;
required?: boolean;
// validity?: ValidityState;
// validationMessage?: string;
// willValidate?: boolean;
@ -83,7 +83,7 @@ export function Input(props: InputProps) {
prefix,
duration = true,
suffix,
autofocus,
required,
disabled,
min,
max,
@ -171,8 +171,6 @@ export function Input(props: InputProps) {
// 'bg-white',
'text-gray-700',
'leading-tight',
'outline-black',
'outline-secondary',
'focus:outline-black',
border !== 'none' && 'border-2',
size === 'small' ? 'px-2' : 'px-4',
@ -211,6 +209,7 @@ export function Input(props: InputProps) {
ref={inputRef}
spellCheck={false}
disabled={disabled}
required={required}
placeholder={placeholder}
value={props.value ?? value}
step={step}

View File

@ -31,6 +31,7 @@ const CMS = {
TRADE_LINKS: /^{{[\s*]trade_links[\s*]}}$/,
CTA_WHO_USES_OXEN: /^\{\{[\s]*who_uses_oxen[\s]*\}\}$/,
CTA_SESSION_LOKINET: /^\{\{[\s]*session_lokinet[\s]*\}\}$/,
CTA_EMAIL_SIGNUP: /^\{\{[\s]*email_signup[\s]*\}\}$/,
},
BLOG_RESULTS_PER_PAGE: 13,
BLOG_RESULTS_PER_PAGE_TAGGED: 12,

View File

@ -25,7 +25,8 @@
"D": "^1.0.0",
"babel-plugin-tailwind": "^0.1.10",
"babel-preset-env": "^1.7.0",
"classnames": "^2.2.6",
"base-64": "^1.0.0",
"classnames": "^2.3.1",
"contentful": "^8.4.2",
"date-fns": "^2.23.0",
"dotenv": "^8.2.0",
@ -64,6 +65,8 @@
"@babel/preset-env": "^7.12.17",
"@babel/preset-react": "^7.12.13",
"@contentful/rich-text-types": "^15.0.0",
"@tailwindcss/forms": "^0.3.3",
"@types/base-64": "^1.0.0",
"@types/lodash.get": "^4.4.6",
"@types/lodash.set": "^4.3.6",
"@types/node": "^14.14.28",

42
pages/api/email.ts Normal file
View File

@ -0,0 +1,42 @@
import { NextApiRequest, NextApiResponse } from 'next';
import base64 from 'base-64';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== 'POST') {
res.status(400).json({
message: 'Email API: Invalid http method. | Only POST is accepted.',
});
}
const email = req.body.email;
const response = await fetch(
`https://api.createsend.com/api/v3.2/subscribers/${process.env.CAMPAIGN_MONITOR_LIST_API_ID}.json`,
{
body: JSON.stringify({
EmailAddress: email,
ConsentToTrack: 'Unchanged',
}),
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${base64.encode(
`${process.env.CAMPAIGN_MONITOR_API_KEY}:x`,
)}`,
},
method: 'POST',
},
);
if (response.status === 201) {
// console.log(`Email API: ${email} subscribed!`);
res.status(201).json({ email });
} else {
// const result = await response.json();
// console.warn(
// `Email API: | Code: ${result.Code} | Email: ${email} | ${result.Message}`
// );
res.status(400).json({ email });
}
}

View File

@ -8,6 +8,7 @@ import DiscordSVG from '../assets/svgs/socials/brand-discord.svg';
import RedditSVG from '../assets/svgs/socials/brand-reddit.svg';
import TelegramSVG from '../assets/svgs/socials/brand-telegram.svg';
import { Button } from '../components/Button';
import EmailSignup from '../components/EmailSignup';
import { CMS } from '../constants';
import { SideMenuItem, TPages } from '../state/navigation';
import {
@ -187,10 +188,12 @@ export class CmsApi {
public async fetchPageById(id: SideMenuItem): Promise<ISplitPage> {
return this.client
.getEntries({
content_type: 'splitPage',
'fields.id[in]': id,
})
.getEntries(
loadOptions({
content_type: 'splitPage',
'fields.id[in]': id,
}),
)
.then(entries => {
if (entries && entries.items && entries.items.length > 0) {
return this.convertPage(entries.items[0]);
@ -478,6 +481,11 @@ export const renderShortcode = (shortcode: string) => {
);
}
// Call to Action -> Email Signup
if (CMS.SHORTCODES.CTA_EMAIL_SIGNUP.test(shortcode)) {
return <EmailSignup />;
}
// All shortcode buttons with simple hrefs
const shortcodeButton = Object.values(CMS.SHORTCODE_BUTTONS).find(item =>
item.regex.test(shortcode),

View File

@ -72,12 +72,10 @@ function EmbeddedMedia(node: any, isInline = false): ReactElement {
const imageWidth = node.width ?? media.file.details.image.width;
const imageHeight = node.height ?? media.file.details.image.height;
const figureClasses = [
isInline && node.position && 'mx-auto mb-5',
isInline && node.position && 'text-center mx-auto mb-5',
isInline && !node.position && 'inline-block align-middle mx-1',
isInline && node.position === 'left' && 'tablet:float-left tablet:mr-4',
isInline &&
node.position === 'right' &&
'tablet:float-right tablet:ml-4',
isInline && node.position === 'right' && 'tablet:float-right tablet:ml-4',
!isInline && 'text-center mb-5',
];
const captionClasses = [

View File

@ -2105,6 +2105,13 @@
resolved "https://registry.yarnpkg.com/@tailwindcss/aspect-ratio/-/aspect-ratio-0.2.0.tgz#bebd32b7d0756b695294d4db1ae658796ff72a2c"
integrity sha512-v5LyHkwXj/4lI74B06zUrmWEdmSqS43+jw717pkt3fAXqb7ALwu77A8t7j+Bej+ZbdlIIqNMYheGN7wSGV1A6w==
"@tailwindcss/forms@^0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.3.3.tgz#a29d22668804f3dae293dcadbef1aa6315c45b64"
integrity sha512-U8Fi/gq4mSuaLyLtFISwuDYzPB73YzgozjxOIHsK6NXgg/IWD1FLaHbFlWmurAMyy98O+ao74ksdQefsquBV1Q==
dependencies:
mini-svg-data-uri "^1.2.3"
"@types/anymatch@*":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
@ -2143,6 +2150,11 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/base-64@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/base-64/-/base-64-1.0.0.tgz#de9c6070ea457fbd65a1b5ebf13976b3ac0bdad0"
integrity sha512-AvCJx/HrfYHmOQRFdVvgKMplXfzTUizmh0tz9GFTpDePWgCY4uoKll84zKlaRoeiYiCr7c9ZnqSTzkl0BUVD6g==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -3891,6 +3903,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base-64@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a"
integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -4679,6 +4696,11 @@ classnames@2.2.6, classnames@^2.2.6:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
classnames@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-css@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@ -10654,6 +10676,11 @@ mini-css-extract-plugin@0.11.3:
schema-utils "^1.0.0"
webpack-sources "^1.1.0"
mini-svg-data-uri@^1.2.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.3.3.tgz#91d2c09f45e056e5e1043340b8b37ba7b50f4fac"
integrity sha512-+fA2oRcR1dJI/7ITmeQJDrYWks0wodlOz0pAEhKYJ2IVc1z0AnwJUsKY2fzFmPAM3Jo9J0rBx8JAA9QQSJ5PuA==
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"