cleaner landing page

This commit is contained in:
m5r 2022-07-09 01:34:18 +02:00
parent 9e783b506d
commit 27f8ed4c7c
29 changed files with 425 additions and 492 deletions

View File

@ -41,6 +41,22 @@ invariant(
`Please define the "WEB_PUSH_VAPID_PUBLIC_KEY" environment variable`, `Please define the "WEB_PUSH_VAPID_PUBLIC_KEY" environment variable`,
); );
invariant(typeof process.env.FATHOM_SITE_ID === "string", `Please define the "FATHOM_SITE_ID" environment variable`); invariant(typeof process.env.FATHOM_SITE_ID === "string", `Please define the "FATHOM_SITE_ID" environment variable`);
invariant(
typeof process.env.MAILCHIMP_API_KEY === "string",
`Please define the "MAILCHIMP_API_KEY" environment variable`,
);
invariant(
typeof process.env.MAILCHIMP_AUDIENCE_ID === "string",
`Please define the "MAILCHIMP_AUDIENCE_ID" environment variable`,
);
invariant(
typeof process.env.DISCORD_WEBHOOK_ID === "string",
`Please define the "DISCORD_WEBHOOK_ID" environment variable`,
);
invariant(
typeof process.env.DISCORD_WEBHOOK_TOKEN === "string",
`Please define the "DISCORD_WEBHOOK_TOKEN" environment variable`,
);
export default { export default {
app: { app: {
@ -61,10 +77,18 @@ export default {
secretAccessKey: process.env.AWS_S3_ACCESS_KEY_SECRET, secretAccessKey: process.env.AWS_S3_ACCESS_KEY_SECRET,
}, },
}, },
discord: {
webhookId: process.env.DISCORD_WEBHOOK_ID,
webhookToken: process.env.DISCORD_WEBHOOK_TOKEN,
},
fathom: { fathom: {
siteId: process.env.FATHOM_SITE_ID, siteId: process.env.FATHOM_SITE_ID,
domain: process.env.FATHOM_CUSTOM_DOMAIN, domain: process.env.FATHOM_CUSTOM_DOMAIN,
}, },
mailchimp: {
apiKey: process.env.MAILCHIMP_API_KEY,
audienceId: process.env.MAILCHIMP_AUDIENCE_ID,
},
redis: { redis: {
url: process.env.REDIS_URL, url: process.env.REDIS_URL,
password: process.env.REDIS_PASSWORD, password: process.env.REDIS_PASSWORD,

View File

@ -0,0 +1,23 @@
import { type ActionFunction, json } from "@remix-run/node";
import { addSubscriber } from "~/utils/mailchimp.server";
import { executeWebhook } from "~/utils/discord.server";
export type JoinWaitlistActionData = { submitted: true };
const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const email = formData.get("email");
if (!formData.get("email") || typeof email !== "string") {
throw new Error("Something wrong happened");
}
// await addSubscriber(email);
const res = await executeWebhook(email);
console.log(res.status);
console.log(await res.text());
return json<JoinWaitlistActionData>({ submitted: true });
};
export default action;

View File

@ -1,20 +0,0 @@
import type { FunctionComponent, PropsWithChildren } from "react";
import Header from "./header";
import Footer from "./footer";
const BaseLayout: FunctionComponent<PropsWithChildren<{}>> = ({ children }) => (
<>
<section className="font-inter antialiased bg-white text-gray-900 tracking-tight">
<section className="flex flex-col min-h-screen overflow-hidden">
<Header />
<main className="flex-grow">{children}</main>
<Footer />
</section>
</section>
</>
);
export default BaseLayout;

View File

@ -0,0 +1,41 @@
import type { ButtonHTMLAttributes } from "react";
import clsx from "clsx";
const baseStyles = {
solid: "group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2",
outline: "group inline-flex ring-1 items-center justify-center rounded-full py-2 px-4 text-sm focus:outline-none",
};
const variantStyles = {
solid: {
slate: "bg-slate-900 text-white hover:bg-slate-700 hover:text-slate-100 active:bg-slate-800 active:text-slate-300 focus-visible:outline-slate-900",
primary:
"bg-primary-600 text-white hover:text-slate-100 hover:bg-primary-500 active:bg-primary-800 active:text-primary-100 focus-visible:outline-primary-600",
white: "bg-white text-slate-900 hover:bg-primary-50 active:bg-primary-200 active:text-slate-600 focus-visible:outline-white",
},
outline: {
slate: "ring-slate-200 text-slate-700 hover:text-slate-900 hover:ring-slate-300 active:bg-slate-100 active:text-slate-600 focus-visible:outline-primary-600 focus-visible:ring-slate-300",
white: "ring-slate-700 text-white hover:ring-slate-500 active:ring-slate-700 active:text-slate-400 focus-visible:outline-white",
},
};
type Props = ButtonHTMLAttributes<HTMLButtonElement> &
(
| {
variant: "solid";
color: "slate" | "primary" | "white";
}
| {
variant: "outline";
color: "slate" | "white";
}
) & {
className?: string;
};
export default function Button({ variant, color, className, ...props }: Props) {
// @ts-ignore
const fullClassName = clsx(baseStyles[variant], variantStyles[variant][color], className);
return <button className={fullClassName} {...props} />;
}

View File

@ -0,0 +1,62 @@
import { Form, useActionData } from "@remix-run/react";
import type { JoinWaitlistActionData } from "~/features/public-area/actions";
import Button from "./button";
import Container from "./container";
import { TextField } from "./fields";
import backgroundImage from "../images/background-call-to-action.jpg";
import Alert from "~/features/core/components/alert";
export default function CallToAction() {
const actionData = useActionData<JoinWaitlistActionData>();
return (
<section id="get-started-today" className="relative overflow-hidden bg-blue-600 py-32">
<img
className="absolute top-1/2 left-1/2 max-w-none -translate-x-1/2 -translate-y-1/2"
src={backgroundImage}
alt=""
width={2347}
height={1244}
/>
<Container className="relative">
<div className="mx-auto max-w-lg text-center">
<h2 className="font-mackinac font-bold text-3xl tracking-tight text-white sm:text-4xl">
Request access
</h2>
<p className="mt-4 text-lg tracking-tight text-white">
Shellphone is currently invite-only but we onboard new users on a regular basis. Enter your
email address to join the waitlist and receive important updates in your inbox.
</p>
</div>
<Form method="post" className="max-w-2xl mx-auto flex mt-10 space-x-4">
{actionData?.submitted ? (
<div className="m-auto">
<Alert
title="You made it!"
message="You&#39;re on the list, we will be in touch soon"
variant="success"
/>
</div>
) : (
<>
<TextField
id="email"
name="email"
type="email"
autoComplete="email"
className="w-full"
required
/>
<Button type="submit" variant="solid" color="white" className="w-40">
<span>Join waitlist</span>
</Button>
</>
)}
</Form>
</Container>
</section>
);
}

View File

@ -0,0 +1,10 @@
import type { HTMLAttributes } from "react";
import clsx from "clsx";
type Props = HTMLAttributes<HTMLDivElement> & {
className?: string;
};
export default function Container({ className, ...props }: Props) {
return <div className={clsx("mx-auto max-w-7xl px-4 sm:px-6 lg:px-8", className)} {...props} />;
}

View File

@ -1,32 +0,0 @@
import { useState } from "react";
export default function CTAForm() {
// TODO
const [{ isSubmitted }, setState] = useState({ isSubmitted: false });
const onSubmit = () => setState({ isSubmitted: true });
return (
<form onSubmit={onSubmit}>
{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 w-full md:max-w-md md:mx-0">
<input
name="email"
type="email"
className="form-input w-full mb-2 sm:mb-0 sm:mr-2 focus:outline-none focus:ring-rebeccapurple-500 focus:border-rebeccapurple-500"
placeholder="Enter your email address"
/>
<button
type="submit"
className="btn text-white bg-rebeccapurple-500 hover:bg-rebeccapurple-400 flex-shrink-0"
>
Request access
</button>
</div>
)}
</form>
);
}

View File

@ -2,7 +2,97 @@ import type { FunctionComponent, PropsWithChildren } from "react";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
import clsx from "clsx"; import clsx from "clsx";
export default function FAQs() { import Container from "./container";
import backgroundImage from "../images/background-faqs.jpg";
const faqs = [
[
{
question: "Does TaxPal handle VAT?",
answer: "Well no, but if you move your company offshore you can probably ignore it.",
},
{
question: "Can I pay for my subscription via purchase order?",
answer: "Absolutely, we are happy to take your money in all forms.",
},
{
question: "How do I apply for a job at TaxPal?",
answer: "We only hire our customers, so subscribe for a minimum of 6 months and then lets talk.",
},
],
[
{
question: "What was that testimonial about tax fraud all about?",
answer: "TaxPal is just a software application, ultimately your books are your responsibility.",
},
{
question: "TaxPal sounds horrible but why do I still feel compelled to purchase?",
answer: "This is the power of excellent visual design. You just cant resist it, no matter how poorly it actually functions.",
},
{
question: "I found other companies called TaxPal, are you sure you can use this name?",
answer: "Honestly not sure at all. We havent actually incorporated or anything, we just thought it sounded cool and made this website.",
},
],
[
{
question: "How do you generate reports?",
answer: "You just tell us what data you need a report for, and we get our kids to create beautiful charts for you using only the finest crayons.",
},
{
question: "Can we expect more inventory features?",
answer: "In life its really better to never expect anything at all.",
},
{
question: "I lost my password, how do I get into my account?",
answer: "Send us an email and we will send you a copy of our latest password spreadsheet so you can find your information.",
},
],
];
export default function Faqs() {
return (
<section id="faq" aria-labelledby="faq-title" className="relative overflow-hidden bg-slate-50 py-20 sm:py-32">
<img
className="absolute top-0 left-1/2 max-w-none translate-x-[-30%] -translate-y-1/4"
src={backgroundImage}
alt=""
width={1558}
height={946}
/>
<Container className="relative">
<div className="mx-auto max-w-2xl lg:mx-0">
<h2
id="faq-title"
className="font-mackinac font-bold text-3xl tracking-tight text-slate-900 sm:text-4xl"
>
Frequently asked questions
</h2>
</div>
<ul className="mt-16 grid grid-cols-1 max-w-3xl mx-auto pl-12 lg:mx-0">
<Accordion title="How does it work?">
Shellphone is your go-to app to use your phone number over the internet. It integrates
seamlessly with Twilio to provide the best experience for your personal cloud phone.
</Accordion>
<Accordion title="What do I need to use Shellphone?">
Shellphone is still in its early stages and we&#39;re working hard to make it as easy-to-use as
possible. Currently, you must have a Twilio account to set up your personal cloud phone with
Shellphone.
</Accordion>
<Accordion title="Why would I use this over an eSIM?">
Chances are you&#39;re currently using an eSIM-compatible device. eSIMs are a reasonable way of
using a phone number internationally but they are still subject to some irky limitations. For
example, you can only use an eSIM on one device at a time and you are still subject to
exorbitant rates from your carrier.
</Accordion>
<span className="block border-t border-gray-200" aria-hidden="true" />
</ul>
</Container>
</section>
);
}
function FAQs() {
return ( return (
<section className="max-w-6xl mx-auto px-4 sm:px-6"> <section className="max-w-6xl mx-auto px-4 sm:px-6">
<div className="py-12 md:py-20"> <div className="py-12 md:py-20">

View File

@ -0,0 +1,27 @@
import type { InputHTMLAttributes, HTMLAttributes, PropsWithChildren } from "react";
const formClasses =
"block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm";
function Label({ id, children }: PropsWithChildren<HTMLAttributes<HTMLLabelElement>>) {
return (
<label htmlFor={id} className="mb-3 block text-sm font-medium text-gray-700">
{children}
</label>
);
}
export function TextField({
id,
label,
type = "text",
className = "",
...props
}: InputHTMLAttributes<HTMLInputElement> & { label?: string }) {
return (
<div className={className}>
{label && <Label id={id}>{label}</Label>}
<input id={id} type={type} {...props} className={formClasses} />
</div>
);
}

View File

@ -1,40 +0,0 @@
import type { FunctionComponent } from "react";
import { Link, type LinkProps } from "@remix-run/react";
export default function Footer() {
// TODO
const isDisabled = true;
if (isDisabled) {
return null;
}
return (
<footer className="bg-white">
<div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8">
<nav className="-mx-5 -my-2 flex flex-wrap justify-center" aria-label="Footer">
<FooterLink to="/blog" name="Blog" />
<FooterLink to="/privacy" name="Privacy Policy" />
<FooterLink to="/terms" name="Terms of Service" />
<FooterLink to="mailto:support@shellphone.app" name="Email Us" />
</nav>
<p className="mt-8 text-center text-base text-gray-400">
&copy; 2021 Capsule Corp. Dev Pte. Ltd. All rights reserved.
{/*&copy; 2021 Mokhtar Mial All rights reserved.*/}
</p>
</div>
</footer>
);
}
type Props = {
to: LinkProps["to"];
name: string;
};
const FooterLink: FunctionComponent<Props> = ({ to, name }) => (
<div className="px-5 py-2">
<Link to={to} className="text-base text-gray-500 hover:text-gray-900">
{name}
</Link>
</div>
);

View File

@ -1,240 +1,34 @@
import { Fragment, useState, useRef, useEffect } from "react"; import { Link } from "@remix-run/react";
import { Link, type LinkProps } from "@remix-run/react";
import { Dialog, Transition } from "@headlessui/react";
import { IoClose } from "react-icons/io5";
function Header() { import Button from "./button";
import Container from "./container";
import Logo from "./logo";
import NavLink from "./nav-link";
export default function Header() {
return ( return (
<header className="absolute inset-x-0 top-0 z-10 w-full"> <header className="py-10">
<div className="px-4 mx-auto sm:px-6 lg:px-8"> <Container>
<div className="flex items-center justify-between h-16 lg:h-20"> <nav className="relative z-50 flex justify-between">
<div className="hidden lg:flex lg:items-center lg:justify-center lg:ml-10 lg:mr-auto lg:space-x-10"> <div className="flex items-center md:gap-x-12">
<a <Link to="/" aria-label="Home">
href="#" <Logo />
title=""
className="text-base text-black transition-all duration-200 hover:text-opacity-80"
>
{" "}
Features{" "}
</a>
<a
href="#"
title=""
className="text-base text-black transition-all duration-200 hover:text-opacity-80"
>
{" "}
Solutions{" "}
</a>
<a
href="#"
title=""
className="text-base text-black transition-all duration-200 hover:text-opacity-80"
>
{" "}
Resources{" "}
</a>
<a
href="#"
title=""
className="text-base text-black transition-all duration-200 hover:text-opacity-80"
>
{" "}
Pricing{" "}
</a>
</div>
</div>
</div>
</header>
);
}
function Headerold() {
return (
<header className="absolute w-full z-30 inset-x-0 top-0">
<div className="px-4 mx-auto sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-20">
<div className="flex-shrink-0 mr-5">
<Link to="/" className="block">
<img className="w-10 h-10" src="/shellphone.png" alt="Shellphone logo" />
</Link> </Link>
</div> </div>
<div className="flex items-center gap-x-5 md:gap-x-8">
<nav className="hidden md:flex md:flex-grow"> <NavLink href="/sign-in">Have an account?</NavLink>
<ul className="flex items-center justify-center ml-10 mr-auto space-x-10"> <Button
<li> variant="solid"
<DesktopNavLink to="/features" label="Features" /> color="primary"
</li> onClick={() => {
<li> document.querySelector("#get-started-today")?.scrollIntoView({ behavior: "smooth" });
<DesktopNavLink to="/roadmap" label="Roadmap" /> }}
</li> >
<li> <span>Request access</span>
<DesktopNavLink to="/open" label="Open Metrics" /> </Button>
</li> </div>
<li>
<DesktopNavLink to="/pricing" label="Pricing" />
</li>
</ul>
</nav> </nav>
</Container>
<MobileNav />
</div>
</div>
</header> </header>
); );
} }
type NavLinkProps = {
to: LinkProps["to"];
label: string;
};
function DesktopNavLink({ to, label }: NavLinkProps) {
return (
<Link to={to} className="text-base text-gray-600 hover:text-gray-900 transition duration-150 ease-in-out">
{label}
</Link>
);
}
function MobileNav() {
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const trigger = useRef<HTMLButtonElement>(null);
const mobileNav = useRef<HTMLDivElement>(null);
// close the mobile menu on click outside
useEffect(() => {
const clickHandler = ({ target }: MouseEvent) => {
if (!mobileNav.current || !trigger.current) {
return;
}
console.log(mobileNav.current.contains(target as Node));
if (
!mobileNavOpen ||
mobileNav.current.contains(target as Node) ||
trigger.current.contains(target as Node)
) {
return;
}
setMobileNavOpen(false);
};
document.addEventListener("click", clickHandler);
return () => document.removeEventListener("click", clickHandler);
});
// close the mobile menu if the esc key is pressed
useEffect(() => {
const keyHandler = ({ keyCode }: KeyboardEvent) => {
if (!mobileNavOpen || keyCode !== 27) return;
setMobileNavOpen(false);
};
document.addEventListener("keydown", keyHandler);
return () => document.removeEventListener("keydown", keyHandler);
});
return (
<div className="inline-flex md:hidden">
<button
ref={trigger}
className={`hamburger ${mobileNavOpen && "active"}`}
aria-controls="mobile-nav"
aria-expanded={mobileNavOpen}
onClick={() => setMobileNavOpen(!mobileNavOpen)}
>
<span className="sr-only">Menu</span>
<svg
className="w-6 h-6 fill-current text-gray-900 hover:text-gray-900 transition duration-150 ease-in-out"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<rect y="4" width="24" height="2" rx="1" />
<rect y="11" width="24" height="2" rx="1" />
<rect y="18" width="24" height="2" rx="1" />
</svg>
</button>
<Transition.Root show={mobileNavOpen} as={Fragment}>
<Dialog as="div" className="fixed z-40 inset-0 overflow-hidden" onClose={setMobileNavOpen}>
<div className="absolute inset-0 overflow-hidden">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-500"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-500"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="absolute inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div ref={mobileNav} className="w-screen max-w-[16rem] sm:max-w-sm">
<div className="h-full flex flex-col py-6 bg-white shadow-xl overflow-y-scroll">
<div className="px-4 sm:px-6">
<div className="flex items-start justify-between">
<Dialog.Title className="text-lg font-medium text-gray-900">
Shellphone
</Dialog.Title>
<div className="ml-3 h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rebeccapurple-500"
onClick={() => setMobileNavOpen(false)}
>
<span className="sr-only">Close panel</span>
<IoClose className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div className="mt-6 relative flex-1 px-4 sm:px-6">
<div className="absolute inset-0 px-4 sm:px-6">
<ul className="space-y-4">
<li>
<MobileNavLink to="/features" label="Features" />
</li>
<li>
<MobileNavLink to="/roadmap" label="Roadmap" />
</li>
<li>
<MobileNavLink to="open" label="Open Metrics" />
</li>
<li>
<MobileNavLink to="/pricing" label="Pricing" />
</li>
</ul>
</div>
</div>
</div>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</div>
);
function MobileNavLink({ to, label }: NavLinkProps) {
return (
<Link to={to} onClick={() => setMobileNavOpen(false)} className="text-base flex text-gray-600 hover:text-gray-900">
{label}
</Link>
);
}
}
export default Headerold;

View File

@ -1,51 +1,38 @@
import CTAForm from "./cta-form"; import Button from "./button";
import Container from "./container";
import mockupImage from "../images/phone-mockup.png"; /*
height: calc(100vh - 120px);
display: flex;
flex-direction: column;
justify-content: center;
margin-top: -120px;
*/
export default function Hero() { export default function Hero() {
return ( return (
<div className="relative bg-gradient-to-b from-rebeccapurple-100 to-rebeccapurple-200"> <Container className="pt-20 pb-16 text-center lg:pt-32 landing-hero">
<section className="overflow-hidden"> <h1 className="mx-auto max-w-4xl font-mackinac font-heading text-5xl font-medium tracking-tight text-[#24185B] sm:text-7xl">
<div className="flex flex-col lg:flex-row lg:items-stretch lg:min-h-screen lg:max-h-[900px]"> <span className="background-primary bg-clip-text decoration-clone text-transparent">
<div className="flex items-center justify-center w-full lg:order-2 lg:w-7/12"> Calling your bank from abroad
<div className="h-full px-4 pt-24 pb-16 sm:px-6 lg:px-24 2xl:px-32 lg:pt-40 lg:pb-14">
<div className="flex flex-col flex-1 justify-center h-full space-y-8">
<h1 className="font-heading text-4xl leading-none lg:leading-tight xl:text-5xl xl:leading-tight">
<span className="bg-gradient-to-br from-rebeccapurple-500 to-indigo-600 bg-clip-text decoration-clone text-transparent">
Take your phone number
</span>{" "} </span>{" "}
<span className="text-[#24185B]">anywhere you go</span> just got{" "}
<span className="relative whitespace-nowrap">
<svg
aria-hidden="true"
viewBox="0 0 418 42"
className="absolute top-2/3 left-0 h-[0.58em] w-full fill-rebeccapurple-300/70"
preserveAspectRatio="none"
>
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
</svg>
<span className="relative">easier</span>
</span>{" "}
!
</h1> </h1>
<p className="mx-auto mt-6 max-w-2xl text-lg tracking-tight text-slate-700">
<p className="text-base lg:text-lg xl:text-xl text-black"> Coming soon, the personal cloud phone for digital nomads! Take your phone number anywhere you go 🌏
Coming soon! &#128026; Keep your phone number and pay less for your communications,
even abroad.
</p> </p>
</Container>
<CTAForm />
<div className="max-w-lg mx-auto md:mx-0">
<span className="block md:inline mx-2">
<em> </em>Free trial
</span>
<span className="block md:inline mx-2">
<em> </em>No credit card required
</span>
<span className="block md:inline mx-2">
<em> </em>Cancel anytime
</span>
</div>
</div>
</div>
</div>
<div className="relative w-full overflow-hidden lg:w-5/12 lg:order-1">
<div className="lg:absolute lg:bottom-0 lg:left-0">
<img className="w-full" src={mockupImage} alt="App screenshot on a phone" />
</div>
</div>
</div>
</section>
</div>
); );
} }

View File

@ -1,25 +0,0 @@
import type { FunctionComponent, PropsWithChildren } from "react";
import BaseLayout from "./base-layout";
type Props = {
title?: string;
};
const Layout: FunctionComponent<PropsWithChildren<Props>> = ({ children, title }) => (
<BaseLayout>
<section className="max-w-6xl mx-auto px-4 sm:px-6">
<div className="pt-32 pb-10 md:pt-34 md:pb-16">
{title ? (
<div className="max-w-5xl mx-auto">
<h1 className="h1 mb-16 text-navy font-extrabold font-mackinac">{title}</h1>
</div>
) : null}
<div className="max-w-3xl mx-auto text-lg xl:text-xl flow-root">{children}</div>
</div>
</section>
</BaseLayout>
);
export default Layout;

View File

@ -0,0 +1,3 @@
export default function Logo() {
return <img className="w-10 h-10" src="/shellphone.png" alt="Shellphone logo" />;
}

View File

@ -0,0 +1,13 @@
import type { PropsWithChildren } from "react";
import { Link } from "@remix-run/react";
export default function NavLink({ href, children }: PropsWithChildren<{ href: string }>) {
return (
<Link
to={href}
className="inline-block rounded-lg py-1 px-2 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900"
>
{children}
</Link>
);
}

View File

@ -1,36 +0,0 @@
import { IoClose } from "react-icons/io5";
export default function ReferralBanner() {
// TODO
const isDisabled = true;
if (isDisabled) {
return null;
}
return (
<div className="relative bg-rebeccapurple-600 z-40">
<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-rebeccapurple-500 focus:outline-none focus:ring-2 focus:ring-white"
>
<span className="sr-only">Dismiss</span>
<IoClose className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</div>
</div>
);
}

View File

@ -1,20 +0,0 @@
export default function Testimonials() {
return (
<div className="bg-rebeccapurple-600">
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8">
<p className="text-xl text-white text-center text-base font-semibold uppercase text-gray-600 tracking-wider">
Trusted by digital nomads in
<div className="h-[2rem] relative flex">
<span className="location">Bali</span>
<span className="location">Tulum</span>
<span className="location">Tbilissi</span>
<span className="location">Bansko</span>
<span className="location">Zanzibar</span>
<span className="location">Mauritius</span>
<span className="location">Amsterdam</span>
</div>
</p>
</div>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -1,23 +1,17 @@
import Header from "../components/header"; import Header from "../components/header";
import Footer from "../components/footer";
import ReferralBanner from "../components/referral-banner";
import Hero from "../components/hero"; import Hero from "../components/hero";
import FAQs from "../components/faqs"; import CallToAction from "../components/call-to-action";
import Faqs from "../components/faqs";
export default function IndexPage() { export default function IndexPage() {
return ( return (
<section className="font-inter antialiased bg-white text-gray-900 tracking-tight"> <section className="flex h-full flex-col">
<section className="flex flex-col min-h-screen overflow-hidden">
<Header /> <Header />
<main>
<main className="flex-grow">
<ReferralBanner />
<Hero /> <Hero />
<FAQs /> <CallToAction />
<Faqs />
</main> </main>
<Footer />
</section>
</section> </section>
); );
} }

View File

@ -1,10 +1,13 @@
import type { LinksFunction, MetaFunction } from "@remix-run/node"; import type { LinksFunction, MetaFunction } from "@remix-run/node";
import IndexPage from "~/features/public-area/pages"; import joinWaitlistAction from "~/features/public-area/actions/index";
import IndexPage from "~/features/public-area/pages/index";
import { getSeoMeta } from "~/utils/seo"; import { getSeoMeta } from "~/utils/seo";
import styles from "../styles/index.css"; import styles from "../styles/index.css";
export const action = joinWaitlistAction;
export const meta: MetaFunction = () => ({ export const meta: MetaFunction = () => ({
...getSeoMeta({ title: "", description: "Welcome to Remixtape!" }), ...getSeoMeta({ title: "", description: "Welcome to Remixtape!" }),
}); });

View File

@ -0,0 +1,12 @@
import config from "~/config/config.server";
const { webhookId, webhookToken } = config.discord;
export function executeWebhook(email: string) {
const url = `https://discord.com/api/webhooks/${webhookId}/${webhookToken}`;
return fetch(url, {
body: JSON.stringify({ content: `\`${email}\` just joined Shellphone's waitlist` }),
headers: { "Content-Type": "application/json" },
method: "post",
});
}

View File

@ -0,0 +1,22 @@
import config from "~/config/config.server";
export async function addSubscriber(email: string) {
const { apiKey, audienceId } = config.mailchimp;
const region = apiKey.split("-")[1];
const url = `https://${region}.api.mailchimp.com/3.0/lists/${audienceId}/members`;
const data = {
email_address: email,
status: "subscribed",
};
const base64ApiKey = Buffer.from(`any:${apiKey}`).toString("base64");
const headers = {
"Content-Type": "application/json",
Authorization: `Basic ${base64ApiKey}`,
};
return fetch(url, {
body: JSON.stringify(data),
headers,
method: "post",
});
}

View File

@ -56,3 +56,13 @@
@apply font-mackinac tracking-tight font-bold; @apply font-mackinac tracking-tight font-bold;
word-spacing: 0.025em; word-spacing: 0.025em;
} }
.background-primary {
@apply bg-gradient-to-br from-rebeccapurple-500 to-indigo-600;
}
.landing-hero {
height: calc(100vh - 120px);
margin-top: -120px;
@apply flex flex-col justify-center;
}

View File

@ -95,7 +95,7 @@ module.exports = {
navy: "#24185B", navy: "#24185B",
}, },
fontSize: { fontSize: {
xs: ["0.75rem", { lineHeight: "1.5" }], /*xs: ["0.75rem", { lineHeight: "1.5" }],
sm: ["0.875rem", { lineHeight: "1.5" }], sm: ["0.875rem", { lineHeight: "1.5" }],
base: ["1rem", { lineHeight: "1.5" }], base: ["1rem", { lineHeight: "1.5" }],
lg: ["1.125rem", { lineHeight: "1.5" }], lg: ["1.125rem", { lineHeight: "1.5" }],
@ -104,10 +104,20 @@ module.exports = {
"3xl": ["2.63rem", { lineHeight: "1.24" }], "3xl": ["2.63rem", { lineHeight: "1.24" }],
"4xl": ["3.5rem", { lineHeight: "1.18" }], "4xl": ["3.5rem", { lineHeight: "1.18" }],
"5xl": ["4rem", { lineHeight: "1.16" }], "5xl": ["4rem", { lineHeight: "1.16" }],
"6xl": ["5.5rem", { lineHeight: "1.11" }], "6xl": ["5.5rem", { lineHeight: "1.11" }],*/
}, xs: ["0.75rem", { lineHeight: "1rem" }],
boxShadow: { sm: ["0.875rem", { lineHeight: "1.5rem" }],
"2xl": "0 25px 50px -12px rgba(0, 0, 0, 0.08)", base: ["1rem", { lineHeight: "1.75rem" }],
lg: ["1.125rem", { lineHeight: "2rem" }],
xl: ["1.25rem", { lineHeight: "2rem" }],
"2xl": ["1.5rem", { lineHeight: "2rem" }],
"3xl": ["2rem", { lineHeight: "2.5rem" }],
"4xl": ["2.5rem", { lineHeight: "3.5rem" }],
"5xl": ["3rem", { lineHeight: "3.5rem" }],
"6xl": ["3.75rem", { lineHeight: "1" }],
"7xl": ["4.5rem", { lineHeight: "1.1" }],
"8xl": ["6rem", { lineHeight: "1" }],
"9xl": ["8rem", { lineHeight: "1" }],
}, },
outline: { outline: {
blue: "2px solid rgba(0, 112, 244, 0.5)", blue: "2px solid rgba(0, 112, 244, 0.5)",
@ -130,25 +140,6 @@ module.exports = {
wider: "0.02em", wider: "0.02em",
widest: "0.4em", widest: "0.4em",
}, },
minWidth: {
10: "2.5rem",
},
scale: {
98: ".98",
},
animation: {
float: "float 5s ease-in-out infinite",
},
keyframes: {
float: {
"0%, 100%": { transform: "translateY(0)" },
"50%": { transform: "translateY(-10%)" },
},
},
zIndex: {
"-1": "-1",
"-10": "-10",
},
}, },
}, },
variants: { variants: {