click to action on landing page

This commit is contained in:
m5r 2021-08-28 05:09:45 +08:00
parent e7c69a4d7a
commit 1e89e57145
9 changed files with 140 additions and 190 deletions

View File

@ -1,4 +0,0 @@
export type ApiError = {
statusCode: number;
errorMessage: string;
};

View File

@ -1,56 +0,0 @@
import type { BlitzApiRequest, BlitzApiResponse } from "blitz";
import zod from "zod";
import type { ApiError } from "../_types";
import appLogger from "../../../integrations/logger";
import { addSubscriber } from "./_mailchimp";
type Response = {} | ApiError;
const logger = appLogger.child({ route: "/api/newsletter/subscribe" });
const bodySchema = zod.object({
email: zod.string().email(),
});
export default async function subscribeToNewsletter(req: BlitzApiRequest, res: BlitzApiResponse<Response>) {
if (req.method !== "POST") {
const statusCode = 405;
const apiError: ApiError = {
statusCode,
errorMessage: `Method ${req.method} Not Allowed`,
};
logger.error(apiError);
res.setHeader("Allow", ["POST"]);
res.status(statusCode).send(apiError);
return;
}
let body;
try {
body = bodySchema.parse(req.body);
} catch (error: any) {
const statusCode = 400;
const apiError: ApiError = {
statusCode,
errorMessage: "Body is malformed",
};
logger.error(error);
res.status(statusCode).send(apiError);
return;
}
try {
await addSubscriber(body.email);
} catch (error: any) {
console.log("error", error.response?.data);
if (error.response?.data.title !== "Member Exists") {
return res.status(error.response?.status ?? 400).end();
}
}
res.status(200).end();
}

View File

@ -0,0 +1,46 @@
import { useMutation } from "blitz";
import { useForm } from "react-hook-form";
import joinWaitlist from "../mutations/join-waitlist";
type Form = {
email: string;
};
export default function CTAForm() {
const [joinWaitlistMutation] = useMutation(joinWaitlist);
const {
handleSubmit,
register,
formState: { isSubmitted },
} = useForm<Form>();
const onSubmit = handleSubmit(async ({ email }) => {
if (isSubmitted) {
return;
}
return joinWaitlistMutation({ email });
});
return (
<form onSubmit={onSubmit} className="mt-8">
{isSubmitted ? (
<p className="text-center md:text-left mt-2 opacity-75 text-green-900 text-md">
You&#39;re on the list! We will be in touch soon
</p>
) : (
<div className="flex flex-col sm:flex-row justify-center max-w-sm mx-auto sm:max-w-md md:mx-0">
<input
{...register("email")}
type="email"
className="form-input w-full mb-2 sm:mb-0 sm:mr-2 focus:outline-none focus:ring-primary-500 focus:border-primary-500"
placeholder="Enter your email address"
/>
<button type="submit" className="btn text-white bg-primary-500 hover:bg-primary-400 flex-shrink-0">
Request access
</button>
</div>
)}
</form>
);
}

View File

@ -0,0 +1,26 @@
import mockupImage from "../images/mockup-image-01.png";
import iphoneMockup from "../images/iphone-mockup.png";
export default function PhoneMockup() {
return (
<div className="md:col-span-5 lg:col-span-5 text-center md:text-right">
<div className="inline-flex relative justify-center items-center">
<img
className="absolute max-w-[84.33%]"
src={mockupImage.src}
width={290}
height={624}
alt="Features illustration"
/>
<img
className="relative max-w-full mx-auto md:mr-0 md:max-w-none h-auto pointer-events-none"
src={iphoneMockup.src}
width={344}
height={674}
alt="iPhone mockup"
aria-hidden="true"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,35 @@
import { XIcon } from "@heroicons/react/outline";
export default function ReferralBanner() {
const isDisabled = true;
if (isDisabled) {
return null;
}
return (
<div className="relative bg-primary-600">
<div className="max-w-7xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
<div className="pr-16 sm:text-center sm:px-16">
<p className="font-medium text-white">
<span>&#127881; New: Get one month free for every friend that joins and subscribe!</span>
<span className="block sm:ml-2 sm:inline-block">
<a href="#" className="text-white font-bold underline">
{" "}
Learn more <span aria-hidden="true">&rarr;</span>
</a>
</span>
</p>
</div>
<div className="absolute inset-y-0 right-0 pt-1 pr-1 flex items-start sm:pt-1 sm:pr-2 sm:items-start">
<button
type="button"
className="flex p-2 rounded-md hover:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-white"
>
<span className="sr-only">Dismiss</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
import { resolver } from "blitz";
import { z } from "zod";
import appLogger from "../../../integrations/logger";
import { addSubscriber } from "../../../integrations/mailchimp";
const logger = appLogger.child({ mutation: "join-waitlist" });
const bodySchema = z.object({
email: z.string().email(),
});
export default resolver.pipe(resolver.zod(bodySchema), async ({ email }, ctx) => {
try {
await addSubscriber(email);
} catch (error: any) {
logger.error(error.response?.data);
if (error.response?.data.title !== "Member Exists") {
throw error;
}
}
});

View File

@ -1,12 +1,11 @@
import type { BlitzPage } from "blitz"; import type { BlitzPage } from "blitz";
import { Head } from "blitz"; import { Head } from "blitz";
import { XIcon } from "@heroicons/react/outline";
import Header from "../components/header"; import Header from "../components/header";
import iphoneMockup from "../images/iphone-mockup.png";
import mockupImage from "../images/mockup-image-01.png";
import Checkmark from "../components/checkmark"; import Checkmark from "../components/checkmark";
import CTAForm from "../components/cta-form";
import PhoneMockup from "../components/phone-mockup";
import ReferralBanner from "../components/referral-banner";
const LandingPage: BlitzPage = () => { const LandingPage: BlitzPage = () => {
return ( return (
@ -30,9 +29,7 @@ const LandingPage: BlitzPage = () => {
<section> <section>
<div className="max-w-6xl mx-auto px-4 sm:px-6"> <div className="max-w-6xl mx-auto px-4 sm:px-6">
<div className="pt-32 pb-10 md:pt-34 md:pb-16"> <div className="pt-32 pb-10 md:pt-34 md:pb-16">
{/* Hero content */}
<div className="md:grid md:grid-cols-12 md:gap-12 lg:gap-20 items-center"> <div className="md:grid md:grid-cols-12 md:gap-12 lg:gap-20 items-center">
{/* Content */}
<div className="md:col-span-7 lg:col-span-7 mb-8 md:mb-0 text-center md:text-left"> <div className="md:col-span-7 lg:col-span-7 mb-8 md:mb-0 text-center md:text-left">
<h1 className="h1 lg:text-5xl mb-4 font-extrabold font-mackinac"> <h1 className="h1 lg:text-5xl mb-4 font-extrabold font-mackinac">
<strong className="bg-gradient-to-br from-primary-500 to-indigo-600 bg-clip-text decoration-clone text-transparent"> <strong className="bg-gradient-to-br from-primary-500 to-indigo-600 bg-clip-text decoration-clone text-transparent">
@ -45,24 +42,7 @@ const LandingPage: BlitzPage = () => {
Coming soon! &#128026; Keep your phone number and pay less for your Coming soon! &#128026; Keep your phone number and pay less for your
communications, even abroad. communications, even abroad.
</p> </p>
{/* CTA form */} <CTAForm />
<form className="mt-8">
<div className="flex flex-col sm:flex-row justify-center max-w-sm mx-auto sm:max-w-md md:mx-0">
<input
type="email"
className="form-input w-full mb-2 sm:mb-0 sm:mr-2"
placeholder="Enter your email address"
/>
<a
className="btn text-white bg-primary-500 hover:bg-primary-400 flex-shrink-0"
href="#0"
>
Request access
</a>
</div>
{/* Success message */}
{/* <p className="text-center md:text-left mt-2 opacity-75 text-sm">Thanks for subscribing!</p> */}
</form>
<ul className="max-w-sm sm:max-w-md mx-auto md:max-w-none text-gray-600 mt-8 -mb-2"> <ul className="max-w-sm sm:max-w-md mx-auto md:max-w-none text-gray-600 mt-8 -mb-2">
<li className="flex items-center mb-2"> <li className="flex items-center mb-2">
<Checkmark /> <Checkmark />
@ -79,77 +59,7 @@ const LandingPage: BlitzPage = () => {
</ul> </ul>
</div> </div>
{/* Mobile mockup */} <PhoneMockup />
<div className="md:col-span-5 lg:col-span-5 text-center md:text-right">
<div className="inline-flex relative justify-center items-center">
{/* Glow illustration */}
<svg
className="absolute mr-12 mt-32 pointer-events-none -z-1"
aria-hidden="true"
width="678"
height="634"
viewBox="0 0 678 634"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="240"
cy="394"
r="240"
fill="url(#piphoneill_paint0_radial)"
fillOpacity=".4"
/>
<circle
cx="438"
cy="240"
r="240"
fill="url(#piphoneill_paint1_radial)"
fillOpacity=".6"
/>
<defs>
<radialGradient
id="piphoneill_paint0_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(90 -77 317) scale(189.054)"
>
<stop stopColor="#667EEA" />
<stop offset="1" stopColor="#667EEA" stopOpacity=".01" />
</radialGradient>
<radialGradient
id="piphoneill_paint1_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(90 99 339) scale(189.054)"
>
<stop stopColor="#9F7AEA" />
<stop offset="1" stopColor="#9F7AEA" stopOpacity=".01" />
</radialGradient>
</defs>
</svg>
{/* Image inside mockup size: 290x624px (or 580x1248px for Retina devices) */}
<img
className="absolute max-w-[84.33%]"
src={mockupImage.src}
width={290}
height={624}
alt="Features illustration"
/>
{/* iPhone mockup */}
<img
className="relative max-w-full mx-auto md:mr-0 md:max-w-none h-auto pointer-events-none"
src={iphoneMockup.src}
width={344}
height={674}
alt="iPhone mockup"
aria-hidden="true"
/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -161,40 +71,6 @@ const LandingPage: BlitzPage = () => {
); );
}; };
function ReferralBanner() {
const isDisabled = true;
if (isDisabled) {
return null;
}
return (
<div className="relative bg-primary-600">
<div className="max-w-7xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
<div className="pr-16 sm:text-center sm:px-16">
<p className="font-medium text-white">
<span>&#127881; New: Get one month free for every friend that joins and subscribe!</span>
<span className="block sm:ml-2 sm:inline-block">
<a href="#" className="text-white font-bold underline">
{" "}
Learn more <span aria-hidden="true">&rarr;</span>
</a>
</span>
</p>
</div>
<div className="absolute inset-y-0 right-0 pt-1 pr-1 flex items-start sm:pt-1 sm:pr-2 sm:items-start">
<button
type="button"
className="flex p-2 rounded-md hover:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-white"
>
<span className="sr-only">Dismiss</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</div>
</div>
);
}
LandingPage.suppressFirstRenderFlicker = true; LandingPage.suppressFirstRenderFlicker = true;
export default LandingPage; export default LandingPage;

View File

@ -2,12 +2,16 @@ import type { BlitzApiRequest, BlitzApiResponse } from "blitz";
import { getConfig } from "blitz"; import { getConfig } from "blitz";
import twilio from "twilio"; import twilio from "twilio";
import type { ApiError } from "../../../api/_types";
import appLogger from "../../../../integrations/logger"; import appLogger from "../../../../integrations/logger";
import db from "../../../../db"; import db from "../../../../db";
import insertIncomingMessageQueue from "../queue/insert-incoming-message"; import insertIncomingMessageQueue from "../queue/insert-incoming-message";
import notifyIncomingMessageQueue from "../queue/notify-incoming-message"; import notifyIncomingMessageQueue from "../queue/notify-incoming-message";
type ApiError = {
statusCode: number;
errorMessage: string;
};
const logger = appLogger.child({ route: "/api/webhook/incoming-message" }); const logger = appLogger.child({ route: "/api/webhook/incoming-message" });
const { serverRuntimeConfig } = getConfig(); const { serverRuntimeConfig } = getConfig();