Add scroll-reveal animations across home, services, portfolio, about, apps

New ScrollReveal client component wraps section-containers and cards with
a subtle opacity + y-translate fade-in triggered by IntersectionObserver.
Bails out to an unanimated render when prefers-reduced-motion is set.

- home: Selected Works, Inclusive by Design, Start a Project
- services: each of the four verticals and the Recent Work card
- portfolio: each product card, slight stagger by column index
- about: Leadership section and Get in Touch
- apps: each app card, stagger by index
This commit is contained in:
jwinterm
2026-04-20 13:28:03 -04:00
parent 7eb531170d
commit 5c1e3d597f
6 changed files with 160 additions and 72 deletions
+14 -11
View File
@@ -2,6 +2,7 @@ import { FounderSignature } from "@/components/about/founder-signature";
import { ContactForm } from "@/components/contact/contact-form";
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { ScrollReveal } from "@/components/scroll-reveal";
import { FaMapMarkerAlt, FaPhone, FaBuilding, FaExternalLinkAlt } from "react-icons/fa";
import { FaXTwitter, FaTelegram, FaDiscord, FaBluesky, FaTiktok, FaYoutube, FaInstagram, FaLinkedinIn, FaFacebookF, FaPinterestP } from "react-icons/fa6";
import type { Metadata } from "next";
@@ -117,20 +118,22 @@ export default function AboutPage() {
</div>
</div>
<div className="border-t border-border/50 pt-10">
<ScrollReveal className="border-t border-border/50 pt-10">
<h2 className="text-2xl font-bold mb-6 text-center">Leadership</h2>
<FounderSignature />
</ScrollReveal>
</div>
<ScrollReveal as="section" className="mt-16 border-t border-border/50 pt-16">
<div id="contact">
<h2 className="text-3xl font-bold mb-3 tracking-tight">Get in Touch</h2>
<p className="text-muted-foreground text-lg mb-12 max-w-2xl">
Have a project in mind? We'd love to hear about it.
</p>
<ContactForm />
</div>
</div>
<div id="contact" className="mt-16 border-t border-border/50 pt-16">
<h2 className="text-3xl font-bold mb-3 tracking-tight">Get in Touch</h2>
<p className="text-muted-foreground text-lg mb-12 max-w-2xl">
Have a project in mind? We'd love to hear about it.
</p>
<ContactForm />
</div>
</ScrollReveal>
</div>
<MobileNav />
</main>
+20 -17
View File
@@ -1,5 +1,6 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { ScrollReveal } from "@/components/scroll-reveal";
import { StoreButtons } from "@/components/store-buttons";
import Image from "next/image";
import Link from "next/link";
@@ -65,25 +66,27 @@ export default function LinksPage() {
<p className="text-muted-foreground text-center mb-10">Our apps</p>
<div className="space-y-8">
{apps.map((app) => (
<div key={app.name} className={`rounded-xl border ${app.borderColor} bg-card p-6`}>
<div className="flex items-center gap-4 mb-3">
<Image
src={app.icon}
alt={`${app.name} icon`}
width={56}
height={56}
className="rounded-xl"
/>
<div>
<Link href={app.href}>
<h2 className={`text-xl font-bold ${app.color} hover:underline`}>{app.name}</h2>
</Link>
<p className="text-sm text-muted-foreground">{app.tagline}</p>
{apps.map((app, i) => (
<ScrollReveal key={app.name} delay={i * 0.08}>
<div className={`rounded-xl border ${app.borderColor} bg-card p-6`}>
<div className="flex items-center gap-4 mb-3">
<Image
src={app.icon}
alt={`${app.name} icon`}
width={56}
height={56}
className="rounded-xl"
/>
<div>
<Link href={app.href}>
<h2 className={`text-xl font-bold ${app.color} hover:underline`}>{app.name}</h2>
</Link>
<p className="text-sm text-muted-foreground">{app.tagline}</p>
</div>
</div>
<StoreButtons links={app.links} />
</div>
<StoreButtons links={app.links} />
</div>
</ScrollReveal>
))}
</div>
+10 -9
View File
@@ -7,6 +7,7 @@ import { ServicesPreview } from "@/components/landing/services-preview";
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { DynamicBackground } from "@/components/landing/dynamic-background";
import { ScrollReveal } from "@/components/scroll-reveal";
import Link from "next/link";
import { Button } from "@repo/ui/components/button";
import { useRef, useState, useEffect } from "react";
@@ -82,8 +83,8 @@ export default function Home() {
</div>
<ServicesPreview />
<div className="w-full max-w-6xl mx-auto mb-16 px-4">
<ScrollReveal className="w-full max-w-6xl mx-auto mb-16 px-4">
<div className="text-left mb-8 section-container border-l-4 border-l-blue-500">
<h2 className="text-3xl font-bold mb-3 tracking-tight text-foreground">Selected Works</h2>
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
@@ -91,28 +92,28 @@ export default function Home() {
</p>
</div>
<PortfolioGrid />
</div>
</ScrollReveal>
<div className="w-full max-w-6xl mx-auto mb-20 px-4">
<ScrollReveal className="w-full max-w-6xl mx-auto mb-20 px-4">
<div className="text-left mb-12 section-container border-l-4 border-l-purple-500">
<h2 className="text-3xl font-bold mb-3 tracking-tight text-foreground">Inclusive by Design</h2>
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
We believe the web is for everyone. Every line of code we write is
We believe the web is for everyone. Every line of code we write is
audited for WCAG 2.2 compliance and high-performance across all devices.
</p>
</div>
<FounderSignature />
</div>
</ScrollReveal>
<div id="contact" className="w-full mt-20 mb-20 pb-16 md:pb-0 flex flex-col items-center">
<div className="w-full max-w-6xl text-left mb-12 section-container border-l-4 border-l-cyan-500">
<ScrollReveal as="section" className="w-full mt-20 mb-20 pb-16 md:pb-0 flex flex-col items-center">
<div id="contact" className="w-full max-w-6xl text-left mb-12 section-container border-l-4 border-l-cyan-500">
<h2 className="text-3xl font-bold mb-3 tracking-tight text-foreground">Start a Project</h2>
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
Have a project in mind? Let's talk about what you need and how we can help.
</p>
</div>
<ContactForm />
</div>
</ScrollReveal>
</div>
<MobileNav />
</main>
+23 -20
View File
@@ -1,5 +1,6 @@
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { ScrollReveal } from "@/components/scroll-reveal";
import { Card } from "@repo/ui/components/card";
import Image from "next/image";
import Link from "next/link";
@@ -112,26 +113,28 @@ export default function ProductsPage() {
</div>
<div className="grid md:grid-cols-2 gap-8">
{products.map((product) => (
<Link key={product.slug} href={`/products/${product.slug}`}>
<Card className={`glass-card h-full min-h-[200px] ${colorClasses[product.color]} transition-all cursor-pointer`}>
<div className="flex items-center gap-4 mb-3">
<Image
src={product.icon}
alt={`${product.title} icon`}
width={56}
height={56}
className="rounded-xl flex-shrink-0"
/>
<h2 className={`text-2xl font-bold ${titleColors[product.color]}`}>
{product.title}
</h2>
</div>
<p className="text-muted-foreground leading-relaxed">
{product.description}
</p>
</Card>
</Link>
{products.map((product, i) => (
<ScrollReveal key={product.slug} delay={(i % 2) * 0.08}>
<Link href={`/products/${product.slug}`}>
<Card className={`glass-card h-full min-h-[200px] ${colorClasses[product.color]} transition-all cursor-pointer`}>
<div className="flex items-center gap-4 mb-3">
<Image
src={product.icon}
alt={`${product.title} icon`}
width={56}
height={56}
className="rounded-xl flex-shrink-0"
/>
<h2 className={`text-2xl font-bold ${titleColors[product.color]}`}>
{product.title}
</h2>
</div>
<p className="text-muted-foreground leading-relaxed">
{product.description}
</p>
</Card>
</Link>
</ScrollReveal>
))}
</div>
+16 -15
View File
@@ -2,6 +2,7 @@ import { DataGrid } from "@repo/ui/procedural/data-grid";
import { Button } from "@repo/ui/components/button";
import { Header } from "@/components/layout/header";
import { MobileNav } from "@/components/layout/mobile-nav";
import { ScrollReveal } from "@/components/scroll-reveal";
import Image from "next/image";
import Link from "next/link";
import { Card } from "@repo/ui/components/card";
@@ -38,7 +39,7 @@ export default function ServicesPage() {
</div>
{/* Philosophy Card */}
<div className="mb-20">
<ScrollReveal className="mb-20">
<div className="text-left section-container border-l-4 border-l-emerald-500">
<h2 className="text-3xl font-bold mb-2">Partnership-First Development</h2>
<p className="text-muted-foreground text-lg max-w-3xl">
@@ -46,10 +47,10 @@ export default function ServicesPage() {
we provide the engineering. We design systems that grow with your business.
</p>
</div>
</div>
</ScrollReveal>
{/* E-Commerce Section */}
<div className="mb-24">
<ScrollReveal className="mb-24">
<div className="text-left mb-10 section-container border-l-4 border-l-blue-500">
<h2 className="text-3xl font-bold mb-2">Custom E-Commerce Engines</h2>
<p className="text-muted-foreground text-lg max-w-3xl">
@@ -80,10 +81,10 @@ export default function ServicesPage() {
</p>
</Card>
</div>
</div>
</ScrollReveal>
{/* Custom Apps Section */}
<div className="mb-24">
<ScrollReveal className="mb-24">
<div className="text-left mb-10 section-container border-l-4 border-l-purple-500">
<h2 className="text-3xl font-bold mb-2">Custom Apps & Interactive Websites</h2>
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
@@ -95,7 +96,7 @@ export default function ServicesPage() {
<Card className="glass-card hover:border-purple-500/30">
<h3 className="text-xl font-bold mb-3">Real-time Platforms</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Socket.io and WebRTC integration for collaborative tools, dashboards,
Socket.io and WebRTC integration for collaborative tools, dashboards,
and live data visualization.
</p>
</Card>
@@ -106,10 +107,10 @@ export default function ServicesPage() {
</p>
</Card>
</div>
</div>
</ScrollReveal>
{/* Crypto Section */}
<div className="mb-24">
<ScrollReveal className="mb-24">
<div className="text-left mb-10 section-container border-l-4 border-l-cyan-500">
<h2 className="text-3xl font-bold mb-2">Cryptocurrency & Payment Consulting</h2>
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
@@ -120,22 +121,22 @@ export default function ServicesPage() {
<Card className="glass-card hover:border-cyan-500/30">
<h3 className="text-xl font-bold mb-3">Payment Orchestration</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Multi-PSP routing, automated reconciliation, and fraud prevention for
Multi-PSP routing, automated reconciliation, and fraud prevention for
high-volume merchants.
</p>
</Card>
<Card className="glass-card hover:border-cyan-500/30">
<h3 className="text-xl font-bold mb-3">Web3 Integration</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Non-custodial wallet auth, on/off ramp solutions, and smart contract
Non-custodial wallet auth, on/off ramp solutions, and smart contract
interactions for modern dApps.
</p>
</Card>
</div>
</div>
</ScrollReveal>
{/* Recent Work */}
<div className="mb-24">
<ScrollReveal className="mb-24">
<div className="text-left mb-10 section-container border-l-4 border-l-amber-500">
<h2 className="text-3xl font-bold mb-2">Recent Work</h2>
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
@@ -167,9 +168,9 @@ export default function ServicesPage() {
With us since day one. Five years on WordPress, migrating to a custom Medusa v2 storefront this spring.
</p>
</Card>
</div>
</ScrollReveal>
<div className="mt-20 p-16 rounded-[2.5rem] bg-gradient-to-br from-slate-900 via-slate-800 to-indigo-950 text-white text-center shadow-2xl relative overflow-hidden group border-2 border-white/5">
<ScrollReveal className="mt-20 p-16 rounded-[2.5rem] bg-gradient-to-br from-slate-900 via-slate-800 to-indigo-950 text-white text-center shadow-2xl relative overflow-hidden group border-2 border-white/5">
<div className="absolute inset-0 bg-blue-500/10 opacity-0 group-hover:opacity-100 transition-opacity" />
<h2 className="text-4xl font-bold mb-6 relative z-10 tracking-tight">Let's build together</h2>
<p className="mb-10 text-slate-300 text-lg max-w-2xl mx-auto relative z-10 leading-relaxed font-medium">
@@ -181,7 +182,7 @@ export default function ServicesPage() {
Request a Consultation
</Button>
</Link>
</div>
</ScrollReveal>
</div>
<MobileNav />
</main>
+77
View File
@@ -0,0 +1,77 @@
'use client';
import { motion, useReducedMotion, type Variants } from 'framer-motion';
import { createElement, type ReactNode } from 'react';
type Direction = 'up' | 'down' | 'left' | 'right' | 'none';
type Tag = 'div' | 'section' | 'article' | 'li';
interface ScrollRevealProps {
children: ReactNode;
direction?: Direction;
delay?: number;
duration?: number;
distance?: number;
amount?: number;
className?: string;
as?: Tag;
}
const axisFor = (direction: Direction, distance: number) => {
switch (direction) {
case 'up': return { x: 0, y: distance };
case 'down': return { x: 0, y: -distance };
case 'left': return { x: distance, y: 0 };
case 'right': return { x: -distance, y: 0 };
case 'none': return { x: 0, y: 0 };
}
};
export function ScrollReveal({
children,
direction = 'up',
delay = 0,
duration = 0.6,
distance = 24,
amount = 0.2,
className,
as = 'div',
}: ScrollRevealProps) {
const prefersReducedMotion = useReducedMotion();
if (prefersReducedMotion) {
return createElement(as, { className }, children);
}
const offset = axisFor(direction, distance);
const variants: Variants = {
hidden: { opacity: 0, ...offset },
visible: {
opacity: 1,
x: 0,
y: 0,
transition: {
duration,
delay,
ease: [0.16, 1, 0.3, 1],
},
},
};
const common = {
className,
initial: 'hidden',
whileInView: 'visible',
viewport: { once: true, amount },
variants,
children,
};
const Component =
as === 'section' ? motion.section :
as === 'article' ? motion.article :
as === 'li' ? motion.li :
motion.div;
return createElement(Component as never, common);
}