remixed v0
This commit is contained in:
20
app/features/public-area/components/base-layout.tsx
Normal file
20
app/features/public-area/components/base-layout.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
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;
|
31
app/features/public-area/components/cta-form.tsx
Normal file
31
app/features/public-area/components/cta-form.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CTAForm() {
|
||||
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'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>
|
||||
);
|
||||
}
|
86
app/features/public-area/components/faqs.tsx
Normal file
86
app/features/public-area/components/faqs.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import type { FunctionComponent, PropsWithChildren } from "react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export default function FAQs() {
|
||||
return (
|
||||
<section className="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div className="py-12 md:py-20">
|
||||
<div className="max-w-3xl mx-auto text-center pb-20">
|
||||
<h2 className="h2 font-mackinac">Questions & Answers</h2>
|
||||
</div>
|
||||
|
||||
<ul className="max-w-3xl mx-auto pl-12">
|
||||
<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'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'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>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const Accordion: FunctionComponent<PropsWithChildren<{ title: string }>> = ({ title, children }) => {
|
||||
return (
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button className="flex items-center w-full text-lg font-medium text-left py-5 border-t border-gray-200">
|
||||
<svg
|
||||
className="w-4 h-4 fill-current text-rebeccapurple-500 flex-shrink-0 mr-8 -ml-12"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
y="7"
|
||||
width="16"
|
||||
height="2"
|
||||
rx="1"
|
||||
className={clsx("transform origin-center transition duration-200 ease-out", {
|
||||
"rotate-180": open,
|
||||
})}
|
||||
/>
|
||||
<rect
|
||||
y="7"
|
||||
width="16"
|
||||
height="2"
|
||||
rx="1"
|
||||
className={clsx("transform origin-center transition duration-200 ease-out", {
|
||||
"rotate-90": !open,
|
||||
"rotate-180": open,
|
||||
})}
|
||||
/>
|
||||
</svg>
|
||||
<span>{title}</span>
|
||||
</Disclosure.Button>
|
||||
|
||||
<Transition
|
||||
enter="transition duration-300 ease-in-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel className="text-gray-600 overflow-hidden">
|
||||
<p className="pb-5">{children}</p>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
40
app/features/public-area/components/footer.tsx
Normal file
40
app/features/public-area/components/footer.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
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">
|
||||
© 2021 Capsule Corp. Dev Pte. Ltd. All rights reserved.
|
||||
{/*© 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>
|
||||
);
|
240
app/features/public-area/components/header.tsx
Normal file
240
app/features/public-area/components/header.tsx
Normal file
@ -0,0 +1,240 @@
|
||||
import { Fragment, useState, useRef, useEffect } from "react";
|
||||
import { Link, type LinkProps } from "@remix-run/react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { IoClose } from "react-icons/io5";
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<header className="absolute inset-x-0 top-0 z-10 w-full">
|
||||
<div className="px-4 mx-auto sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16 lg:h-20">
|
||||
<div className="hidden lg:flex lg:items-center lg:justify-center lg:ml-10 lg:mr-auto lg:space-x-10">
|
||||
<a
|
||||
href="#"
|
||||
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>
|
||||
</div>
|
||||
|
||||
<nav className="hidden md:flex md:flex-grow">
|
||||
<ul className="flex items-center justify-center ml-10 mr-auto space-x-10">
|
||||
<li>
|
||||
<DesktopNavLink to="/features" label="Features" />
|
||||
</li>
|
||||
<li>
|
||||
<DesktopNavLink to="/roadmap" label="Roadmap" />
|
||||
</li>
|
||||
<li>
|
||||
<DesktopNavLink to="/open" label="Open Metrics" />
|
||||
</li>
|
||||
<li>
|
||||
<DesktopNavLink to="/pricing" label="Pricing" />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<MobileNav />
|
||||
</div>
|
||||
</div>
|
||||
</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;
|
51
app/features/public-area/components/hero.tsx
Normal file
51
app/features/public-area/components/hero.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import CTAForm from "./cta-form";
|
||||
|
||||
import mockupImage from "../images/phone-mockup.png";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<div className="relative bg-gradient-to-b from-rebeccapurple-100 to-rebeccapurple-200">
|
||||
<section className="overflow-hidden">
|
||||
<div className="flex flex-col lg:flex-row lg:items-stretch lg:min-h-screen lg:max-h-[900px]">
|
||||
<div className="flex items-center justify-center w-full lg:order-2 lg:w-7/12">
|
||||
<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 className="text-[#24185B]">anywhere you dgo</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-base lg:text-lg xl:text-xl text-black">
|
||||
Coming soon! 🐚 Keep your phone number and pay less for your communications,
|
||||
even abroad.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
25
app/features/public-area/components/layout.tsx
Normal file
25
app/features/public-area/components/layout.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
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;
|
36
app/features/public-area/components/referral-banner.tsx
Normal file
36
app/features/public-area/components/referral-banner.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
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>🎉 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-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>
|
||||
);
|
||||
}
|
20
app/features/public-area/components/testimonials.tsx
Normal file
20
app/features/public-area/components/testimonials.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user