click to action on landing page
This commit is contained in:
parent
e7c69a4d7a
commit
1e89e57145
@ -1,4 +0,0 @@
|
|||||||
export type ApiError = {
|
|
||||||
statusCode: number;
|
|
||||||
errorMessage: string;
|
|
||||||
};
|
|
@ -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();
|
|
||||||
}
|
|
46
app/landing-page/components/cta-form.tsx
Normal file
46
app/landing-page/components/cta-form.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
26
app/landing-page/components/phone-mockup.tsx
Normal file
26
app/landing-page/components/phone-mockup.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
35
app/landing-page/components/referral-banner.tsx
Normal file
35
app/landing-page/components/referral-banner.tsx
Normal 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>🎉 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">→</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>
|
||||||
|
);
|
||||||
|
}
|
23
app/landing-page/mutations/join-waitlist.ts
Normal file
23
app/landing-page/mutations/join-waitlist.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -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! 🐚 Keep your phone number and pay less for your
|
Coming soon! 🐚 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>🎉 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">→</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;
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user