feat: init waitlist website (#60)

* feat: init waitlist website

Co-authored-by: Ona <no-reply@ona.com>

* feat[waitlist]: tweak copy

Co-authored-by: Ona <no-reply@ona.com>

* fix[waitlist]: reminify lottie json

Co-authored-by: Ona <no-reply@ona.com>

* feat[waitlist]: seo and preview stuff

* chore[waitlist]: clean up

* build[waitlist]: add fly.io config

* feat(waitlist): add time-of-day greeting and duplicate email message

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): handle duplicate emails and send confirmation

Co-authored-by: Ona <no-reply@ona.com>

* chore: remove stray console.log

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add privacy policy page

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add footer with bottom progressive blur

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add trouble message and improve error handling

Co-authored-by: Ona <no-reply@ona.com>

* fix(waitlist): fix timeOfDay logic, typo, and add audienceId

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add .ico fallback favicon and style error page

Co-authored-by: Ona <no-reply@ona.com>

* chore(waitlist): add robots.txt, sitemap, clean dockerignore

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add footer to privacy policy page

Co-authored-by: Ona <no-reply@ona.com>

* fix(waitlist): use segments instead of audienceId

Co-authored-by: Ona <no-reply@ona.com>

* fix[waitlist]: remove segmentId from dup check

Co-authored-by: Ona <no-reply@ona.com>

* fix(waitlist): reset logo animation on mouse leave

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-03-08 02:54:56 +00:00
committed by GitHub
parent badc00c43b
commit 0a08706cf9
33 changed files with 2403 additions and 27 deletions

View File

@@ -0,0 +1,168 @@
import Lottie, { type LottieRef } from "lottie-react"
import { useEffect, useRef, useState } from "react"
import { useColorScheme } from "~/hooks/use-color-scheme"
import clickedAnimationDark from "~/lottie/clicked-dark.json"
import clickedAnimationLight from "~/lottie/clicked-light.json"
import loadingAnimationDark from "~/lottie/loading-dark.json"
import loadingAnimationLight from "~/lottie/loading-light.json"
import startLoadingAnimationDark from "~/lottie/start-loading-dark.json"
import startLoadingAnimationLight from "~/lottie/start-loading-light.json"
export const AnimatedLogoState = {
Idle: "idle",
Loading: "loading",
} as const
export type AnimatedLogoState = (typeof AnimatedLogoState)[keyof typeof AnimatedLogoState]
interface AnimatedLogoProps {
state: AnimatedLogoState
className?: string
}
interface Animation {
loop: boolean
reverse: boolean
sticky: boolean
data: unknown
}
export function AnimatedLogo({ state, className }: AnimatedLogoProps) {
const colorScheme = useColorScheme()
const [animationQueue, setAnimationQueue] = useState<Animation[]>([])
const lottieRef: LottieRef = useRef(null)
let currentAnimation: Animation
let isIdle = false
if (animationQueue.length === 0) {
isIdle = true
currentAnimation = {
loop: false,
reverse: false,
sticky: false,
data: colorScheme === "dark" ? startLoadingAnimationDark : startLoadingAnimationLight,
}
} else {
isIdle = false
currentAnimation = animationQueue[0]
}
useEffect(() => {
if (state === AnimatedLogoState.Loading) {
setAnimationQueue((queue) => [
...queue,
{
loop: false,
reverse: false,
sticky: false,
data: colorScheme === "dark" ? startLoadingAnimationDark : startLoadingAnimationLight,
},
{
loop: true,
reverse: false,
sticky: false,
data: colorScheme === "dark" ? loadingAnimationDark : loadingAnimationLight,
},
])
} else if (state === AnimatedLogoState.Idle) {
setAnimationQueue((queue) => {
const last = queue.at(-1)
if (!last) {
return []
}
if (
last.loop &&
(last.data === loadingAnimationDark || last.data === loadingAnimationLight)
) {
return [
...queue,
{
loop: false,
sticky: false,
reverse: false,
data: colorScheme === "dark" ? loadingAnimationDark : loadingAnimationLight,
},
{
loop: false,
sticky: false,
reverse: true,
data: colorScheme === "dark" ? startLoadingAnimationDark : startLoadingAnimationLight,
},
]
}
return []
})
}
}, [state])
useEffect(() => {
if (!lottieRef.current) {
return
}
if (currentAnimation.reverse) {
const frames = lottieRef.current.getDuration(true)
if (frames) {
lottieRef.current.setDirection(-1)
lottieRef.current.goToAndPlay(frames - 1, true)
}
} else if (!isIdle) {
lottieRef.current.setDirection(1)
lottieRef.current.play()
}
}, [currentAnimation])
function onComplete() {
if (animationQueue.length > 0 && !animationQueue[0].sticky) {
setAnimationQueue((queue) => queue.slice(1))
}
}
function onLoopComplete() {
const current = animationQueue[0]
const next = animationQueue[1]
if (current && next && current.data === next.data && current.loop && !next.loop) {
setAnimationQueue((queue) => queue.slice(2))
}
}
function onMouseDown() {
if (state === AnimatedLogoState.Idle) {
setAnimationQueue([
{
loop: false,
sticky: true,
reverse: false,
data: colorScheme === "dark" ? clickedAnimationDark : clickedAnimationLight,
},
])
}
}
function onMouseUp() {
if (state === AnimatedLogoState.Idle) {
setAnimationQueue((queue) => [
{
loop: false,
sticky: false,
reverse: true,
data: colorScheme === "dark" ? clickedAnimationDark : clickedAnimationLight,
},
])
}
}
return (
<Lottie
lottieRef={lottieRef}
autoplay={false}
loop={currentAnimation.loop}
className={className}
animationData={currentAnimation.data}
onComplete={onComplete}
onLoopComplete={onLoopComplete}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseLeave={onMouseUp}
/>
)
}

View File

@@ -0,0 +1,35 @@
export function ProgressiveBlur({
className,
direction = "down",
}: {
className?: string
direction?: "down" | "up"
}) {
if (direction === "up") {
return (
<div className={`pointer-events-none ${className ?? ""}`}>
<div className="absolute inset-0 backdrop-blur-[1px] [mask:linear-gradient(rgba(0,0,0,0)_0%,rgba(0,0,0,1)_10%,rgba(0,0,0,1)_20%,rgba(0,0,0,0)_30%)]" />
<div className="absolute inset-0 backdrop-blur-[2px] [mask:linear-gradient(rgba(0,0,0,0)_10%,rgba(0,0,0,1)_20%,rgba(0,0,0,1)_40%,rgba(0,0,0,0)_50%)]" />
<div className="absolute inset-0 backdrop-blur-[4px] [mask:linear-gradient(rgba(0,0,0,0)_20%,rgba(0,0,0,1)_30%,rgba(0,0,0,1)_50%,rgba(0,0,0,0)_60%)]" />
<div className="absolute inset-0 backdrop-blur-[8px] [mask:linear-gradient(rgba(0,0,0,0)_30%,rgba(0,0,0,1)_40%,rgba(0,0,0,1)_60%,rgba(0,0,0,0)_70%)]" />
<div className="absolute inset-0 backdrop-blur-[16px] [mask:linear-gradient(rgba(0,0,0,0)_40%,rgba(0,0,0,1)_50%,rgba(0,0,0,1)_70%,rgba(0,0,0,0)_80%)]" />
<div className="absolute inset-0 backdrop-blur-[32px] [mask:linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,1)_60%,rgba(0,0,0,1)_80%,rgba(0,0,0,0)_90%)]" />
<div className="absolute inset-0 backdrop-blur-[64px] [mask:linear-gradient(rgba(0,0,0,0)_70%,rgba(0,0,0,1)_80%,rgba(0,0,0,1)_100%)]" />
<div className="absolute inset-0 bg-linear-to-t from-stone-50 dark:from-stone-900 to-transparent" />
</div>
)
}
return (
<div className={`pointer-events-none ${className ?? ""}`}>
<div className="absolute inset-0 backdrop-blur-[64px] [mask:linear-gradient(rgba(0,0,0,1)_0%,rgba(0,0,0,1)_20%,rgba(0,0,0,0)_30%)]" />
<div className="absolute inset-0 backdrop-blur-[32px] [mask:linear-gradient(rgba(0,0,0,0)_10%,rgba(0,0,0,1)_20%,rgba(0,0,0,1)_40%,rgba(0,0,0,0)_50%)]" />
<div className="absolute inset-0 backdrop-blur-[16px] [mask:linear-gradient(rgba(0,0,0,0)_20%,rgba(0,0,0,1)_30%,rgba(0,0,0,1)_50%,rgba(0,0,0,0)_60%)]" />
<div className="absolute inset-0 backdrop-blur-[8px] [mask:linear-gradient(rgba(0,0,0,0)_30%,rgba(0,0,0,1)_40%,rgba(0,0,0,1)_60%,rgba(0,0,0,0)_70%)]" />
<div className="absolute inset-0 backdrop-blur-[4px] [mask:linear-gradient(rgba(0,0,0,0)_40%,rgba(0,0,0,1)_50%,rgba(0,0,0,1)_70%,rgba(0,0,0,0)_80%)]" />
<div className="absolute inset-0 backdrop-blur-[2px] [mask:linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,1)_60%,rgba(0,0,0,1)_80%,rgba(0,0,0,0)_90%)]" />
<div className="absolute inset-0 backdrop-blur-[1px] [mask:linear-gradient(rgba(0,0,0,0)_70%,rgba(0,0,0,1)_80%,rgba(0,0,0,1)_90%,rgba(0,0,0,0)_100%)]" />
<div className="absolute inset-0 bg-linear-to-b from-stone-50 dark:from-stone-900 to-transparent" />
</div>
)
}