diff --git a/app/config/config.server.ts b/app/config/config.server.ts index ad3712d..57aad49 100644 --- a/app/config/config.server.ts +++ b/app/config/config.server.ts @@ -41,6 +41,22 @@ invariant( `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.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 { app: { @@ -61,10 +77,18 @@ export default { secretAccessKey: process.env.AWS_S3_ACCESS_KEY_SECRET, }, }, + discord: { + webhookId: process.env.DISCORD_WEBHOOK_ID, + webhookToken: process.env.DISCORD_WEBHOOK_TOKEN, + }, fathom: { siteId: process.env.FATHOM_SITE_ID, domain: process.env.FATHOM_CUSTOM_DOMAIN, }, + mailchimp: { + apiKey: process.env.MAILCHIMP_API_KEY, + audienceId: process.env.MAILCHIMP_AUDIENCE_ID, + }, redis: { url: process.env.REDIS_URL, password: process.env.REDIS_PASSWORD, diff --git a/app/features/public-area/actions/index.ts b/app/features/public-area/actions/index.ts new file mode 100644 index 0000000..e2fcf77 --- /dev/null +++ b/app/features/public-area/actions/index.ts @@ -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({ submitted: true }); +}; + +export default action; diff --git a/app/features/public-area/components/base-layout.tsx b/app/features/public-area/components/base-layout.tsx deleted file mode 100644 index 0d62f38..0000000 --- a/app/features/public-area/components/base-layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { FunctionComponent, PropsWithChildren } from "react"; - -import Header from "./header"; -import Footer from "./footer"; - -const BaseLayout: FunctionComponent> = ({ children }) => ( - <> -
-
-
- -
{children}
- -
-
-
- -); - -export default BaseLayout; diff --git a/app/features/public-area/components/button.tsx b/app/features/public-area/components/button.tsx new file mode 100644 index 0000000..3b86bc8 --- /dev/null +++ b/app/features/public-area/components/button.tsx @@ -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 & + ( + | { + 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 + + )} + + + + ); +} diff --git a/app/features/public-area/components/container.tsx b/app/features/public-area/components/container.tsx new file mode 100644 index 0000000..aa49c61 --- /dev/null +++ b/app/features/public-area/components/container.tsx @@ -0,0 +1,10 @@ +import type { HTMLAttributes } from "react"; +import clsx from "clsx"; + +type Props = HTMLAttributes & { + className?: string; +}; + +export default function Container({ className, ...props }: Props) { + return
; +} diff --git a/app/features/public-area/components/cta-form.tsx b/app/features/public-area/components/cta-form.tsx deleted file mode 100644 index 030c31d..0000000 --- a/app/features/public-area/components/cta-form.tsx +++ /dev/null @@ -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 ( -
- {isSubmitted ? ( -

- You're on the list! We will be in touch soon -

- ) : ( -
- - -
- )} -
- ); -} diff --git a/app/features/public-area/components/faqs.tsx b/app/features/public-area/components/faqs.tsx index a337548..f703796 100644 --- a/app/features/public-area/components/faqs.tsx +++ b/app/features/public-area/components/faqs.tsx @@ -2,7 +2,97 @@ import type { FunctionComponent, PropsWithChildren } from "react"; import { Disclosure, Transition } from "@headlessui/react"; 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 let’s 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 can’t 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 haven’t 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 it’s 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 ( +
+ + +
+

+ Frequently asked questions +

+
+
    + + 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. + + + 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. + + + 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. + +
+
+
+ ); +} + +function FAQs() { return (
diff --git a/app/features/public-area/components/fields.tsx b/app/features/public-area/components/fields.tsx new file mode 100644 index 0000000..b8f6661 --- /dev/null +++ b/app/features/public-area/components/fields.tsx @@ -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>) { + return ( + + ); +} + +export function TextField({ + id, + label, + type = "text", + className = "", + ...props +}: InputHTMLAttributes & { label?: string }) { + return ( +
+ {label && } + +
+ ); +} diff --git a/app/features/public-area/components/footer.tsx b/app/features/public-area/components/footer.tsx deleted file mode 100644 index 0d4ef6b..0000000 --- a/app/features/public-area/components/footer.tsx +++ /dev/null @@ -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 ( -
-
- -

- © 2021 Capsule Corp. Dev Pte. Ltd. All rights reserved. - {/*© 2021 Mokhtar Mial All rights reserved.*/} -

-
-
- ); -} - -type Props = { - to: LinkProps["to"]; - name: string; -}; - -const FooterLink: FunctionComponent = ({ to, name }) => ( -
- - {name} - -
-); diff --git a/app/features/public-area/components/header.tsx b/app/features/public-area/components/header.tsx index c7d6d88..fa71974 100644 --- a/app/features/public-area/components/header.tsx +++ b/app/features/public-area/components/header.tsx @@ -1,240 +1,34 @@ -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"; +import { Link } from "@remix-run/react"; -function Header() { +import Button from "./button"; +import Container from "./container"; +import Logo from "./logo"; +import NavLink from "./nav-link"; + +export default function Header() { return ( -
- -
- ); -} - -function Headerold() { - return ( -
-
-
-
- - Shellphone logo +
+ +
-
+
+ Have an account? + +
+ +
); } - -type NavLinkProps = { - to: LinkProps["to"]; - label: string; -}; - -function DesktopNavLink({ to, label }: NavLinkProps) { - return ( - - {label} - - ); -} - -function MobileNav() { - const [mobileNavOpen, setMobileNavOpen] = useState(false); - - const trigger = useRef(null); - const mobileNav = useRef(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 ( -
- - - - -
- - - - -
- -
-
-
-
- - Shellphone - -
- -
-
-
-
-
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
-
-
-
-
-
-
-
-
-
- ); - - function MobileNavLink({ to, label }: NavLinkProps) { - return ( - setMobileNavOpen(false)} className="text-base flex text-gray-600 hover:text-gray-900"> - {label} - - ); - } -} - -export default Headerold; diff --git a/app/features/public-area/components/hero.tsx b/app/features/public-area/components/hero.tsx index d9677af..6f8173f 100644 --- a/app/features/public-area/components/hero.tsx +++ b/app/features/public-area/components/hero.tsx @@ -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() { return ( -
-
-
-
-
-
-

- - Take your phone number - {" "} - anywhere you go -

- -

- Coming soon! 🐚 Keep your phone number and pay less for your communications, - even abroad. -

- - - -
- - Free trial - - - No credit card required - - - Cancel anytime - -
-
-
-
- -
-
- App screenshot on a phone -
-
-
-
-
+ +

+ + Calling your bank from abroad + {" "} + just got{" "} + + + easier + {" "} + ! +

+

+ Coming soon, the personal cloud phone for digital nomads! Take your phone number anywhere you go 🌏 +

+
); } diff --git a/app/features/public-area/components/layout.tsx b/app/features/public-area/components/layout.tsx deleted file mode 100644 index e911349..0000000 --- a/app/features/public-area/components/layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { FunctionComponent, PropsWithChildren } from "react"; - -import BaseLayout from "./base-layout"; - -type Props = { - title?: string; -}; - -const Layout: FunctionComponent> = ({ children, title }) => ( - -
-
- {title ? ( -
-

{title}

-
- ) : null} - -
{children}
-
-
-
-); - -export default Layout; diff --git a/app/features/public-area/components/logo.tsx b/app/features/public-area/components/logo.tsx new file mode 100644 index 0000000..9ddfbc0 --- /dev/null +++ b/app/features/public-area/components/logo.tsx @@ -0,0 +1,3 @@ +export default function Logo() { + return Shellphone logo; +} diff --git a/app/features/public-area/components/nav-link.tsx b/app/features/public-area/components/nav-link.tsx new file mode 100644 index 0000000..cf90fa7 --- /dev/null +++ b/app/features/public-area/components/nav-link.tsx @@ -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 ( + + {children} + + ); +} diff --git a/app/features/public-area/components/referral-banner.tsx b/app/features/public-area/components/referral-banner.tsx deleted file mode 100644 index c9aadfc..0000000 --- a/app/features/public-area/components/referral-banner.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { IoClose } from "react-icons/io5"; - -export default function ReferralBanner() { - // TODO - const isDisabled = true; - if (isDisabled) { - return null; - } - - return ( -
-
-
-

- 🎉 New: Get one month free for every friend that joins and subscribe! - - - {" "} - Learn more - - -

-
-
- -
-
-
- ); -} diff --git a/app/features/public-area/components/testimonials.tsx b/app/features/public-area/components/testimonials.tsx deleted file mode 100644 index 9c1b846..0000000 --- a/app/features/public-area/components/testimonials.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export default function Testimonials() { - return ( -
-
-

- Trusted by digital nomads in -

- Bali - Tulum - Tbilissi - Bansko - Zanzibar - Mauritius - Amsterdam -
-

-
-
- ); -} diff --git a/app/features/public-area/images/background-call-to-action.jpg b/app/features/public-area/images/background-call-to-action.jpg new file mode 100644 index 0000000..13d8ee5 Binary files /dev/null and b/app/features/public-area/images/background-call-to-action.jpg differ diff --git a/app/features/public-area/images/background-faqs.jpg b/app/features/public-area/images/background-faqs.jpg new file mode 100644 index 0000000..d9de04f Binary files /dev/null and b/app/features/public-area/images/background-faqs.jpg differ diff --git a/app/features/public-area/images/iphone-mockup.png b/app/features/public-area/images/iphone-mockup.png deleted file mode 100644 index a936f45..0000000 Binary files a/app/features/public-area/images/iphone-mockup.png and /dev/null differ diff --git a/app/features/public-area/images/mockup-image-01.jpg b/app/features/public-area/images/mockup-image-01.jpg deleted file mode 100644 index 7d72f2e..0000000 Binary files a/app/features/public-area/images/mockup-image-01.jpg and /dev/null differ diff --git a/app/features/public-area/images/mockup-image-01.png b/app/features/public-area/images/mockup-image-01.png deleted file mode 100644 index 3617ba1..0000000 Binary files a/app/features/public-area/images/mockup-image-01.png and /dev/null differ diff --git a/app/features/public-area/images/phone-mockup.png b/app/features/public-area/images/phone-mockup.png deleted file mode 100644 index 5eeb2cb..0000000 Binary files a/app/features/public-area/images/phone-mockup.png and /dev/null differ diff --git a/app/features/public-area/pages/index.tsx b/app/features/public-area/pages/index.tsx index 3a04b72..9f89e31 100644 --- a/app/features/public-area/pages/index.tsx +++ b/app/features/public-area/pages/index.tsx @@ -1,23 +1,17 @@ import Header from "../components/header"; -import Footer from "../components/footer"; -import ReferralBanner from "../components/referral-banner"; 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() { return ( -
-
-
- -
- - - -
- -
-
+
+
+
+ + + +
); } diff --git a/app/routes/index.tsx b/app/routes/index.tsx index 300c758..3dbf506 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -1,10 +1,13 @@ 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 styles from "../styles/index.css"; +export const action = joinWaitlistAction; + export const meta: MetaFunction = () => ({ ...getSeoMeta({ title: "", description: "Welcome to Remixtape!" }), }); diff --git a/app/utils/discord.server.ts b/app/utils/discord.server.ts new file mode 100644 index 0000000..d8d1b0d --- /dev/null +++ b/app/utils/discord.server.ts @@ -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", + }); +} diff --git a/app/utils/mailchimp.server.ts b/app/utils/mailchimp.server.ts new file mode 100644 index 0000000..d8de1c5 --- /dev/null +++ b/app/utils/mailchimp.server.ts @@ -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", + }); +} diff --git a/styles/tailwind.css b/styles/tailwind.css index a0aaafb..b6ada4d 100644 --- a/styles/tailwind.css +++ b/styles/tailwind.css @@ -56,3 +56,13 @@ @apply font-mackinac tracking-tight font-bold; 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; +} diff --git a/tailwind.config.js b/tailwind.config.js index 12b791d..9a93d85 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -95,7 +95,7 @@ module.exports = { navy: "#24185B", }, fontSize: { - xs: ["0.75rem", { lineHeight: "1.5" }], + /*xs: ["0.75rem", { lineHeight: "1.5" }], sm: ["0.875rem", { lineHeight: "1.5" }], base: ["1rem", { lineHeight: "1.5" }], lg: ["1.125rem", { lineHeight: "1.5" }], @@ -104,10 +104,20 @@ module.exports = { "3xl": ["2.63rem", { lineHeight: "1.24" }], "4xl": ["3.5rem", { lineHeight: "1.18" }], "5xl": ["4rem", { lineHeight: "1.16" }], - "6xl": ["5.5rem", { lineHeight: "1.11" }], - }, - boxShadow: { - "2xl": "0 25px 50px -12px rgba(0, 0, 0, 0.08)", + "6xl": ["5.5rem", { lineHeight: "1.11" }],*/ + xs: ["0.75rem", { lineHeight: "1rem" }], + sm: ["0.875rem", { lineHeight: "1.5rem" }], + 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: { blue: "2px solid rgba(0, 112, 244, 0.5)", @@ -130,25 +140,6 @@ module.exports = { wider: "0.02em", 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: {