mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
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:
@@ -2,6 +2,7 @@ import { FounderSignature } from "@/components/about/founder-signature";
|
|||||||
import { ContactForm } from "@/components/contact/contact-form";
|
import { ContactForm } from "@/components/contact/contact-form";
|
||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import { MobileNav } from "@/components/layout/mobile-nav";
|
import { MobileNav } from "@/components/layout/mobile-nav";
|
||||||
|
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||||
import { FaMapMarkerAlt, FaPhone, FaBuilding, FaExternalLinkAlt } from "react-icons/fa";
|
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 { FaXTwitter, FaTelegram, FaDiscord, FaBluesky, FaTiktok, FaYoutube, FaInstagram, FaLinkedinIn, FaFacebookF, FaPinterestP } from "react-icons/fa6";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
@@ -117,20 +118,22 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<h2 className="text-2xl font-bold mb-6 text-center">Leadership</h2>
|
||||||
<FounderSignature />
|
<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>
|
||||||
|
</ScrollReveal>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import { MobileNav } from "@/components/layout/mobile-nav";
|
import { MobileNav } from "@/components/layout/mobile-nav";
|
||||||
|
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||||
import { StoreButtons } from "@/components/store-buttons";
|
import { StoreButtons } from "@/components/store-buttons";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
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>
|
<p className="text-muted-foreground text-center mb-10">Our apps</p>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{apps.map((app) => (
|
{apps.map((app, i) => (
|
||||||
<div key={app.name} className={`rounded-xl border ${app.borderColor} bg-card p-6`}>
|
<ScrollReveal key={app.name} delay={i * 0.08}>
|
||||||
<div className="flex items-center gap-4 mb-3">
|
<div className={`rounded-xl border ${app.borderColor} bg-card p-6`}>
|
||||||
<Image
|
<div className="flex items-center gap-4 mb-3">
|
||||||
src={app.icon}
|
<Image
|
||||||
alt={`${app.name} icon`}
|
src={app.icon}
|
||||||
width={56}
|
alt={`${app.name} icon`}
|
||||||
height={56}
|
width={56}
|
||||||
className="rounded-xl"
|
height={56}
|
||||||
/>
|
className="rounded-xl"
|
||||||
<div>
|
/>
|
||||||
<Link href={app.href}>
|
<div>
|
||||||
<h2 className={`text-xl font-bold ${app.color} hover:underline`}>{app.name}</h2>
|
<Link href={app.href}>
|
||||||
</Link>
|
<h2 className={`text-xl font-bold ${app.color} hover:underline`}>{app.name}</h2>
|
||||||
<p className="text-sm text-muted-foreground">{app.tagline}</p>
|
</Link>
|
||||||
|
<p className="text-sm text-muted-foreground">{app.tagline}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<StoreButtons links={app.links} />
|
||||||
</div>
|
</div>
|
||||||
<StoreButtons links={app.links} />
|
</ScrollReveal>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ServicesPreview } from "@/components/landing/services-preview";
|
|||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import { MobileNav } from "@/components/layout/mobile-nav";
|
import { MobileNav } from "@/components/layout/mobile-nav";
|
||||||
import { DynamicBackground } from "@/components/landing/dynamic-background";
|
import { DynamicBackground } from "@/components/landing/dynamic-background";
|
||||||
|
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@repo/ui/components/button";
|
import { Button } from "@repo/ui/components/button";
|
||||||
import { useRef, useState, useEffect } from "react";
|
import { useRef, useState, useEffect } from "react";
|
||||||
@@ -83,7 +84,7 @@ export default function Home() {
|
|||||||
|
|
||||||
<ServicesPreview />
|
<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">
|
<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>
|
<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">
|
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||||
@@ -91,9 +92,9 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PortfolioGrid />
|
<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">
|
<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>
|
<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">
|
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||||
@@ -102,17 +103,17 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FounderSignature />
|
<FounderSignature />
|
||||||
</div>
|
</ScrollReveal>
|
||||||
|
|
||||||
<div id="contact" className="w-full mt-20 mb-20 pb-16 md:pb-0 flex flex-col items-center">
|
<ScrollReveal as="section" 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">
|
<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>
|
<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">
|
<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.
|
Have a project in mind? Let's talk about what you need and how we can help.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ContactForm />
|
<ContactForm />
|
||||||
</div>
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import { MobileNav } from "@/components/layout/mobile-nav";
|
import { MobileNav } from "@/components/layout/mobile-nav";
|
||||||
|
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -112,26 +113,28 @@ export default function ProductsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-8">
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
{products.map((product) => (
|
{products.map((product, i) => (
|
||||||
<Link key={product.slug} href={`/products/${product.slug}`}>
|
<ScrollReveal key={product.slug} delay={(i % 2) * 0.08}>
|
||||||
<Card className={`glass-card h-full min-h-[200px] ${colorClasses[product.color]} transition-all cursor-pointer`}>
|
<Link href={`/products/${product.slug}`}>
|
||||||
<div className="flex items-center gap-4 mb-3">
|
<Card className={`glass-card h-full min-h-[200px] ${colorClasses[product.color]} transition-all cursor-pointer`}>
|
||||||
<Image
|
<div className="flex items-center gap-4 mb-3">
|
||||||
src={product.icon}
|
<Image
|
||||||
alt={`${product.title} icon`}
|
src={product.icon}
|
||||||
width={56}
|
alt={`${product.title} icon`}
|
||||||
height={56}
|
width={56}
|
||||||
className="rounded-xl flex-shrink-0"
|
height={56}
|
||||||
/>
|
className="rounded-xl flex-shrink-0"
|
||||||
<h2 className={`text-2xl font-bold ${titleColors[product.color]}`}>
|
/>
|
||||||
{product.title}
|
<h2 className={`text-2xl font-bold ${titleColors[product.color]}`}>
|
||||||
</h2>
|
{product.title}
|
||||||
</div>
|
</h2>
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
</div>
|
||||||
{product.description}
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
</p>
|
{product.description}
|
||||||
</Card>
|
</p>
|
||||||
</Link>
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</ScrollReveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { DataGrid } from "@repo/ui/procedural/data-grid";
|
|||||||
import { Button } from "@repo/ui/components/button";
|
import { Button } from "@repo/ui/components/button";
|
||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import { MobileNav } from "@/components/layout/mobile-nav";
|
import { MobileNav } from "@/components/layout/mobile-nav";
|
||||||
|
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
@@ -38,7 +39,7 @@ export default function ServicesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Philosophy Card */}
|
{/* Philosophy Card */}
|
||||||
<div className="mb-20">
|
<ScrollReveal className="mb-20">
|
||||||
<div className="text-left section-container border-l-4 border-l-emerald-500">
|
<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>
|
<h2 className="text-3xl font-bold mb-2">Partnership-First Development</h2>
|
||||||
<p className="text-muted-foreground text-lg max-w-3xl">
|
<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.
|
we provide the engineering. We design systems that grow with your business.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* E-Commerce Section */}
|
{/* 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">
|
<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>
|
<h2 className="text-3xl font-bold mb-2">Custom E-Commerce Engines</h2>
|
||||||
<p className="text-muted-foreground text-lg max-w-3xl">
|
<p className="text-muted-foreground text-lg max-w-3xl">
|
||||||
@@ -80,10 +81,10 @@ export default function ServicesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Custom Apps Section */}
|
{/* 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">
|
<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>
|
<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">
|
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
|
||||||
@@ -106,10 +107,10 @@ export default function ServicesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Crypto Section */}
|
{/* 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">
|
<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>
|
<h2 className="text-3xl font-bold mb-2">Cryptocurrency & Payment Consulting</h2>
|
||||||
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
|
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
|
||||||
@@ -132,10 +133,10 @@ export default function ServicesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollReveal>
|
||||||
|
|
||||||
{/* Recent Work */}
|
{/* 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">
|
<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>
|
<h2 className="text-3xl font-bold mb-2">Recent Work</h2>
|
||||||
<p className="text-muted-foreground text-lg max-w-2xl leading-relaxed">
|
<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.
|
With us since day one. Five years on WordPress, migrating to a custom Medusa v2 storefront this spring.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</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" />
|
<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>
|
<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">
|
<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
|
Request a Consultation
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user