Skip to content

Commit a4f5e94

Browse files
committed
feat: enhance public page animations and improve component structure
- Added `PublicPageReveal` component for animated page transitions using GSAP, integrated into the layout. - Introduced `AnimatedOrbitalLogo` component for reusable animated SVG branding with reduced-motion support. - Created `SectionLayout` and `SectionBackground` components for consistent section styling across public pages. - Implemented `useSectionReveal` hook for unified section-level reveal animations with GSAP ScrollTrigger. - Updated various components to use optional chaining and improved nullish coalescing for better readability and safety. - Upgraded dependencies for `@tanstack/react-query` and `@tanstack/react-query-devtools` to latest versions. - Enhanced accessibility and performance by ensuring reduced-motion settings are respected in animations. - Refactored existing components to improve code clarity and maintainability.
1 parent 5eaa295 commit a4f5e94

36 files changed

Lines changed: 1183 additions & 726 deletions

.github/skills/browser/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"dependencies": {
3-
"ws": "^8.18.3"
4-
}
5-
}
2+
"dependencies": {
3+
"ws": "^8.19.0"
4+
}
5+
}

app/AGENTS.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- AGENTS-META {"title":"Next.js App Directory","version":"1.0.0","applies_to":"app/","last_updated":"2025-11-27T00:00:00Z","status":"stable"} -->
1+
<!-- AGENTS-META {"title":"Next.js App Directory","version":"1.0.1","applies_to":"app/","last_updated":"2026-02-15T00:00:00Z","status":"stable"} -->
22

33
# Next.js App Directory
44

@@ -56,6 +56,15 @@ From `components.json`:
5656
- `src/mastra/`: Backend agents and tools
5757
- `next.config.ts`: Next.js configuration
5858

59+
## Recent Updates
60+
61+
- 2026-02-15: Added reusable GSAP-powered custom SVG brand animation component at `app/components/gsap/animated-orbital-logo.tsx`.
62+
- 2026-02-15: Integrated animated SVG branding into shared public UI components:
63+
- `app/components/navbar.tsx`
64+
- `app/components/landing-hero.tsx`
65+
- `app/components/footer.tsx`
66+
- Accessibility/perf: each animation path respects `prefers-reduced-motion` behavior.
67+
5968
---
6069

61-
Last updated: 2025-11-27
70+
Last updated: 2026-02-15

app/components/footer.tsx

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
'use client'
22

3-
import { useEffect, useState } from 'react'
3+
import { useState } from 'react'
4+
import { useRef } from 'react'
45
import Link from 'next/link'
6+
import { useGSAP } from '@gsap/react'
7+
import { gsap } from 'gsap'
58
import { Button } from '@/ui/button'
69
import { Input } from '@/ui/input'
10+
import { ensureGsapRegistered } from '@/app/components/gsap/registry'
11+
import { AnimatedOrbitalLogo } from '@/app/components/gsap/animated-orbital-logo'
712
import { ArrowRightIcon, CheckIcon, LoaderIcon } from 'lucide-react'
813

914
const FOOTER_LINKS = {
@@ -64,6 +69,7 @@ const SOCIAL_LINKS = [
6469
]
6570

6671
export function Footer() {
72+
const footerRef = useRef<HTMLElement>(null)
6773
const [email, setEmail] = useState('')
6874
const [status, setStatus] = useState<
6975
'idle' | 'loading' | 'success' | 'error'
@@ -83,16 +89,52 @@ export function Footer() {
8389
setTimeout(() => setStatus('idle'), 3000)
8490
}
8591

86-
const [year, setYear] = useState<number | null>(null)
92+
const year = new Date().getFullYear()
8793

88-
useEffect(() => {
89-
setYear(new Date().getFullYear())
90-
}, [])
94+
useGSAP(
95+
() => {
96+
ensureGsapRegistered()
97+
98+
const targets = gsap.utils.toArray<HTMLElement>(
99+
'[data-footer-reveal]'
100+
)
101+
102+
if (targets.length === 0) {
103+
return
104+
}
105+
106+
const prefersReducedMotion = window.matchMedia(
107+
'(prefers-reduced-motion: reduce)'
108+
).matches
109+
110+
if (prefersReducedMotion) {
111+
gsap.set(targets, { autoAlpha: 1, y: 0 })
112+
return
113+
}
114+
115+
gsap.set(targets, { autoAlpha: 0, y: 18 })
116+
targets.forEach((target, index) => {
117+
gsap.to(target, {
118+
autoAlpha: 1,
119+
y: 0,
120+
duration: 0.45,
121+
ease: 'power2.out',
122+
delay: index * 0.04,
123+
scrollTrigger: {
124+
trigger: target,
125+
start: 'top 92%',
126+
once: true,
127+
},
128+
})
129+
})
130+
},
131+
{ scope: footerRef }
132+
)
91133

92134
return (
93-
<footer className="border-t border-border bg-background">
135+
<footer ref={footerRef} className="border-t border-border bg-background">
94136
{/* Newsletter section */}
95-
<div className="border-b border-border">
137+
<div className="border-b border-border" data-footer-reveal>
96138
<div className="container mx-auto px-4 py-12">
97139
<div className="mx-auto max-w-2xl text-center">
98140
<h3 className="mb-2 text-2xl font-bold text-foreground">
@@ -103,7 +145,9 @@ export function Footer() {
103145
practices, and AI agent development tips.
104146
</p>
105147
<form
106-
onSubmit={handleSubscribe}
148+
onSubmit={(event) => {
149+
void handleSubscribe(event)
150+
}}
107151
className="flex flex-col gap-3 sm:flex-row sm:gap-2"
108152
>
109153
<div className="relative flex-1">
@@ -122,7 +166,7 @@ export function Footer() {
122166
<Button
123167
type="submit"
124168
size="lg"
125-
className="h-12 min-w-[140px] transition-all duration-200 ease-spring hover:-translate-y-px"
169+
className="h-12 min-w-35 transition-all duration-200 ease-spring hover:-translate-y-px"
126170
disabled={
127171
status === 'loading' || status === 'success'
128172
}
@@ -154,18 +198,19 @@ export function Footer() {
154198
</div>
155199

156200
{/* Main footer content */}
157-
<div className="container mx-auto px-4 py-12 lg:py-16">
201+
<div className="container mx-auto px-4 py-12 lg:py-16" data-footer-reveal>
158202
<div className="grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-6">
159203
{/* Brand column */}
160204
<div className="col-span-2 md:col-span-3 lg:col-span-2">
161205
<Link
162206
href="/"
163207
className="group flex items-center gap-2 transition-opacity duration-200 hover:opacity-80"
164208
>
165-
<div className="flex size-10 items-center justify-center rounded-lg bg-linear-to-br from-primary to-primary/80 shadow-lg transition-all duration-200 ease-spring group-hover:scale-105 group-hover:shadow-xl">
166-
<span className="font-bold text-primary-foreground text-lg">
167-
A
168-
</span>
209+
<div className="flex size-10 items-center justify-center rounded-lg bg-linear-to-br from-primary/15 to-primary/5 shadow-lg transition-all duration-200 ease-spring group-hover:scale-105 group-hover:shadow-xl">
210+
<AnimatedOrbitalLogo
211+
size={26}
212+
className="text-primary"
213+
/>
169214
</div>
170215
<span className="text-xl font-bold text-foreground">
171216
AgentStack
@@ -271,10 +316,10 @@ export function Footer() {
271316
</div>
272317

273318
{/* Bottom bar */}
274-
<div className="border-t border-border">
319+
<div className="border-t border-border" data-footer-reveal>
275320
<div className="container mx-auto flex flex-col items-center justify-between gap-4 px-4 py-6 md:flex-row">
276321
<div className="flex flex-col items-center gap-2 text-sm text-muted-foreground md:flex-row md:gap-4">
277-
<p>© {year ?? ''} AgentStack. All rights reserved.</p>
322+
<p>© {year} AgentStack. All rights reserved.</p>
278323
<span className="hidden md:inline"></span>
279324
<p>Built with ❤️ for developers</p>
280325
</div>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
'use client'
2+
3+
import { useRef } from 'react'
4+
import { useGSAP } from '@gsap/react'
5+
import { gsap } from 'gsap'
6+
import { ensureGsapRegistered } from '@/app/components/gsap/registry'
7+
import { cn } from '@/lib/utils'
8+
9+
interface AnimatedOrbitalLogoProps {
10+
className?: string
11+
size?: number
12+
pulseNodes?: boolean
13+
rotate?: boolean
14+
}
15+
16+
export function AnimatedOrbitalLogo({
17+
className,
18+
size = 48,
19+
pulseNodes = true,
20+
rotate = true,
21+
}: AnimatedOrbitalLogoProps) {
22+
const svgRef = useRef<SVGSVGElement>(null)
23+
24+
useGSAP(
25+
() => {
26+
ensureGsapRegistered()
27+
28+
if (!svgRef.current) {
29+
return
30+
}
31+
32+
const prefersReducedMotion = window.matchMedia(
33+
'(prefers-reduced-motion: reduce)'
34+
).matches
35+
36+
if (prefersReducedMotion) {
37+
gsap.set('[data-orbit-group]', { rotation: 0 })
38+
gsap.set('[data-node]', { scale: 1, opacity: 1 })
39+
gsap.set('[data-core]', { scale: 1, opacity: 1 })
40+
return
41+
}
42+
43+
gsap.fromTo(
44+
'[data-core]',
45+
{ scale: 0.85, autoAlpha: 0.75 },
46+
{
47+
scale: 1,
48+
autoAlpha: 1,
49+
duration: 0.6,
50+
ease: 'power2.out',
51+
}
52+
)
53+
54+
if (rotate) {
55+
gsap.to('[data-orbit-a]', {
56+
rotation: 360,
57+
transformOrigin: '50% 50%',
58+
duration: 10,
59+
ease: 'none',
60+
repeat: -1,
61+
})
62+
63+
gsap.to('[data-orbit-b]', {
64+
rotation: -360,
65+
transformOrigin: '50% 50%',
66+
duration: 14,
67+
ease: 'none',
68+
repeat: -1,
69+
})
70+
}
71+
72+
if (pulseNodes) {
73+
gsap.to('[data-node]', {
74+
scale: 1.12,
75+
autoAlpha: 0.75,
76+
duration: 1.2,
77+
ease: 'sine.inOut',
78+
repeat: -1,
79+
yoyo: true,
80+
stagger: {
81+
each: 0.15,
82+
repeat: -1,
83+
yoyo: true,
84+
},
85+
})
86+
}
87+
},
88+
{ scope: svgRef }
89+
)
90+
91+
return (
92+
<svg
93+
ref={svgRef}
94+
className={cn('text-primary', className)}
95+
width={size}
96+
height={size}
97+
viewBox="0 0 100 100"
98+
fill="none"
99+
role="img"
100+
aria-label="AgentStack animated orbital logo"
101+
>
102+
<g data-orbit-group data-orbit-a>
103+
<circle
104+
cx="50"
105+
cy="50"
106+
r="34"
107+
stroke="currentColor"
108+
strokeOpacity="0.28"
109+
strokeWidth="2"
110+
/>
111+
<circle data-node cx="84" cy="50" r="4" fill="currentColor" />
112+
<circle
113+
data-node
114+
cx="50"
115+
cy="16"
116+
r="3"
117+
fill="currentColor"
118+
fillOpacity="0.9"
119+
/>
120+
</g>
121+
122+
<g data-orbit-group data-orbit-b>
123+
<ellipse
124+
cx="50"
125+
cy="50"
126+
rx="25"
127+
ry="15"
128+
stroke="currentColor"
129+
strokeOpacity="0.2"
130+
strokeWidth="2"
131+
transform="rotate(35 50 50)"
132+
/>
133+
<circle
134+
data-node
135+
cx="72"
136+
cy="35"
137+
r="3"
138+
fill="currentColor"
139+
fillOpacity="0.85"
140+
/>
141+
</g>
142+
143+
<circle
144+
data-core
145+
cx="50"
146+
cy="50"
147+
r="12"
148+
fill="currentColor"
149+
fillOpacity="0.18"
150+
/>
151+
<circle cx="50" cy="50" r="7" fill="currentColor" />
152+
</svg>
153+
)
154+
}

app/components/gsap/registry.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useGSAP } from '@gsap/react'
2+
import { gsap } from 'gsap'
3+
import { ScrollTrigger } from 'gsap/ScrollTrigger'
4+
5+
let hasRegisteredGsap = false
6+
7+
export function ensureGsapRegistered() {
8+
if (hasRegisteredGsap || typeof window === 'undefined') {
9+
return
10+
}
11+
12+
gsap.registerPlugin(useGSAP, ScrollTrigger)
13+
hasRegisteredGsap = true
14+
}

0 commit comments

Comments
 (0)