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 { 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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