Skip to content

Commit

Permalink
feat(ui): add animations
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonsaldan committed Jan 1, 2025
1 parent 5acd678 commit 2efc1fe
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 49 deletions.
124 changes: 84 additions & 40 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Heading, Subheading } from '@/components/text'
import { ChevronRightIcon } from '@heroicons/react/16/solid'
import dynamic from 'next/dynamic'
import 'prismjs/themes/prism.css'
import { useEffect, useRef } from 'react'

const CodeBlock = dynamic(() => import('@/components/code-block'), {
ssr: false,
Expand Down Expand Up @@ -116,6 +117,45 @@ function BentoSection() {
}

function SupportCTA() {
const ctaRef = useRef<HTMLDivElement>(null)
const imageRef = useRef<HTMLImageElement>(null)

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (ctaRef.current) {
ctaRef.current.classList.remove('opacity-0', 'scale-95')
ctaRef.current.classList.add('opacity-100', 'scale-100')
}
if (imageRef.current) {
setTimeout(() => {
if (imageRef.current) {
imageRef.current.classList.remove(
'opacity-0',
'translate-x-8',
)
imageRef.current.classList.add('opacity-100', 'translate-x-0')
}
}, 200)
}
observer.unobserve(entry.target)
}
})
},
{
threshold: 0.2,
},
)

if (ctaRef.current) {
observer.observe(ctaRef.current)
}

return () => observer.disconnect()
}, [])

return (
<Container>
<Subheading>Support Nocturne</Subheading>
Expand All @@ -124,49 +164,53 @@ function SupportCTA() {
</Heading>

<div className="mt-10 pb-20 sm:mt-16">
<div className="relative isolate h-auto overflow-hidden rounded-3xl bg-[linear-gradient(145deg,var(--tw-gradient-stops))] from-[#7456c1] via-[#fa6767] via-[70%] to-[#ff4d4a] px-6 pb-0 pt-16 shadow-sm ring-1 ring-black/5 data-[dark]:bg-gray-800 data-[dark]:ring-white/15 sm:px-16 md:pt-24 lg:flex lg:h-[380px] lg:items-center lg:gap-x-20 lg:pb-0 lg:pl-16 lg:pr-20 lg:pt-0">
<div className="mx-auto max-w-md lg:mx-0 lg:flex-auto lg:text-left">
<h4 className="text-balance text-3xl font-medium tracking-tight text-white group-data-[dark]:text-white">
Choose Your Support Method
</h4>
<p className="mt-4 text-pretty text-lg/8 text-white">
Nocturne is a community-driven initiative. If you find it
valuable, consider supporting our work through a donation. All
contributions go towards development and maintenance.
</p>
<Link href="/about">
<p className="duration-350 mt-4 text-pretty text-lg/8 text-white/80 transition ease-in-out hover:text-white/60">
Learn more about our mission →
<div
ref={ctaRef}
className="transform opacity-0 transition-all duration-700 ease-out"
>
<div className="relative isolate h-auto overflow-hidden rounded-3xl bg-[linear-gradient(145deg,var(--tw-gradient-stops))] from-[#7456c1] via-[#fa6767] via-[70%] to-[#ff4d4a] px-6 pb-0 pt-16 shadow-sm ring-1 ring-black/5 data-[dark]:bg-gray-800 data-[dark]:ring-white/15 sm:px-16 md:pt-24 lg:flex lg:h-[380px] lg:items-center lg:gap-x-20 lg:pb-0 lg:pl-16 lg:pr-20 lg:pt-0">
<div className="mx-auto max-w-md lg:mx-0 lg:flex-auto lg:text-left">
<h4 className="text-balance text-3xl font-medium tracking-tight text-white group-data-[dark]:text-white">
Choose Your Support Method
</h4>
<p className="mt-4 text-pretty text-lg/8 text-white">
Nocturne is a community-driven initiative. If you find it
valuable, consider supporting our work through a donation. All
contributions go towards development and maintenance.
</p>
</Link>
<div className="mt-6 flex flex-col gap-4 sm:flex-row sm:justify-start">
<div className="w-full sm:w-[220px]">
<Button
href="https://buymeacoffee.com/brandonsaldan"
className="w-full"
>
Buy Me a Coffee
</Button>
</div>
<div className="w-full sm:w-[220px]">
<Button
href="https://ko-fi.com/brandonsaldan"
className="w-full"
>
Ko-Fi
</Button>
<Link href="/about">
<p className="duration-350 mt-4 text-pretty text-lg/8 text-white/80 transition ease-in-out hover:text-white/60">
Learn more about our mission →
</p>
</Link>
<div className="mt-6 flex flex-col gap-4 sm:flex-row sm:justify-start">
<div className="w-full sm:w-[220px]">
<Button
href="https://buymeacoffee.com/brandonsaldan"
className="w-full"
>
Buy Me a Coffee
</Button>
</div>
<div className="w-full sm:w-[220px]">
<Button
href="https://ko-fi.com/brandonsaldan"
className="w-full"
>
Ko-Fi
</Button>
</div>
</div>
</div>
</div>

<div className="relative mt-8 h-64 sm:h-80 lg:mt-2">
<img
alt="Nocturne Screenshot"
src="/images/nocturne-2.png"
width={1824}
height={1080}
className="pointer-events-none absolute top-0 w-[41rem] max-w-none rounded-md sm:left-6 sm:w-[57rem]"
/>
<div className="relative mt-8 h-64 sm:h-80 lg:mt-2">
<img
ref={imageRef}
alt="Nocturne Screenshot"
src="/images/nocturne-2.png"
className="pointer-events-none absolute top-0 w-[41rem] max-w-none translate-x-8 transform rounded-md opacity-0 transition-all duration-700 ease-out sm:left-6 sm:w-[57rem]"
/>
</div>
</div>
</div>
</div>
Expand All @@ -179,7 +223,7 @@ export default function Home() {
<div className="overflow-hidden">
<Hero />
<main>
<div className="bg-gradient-to-b from-white from-50% to-gray-100 pt-20">
<div className="bg-gradient-to-b from-white from-50% to-gray-100 pt-8 sm:pt-20">
<BentoSection />
<Testimonials />
<SupportCTA />
Expand Down
8 changes: 8 additions & 0 deletions src/components/bento-card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { useScrollAnimation } from '@/hooks/useScrollAnimation'
import { clsx } from 'clsx'
import { motion } from 'framer-motion'
import { Subheading } from './text'
Expand All @@ -21,8 +22,14 @@ export function BentoCard({
graphic: React.ReactNode
fade?: ('top' | 'bottom')[]
}) {
const cardRef = useScrollAnimation<HTMLDivElement>({
y: 8,
threshold: 0.2,
})

return (
<motion.div
ref={cardRef}
initial="idle"
whileHover="active"
variants={{ idle: {}, active: {} }}
Expand All @@ -32,6 +39,7 @@ export function BentoCard({
'group relative flex flex-col overflow-hidden rounded-lg',
'bg-white shadow-sm ring-1 ring-black/5',
'data-[dark]:bg-gray-800 data-[dark]:ring-white/15',
'translate-y-8 transform opacity-0 transition-all duration-700 ease-out',
)}
>
<div className="relative h-80 shrink-0">{graphic}</div>
Expand Down
53 changes: 52 additions & 1 deletion src/components/interstate-rows.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { clsx } from 'clsx'
import { useEffect, useRef, useState } from 'react'

function Row({ children }: { children: React.ReactNode }) {
return (
Expand All @@ -14,18 +15,24 @@ function Logo({
label,
src,
className,
isActive,
}: {
label: string
src: string
className: string
isActive: boolean
}) {
return (
<div
className={clsx(
className,
'absolute top-2 grid grid-cols-[1rem,1fr] items-center gap-2 whitespace-nowrap px-3 py-1',
'rounded-full bg-gradient-to-t from-gray-50 from-50% to-gray-100 ring-1 ring-inset ring-white/10',
'[--move-x-from:-100%] [--move-x-to:calc(100%+100cqw)] [animation-iteration-count:infinite] [animation-name:move-x] [animation-play-state:paused] [animation-timing-function:linear] group-hover:[animation-play-state:running]',
'[--move-x-from:-100%] [--move-x-to:calc(100%+100cqw)] [animation-iteration-count:infinite] [animation-name:move-x] [animation-timing-function:linear]',
isActive
? '[animation-play-state:running]'
: '[animation-play-state:paused]',
'group-hover:[animation-play-state:running]',
)}
>
<img alt="" src={src} className="size-4" />
Expand All @@ -35,6 +42,38 @@ function Logo({
}

export function InterstateRows() {
const [isInView, setIsInView] = useState(false)
const [isMobile, setIsMobile] = useState(false)
const hasAnimated = useRef(false)
const containerRef = useRef<HTMLDivElement>(null)

useEffect(() => {
setIsMobile(window.innerWidth < 640)
}, [])

useEffect(() => {
if (!isMobile || hasAnimated.current) return

const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !hasAnimated.current) {
setIsInView(true)
hasAnimated.current = true
observer.disconnect()
}
})
},
{ threshold: 0.3 },
)

if (containerRef.current) {
observer.observe(containerRef.current)
}

return () => observer.disconnect()
}, [isMobile])

return (
<div aria-hidden="true" className="relative h-full overflow-hidden">
<div className="absolute inset-0 grid grid-cols-1 pt-8 [container-type:inline-size]">
Expand All @@ -43,71 +82,83 @@ export function InterstateRows() {
label="I-10"
src="/interstate/I-10.svg"
className="[animation-delay:-26s] [animation-duration:30s]"
isActive={isMobile && isInView}
/>
<Logo
label="I-95"
src="/interstate/I-95.svg"
className="[animation-delay:-8s] [animation-duration:30s]"
isActive={isMobile && isInView}
/>
</Row>
<Row>
<Logo
label="I-90"
src="/interstate/I-90.svg"
className="[animation-delay:-40s] [animation-duration:40s]"
isActive={isMobile && isInView}
/>
<Logo
label="I-15"
src="/interstate/I-15.svg"
className="[animation-delay:-20s] [animation-duration:40s]"
isActive={isMobile && isInView}
/>
</Row>
<Row>
<Logo
label="I-25"
src="/interstate/I-25.svg"
className="[animation-delay:-10s] [animation-duration:40s]"
isActive={isMobile && isInView}
/>
<Logo
label="I-42"
src="/interstate/I-42.svg"
className="[animation-delay:-32s] [animation-duration:40s]"
isActive={isMobile && isInView}
/>
</Row>
<Row>
<Logo
label="I-55"
src="/interstate/I-55.svg"
className="[animation-delay:-45s] [animation-duration:45s]"
isActive={isMobile && isInView}
/>
<Logo
label="I-4"
src="/interstate/I-4.svg"
className="[animation-delay:-23s] [animation-duration:45s]"
isActive={isMobile && isInView}
/>
</Row>
<Row>
<Logo
label="I-57"
src="/interstate/I-57.svg"
className="[animation-delay:-55s] [animation-duration:60s]"
isActive={isMobile && isInView}
/>
<Logo
label="I-68"
src="/interstate/I-68.svg"
className="[animation-delay:-20s] [animation-duration:60s]"
isActive={isMobile && isInView}
/>
</Row>
<Row>
<Logo
label="I-76"
src="/interstate/I-76.svg"
className="[animation-delay:-9s] [animation-duration:40s]"
isActive={isMobile && isInView}
/>
<Logo
label="I-81"
src="/interstate/I-81.svg"
className="[animation-delay:-28s] [animation-duration:40s]"
isActive={isMobile && isInView}
/>
</Row>
</div>
Expand Down
Loading

0 comments on commit 2efc1fe

Please sign in to comment.