feat(design): enhance UI with animations, gradients, and hover effects
- Add AnimatedBackground component with canvas-based floating orbs - Add reusable Button component with shimmer effects and variants - Update globals.css with animation keyframes and utility classes - Enhance Header with glass effect and animated nav underlines - Enhance Footer with gradient overlays and hover effects - Update all pages with animated gradient backgrounds - Add hover effects on cards, icons, and buttons - Implement gradient text effects throughout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
9eebabdc7c
commit
238b1c9a87
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface Orb {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
speedX: number;
|
||||
speedY: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
interface AnimatedBackgroundProps {
|
||||
variant?: "dark" | "light" | "red";
|
||||
orbCount?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function AnimatedBackground({
|
||||
variant = "dark",
|
||||
orbCount = 5,
|
||||
className = "",
|
||||
}: AnimatedBackgroundProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const orbsRef = useRef<Orb[]>([]);
|
||||
const animationRef = useRef<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const parent = canvas.parentElement;
|
||||
if (parent) {
|
||||
canvas.width = parent.offsetWidth;
|
||||
canvas.height = parent.offsetHeight;
|
||||
}
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
||||
// Initialize orbs
|
||||
orbsRef.current = Array.from({ length: orbCount }, () => ({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
size: Math.random() * 200 + 100,
|
||||
speedX: (Math.random() - 0.5) * 0.5,
|
||||
speedY: (Math.random() - 0.5) * 0.5,
|
||||
opacity: Math.random() * 0.3 + 0.1,
|
||||
}));
|
||||
|
||||
const getOrbColor = (opacity: number) => {
|
||||
switch (variant) {
|
||||
case "dark":
|
||||
return `rgba(220, 38, 38, ${opacity})`;
|
||||
case "light":
|
||||
return `rgba(220, 38, 38, ${opacity * 0.5})`;
|
||||
case "red":
|
||||
return `rgba(255, 255, 255, ${opacity * 0.3})`;
|
||||
default:
|
||||
return `rgba(220, 38, 38, ${opacity})`;
|
||||
}
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
orbsRef.current.forEach((orb) => {
|
||||
// Update position
|
||||
orb.x += orb.speedX;
|
||||
orb.y += orb.speedY;
|
||||
|
||||
// Bounce off edges
|
||||
if (orb.x < -orb.size) orb.x = canvas.width + orb.size;
|
||||
if (orb.x > canvas.width + orb.size) orb.x = -orb.size;
|
||||
if (orb.y < -orb.size) orb.y = canvas.height + orb.size;
|
||||
if (orb.y > canvas.height + orb.size) orb.y = -orb.size;
|
||||
|
||||
// Draw orb with gradient
|
||||
const gradient = ctx.createRadialGradient(
|
||||
orb.x,
|
||||
orb.y,
|
||||
0,
|
||||
orb.x,
|
||||
orb.y,
|
||||
orb.size
|
||||
);
|
||||
gradient.addColorStop(0, getOrbColor(orb.opacity));
|
||||
gradient.addColorStop(1, getOrbColor(0));
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(orb.x, orb.y, orb.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeCanvas);
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [variant, orbCount]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`absolute inset-0 pointer-events-none ${className}`}
|
||||
style={{ filter: "blur(60px)" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import Link from "next/link";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ButtonProps {
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
children: ReactNode;
|
||||
variant?: "primary" | "secondary" | "ghost" | "outline";
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
external?: boolean;
|
||||
type?: "button" | "submit";
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
href,
|
||||
onClick,
|
||||
children,
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
className = "",
|
||||
external = false,
|
||||
type = "button",
|
||||
disabled = false,
|
||||
}: ButtonProps) {
|
||||
const baseStyles =
|
||||
"relative inline-flex items-center justify-center font-medium rounded-lg transition-all duration-300 overflow-hidden group";
|
||||
|
||||
const sizeStyles = {
|
||||
sm: "px-4 py-2 text-sm",
|
||||
md: "px-6 py-3 text-base",
|
||||
lg: "px-8 py-4 text-lg",
|
||||
};
|
||||
|
||||
const variantStyles = {
|
||||
primary:
|
||||
"bg-gradient-to-r from-red-600 to-red-500 text-white hover:from-red-700 hover:to-red-600 hover:shadow-lg hover:shadow-red-500/25 hover:-translate-y-0.5 active:translate-y-0",
|
||||
secondary:
|
||||
"bg-white text-zinc-900 border border-zinc-200 hover:border-zinc-300 hover:bg-zinc-50 hover:shadow-lg hover:-translate-y-0.5 active:translate-y-0",
|
||||
ghost:
|
||||
"text-red-600 hover:bg-red-50 hover:text-red-700",
|
||||
outline:
|
||||
"border-2 border-red-600 text-red-600 hover:bg-red-600 hover:text-white hover:shadow-lg hover:shadow-red-500/25 hover:-translate-y-0.5 active:translate-y-0",
|
||||
};
|
||||
|
||||
const disabledStyles = "opacity-50 cursor-not-allowed hover:transform-none hover:shadow-none";
|
||||
|
||||
const combinedClassName = `${baseStyles} ${sizeStyles[size]} ${variantStyles[variant]} ${disabled ? disabledStyles : ""} ${className}`;
|
||||
|
||||
// Shimmer effect overlay
|
||||
const shimmer = variant === "primary" && (
|
||||
<span className="absolute inset-0 -translate-x-full group-hover:translate-x-full transition-transform duration-700 bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||
);
|
||||
|
||||
if (href) {
|
||||
if (external) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={combinedClassName}
|
||||
>
|
||||
{shimmer}
|
||||
<span className="relative z-10 flex items-center gap-2">{children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link href={href} className={combinedClassName}>
|
||||
{shimmer}
|
||||
<span className="relative z-10 flex items-center gap-2">{children}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={combinedClassName}
|
||||
>
|
||||
{shimmer}
|
||||
<span className="relative z-10 flex items-center gap-2">{children}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
+35
-18
@@ -30,22 +30,32 @@ const navigation = {
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-zinc-900 text-white">
|
||||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8 lg:py-16">
|
||||
<footer className="relative bg-zinc-900 text-white overflow-hidden">
|
||||
{/* Animated gradient overlay */}
|
||||
<div className="absolute inset-0 opacity-30">
|
||||
<div className="absolute top-0 -left-1/4 w-1/2 h-1/2 bg-red-600/20 rounded-full blur-3xl animate-pulse-slow" />
|
||||
<div className="absolute bottom-0 -right-1/4 w-1/2 h-1/2 bg-red-600/20 rounded-full blur-3xl animate-pulse-slow animation-delay-200" />
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8 lg:py-16">
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
||||
{/* Brand & Description */}
|
||||
<div className="space-y-4">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 bg-red-600 rounded-lg flex items-center justify-center">
|
||||
<Link href="/" className="flex items-center gap-2 group w-fit">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-red-500 to-red-700 rounded-lg flex items-center justify-center shadow-lg shadow-red-500/20 group-hover:shadow-red-500/40 transition-all duration-300 group-hover:scale-105">
|
||||
<span className="text-white font-bold text-lg">AB</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-white">AB Group</span>
|
||||
<span className="text-xl font-bold text-white group-hover:text-red-400 transition-colors duration-300">
|
||||
AB Group
|
||||
</span>
|
||||
</Link>
|
||||
<p className="text-zinc-400 text-sm max-w-xs">
|
||||
Your trusted QuickBooks experts. Providing prompt, professional,
|
||||
and courteous service since 2000.
|
||||
</p>
|
||||
<p className="text-red-500 font-medium text-sm">Se Habla Espanol</p>
|
||||
<p className="text-transparent bg-gradient-to-r from-red-400 to-red-600 bg-clip-text font-semibold text-sm">
|
||||
Se Habla Espanol
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
@@ -58,9 +68,10 @@ export default function Footer() {
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
className="text-sm text-zinc-400 hover:text-white transition-colors duration-300 inline-block relative group"
|
||||
>
|
||||
{item.name}
|
||||
<span className="absolute bottom-0 left-0 w-0 h-px bg-gradient-to-r from-red-500 to-red-600 group-hover:w-full transition-all duration-300" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -73,24 +84,30 @@ export default function Footer() {
|
||||
Contact Us
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-3">
|
||||
<Phone className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<li className="flex items-start gap-3 group">
|
||||
<div className="w-8 h-8 bg-zinc-800 rounded-lg flex items-center justify-center group-hover:bg-red-600/20 transition-colors duration-300">
|
||||
<Phone className="h-4 w-4 text-red-500" />
|
||||
</div>
|
||||
<div className="text-sm text-zinc-400">
|
||||
<p>(866) 218-3322</p>
|
||||
<p>(305) 387-8582</p>
|
||||
<p className="hover:text-white transition-colors duration-300 cursor-pointer">(866) 218-3322</p>
|
||||
<p className="hover:text-white transition-colors duration-300 cursor-pointer">(305) 387-8582</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<Mail className="h-5 w-5 text-red-500 flex-shrink-0" />
|
||||
<li className="flex items-center gap-3 group">
|
||||
<div className="w-8 h-8 bg-zinc-800 rounded-lg flex items-center justify-center group-hover:bg-red-600/20 transition-colors duration-300">
|
||||
<Mail className="h-4 w-4 text-red-500" />
|
||||
</div>
|
||||
<a
|
||||
href="mailto:info@ab-groupllc.com"
|
||||
className="text-sm text-zinc-400 hover:text-white transition-colors"
|
||||
className="text-sm text-zinc-400 hover:text-white transition-colors duration-300"
|
||||
>
|
||||
info@ab-groupllc.com
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<MapPin className="h-5 w-5 text-red-500 flex-shrink-0" />
|
||||
<li className="flex items-center gap-3 group">
|
||||
<div className="w-8 h-8 bg-zinc-800 rounded-lg flex items-center justify-center group-hover:bg-red-600/20 transition-colors duration-300">
|
||||
<MapPin className="h-4 w-4 text-red-500" />
|
||||
</div>
|
||||
<span className="text-sm text-zinc-400">
|
||||
Florida, United States
|
||||
</span>
|
||||
@@ -98,14 +115,14 @@ export default function Footer() {
|
||||
</ul>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="flex gap-4 mt-6">
|
||||
<div className="flex gap-3 mt-6">
|
||||
{navigation.social.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-zinc-400 hover:text-white transition-colors"
|
||||
className="w-10 h-10 bg-zinc-800 rounded-lg flex items-center justify-center text-zinc-400 hover:bg-gradient-to-br hover:from-red-500 hover:to-red-700 hover:text-white transition-all duration-300 hover:scale-110 hover:shadow-lg hover:shadow-red-500/20"
|
||||
>
|
||||
<span className="sr-only">{item.name}</span>
|
||||
<item.icon className="h-5 w-5" />
|
||||
|
||||
+44
-33
@@ -3,6 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import Button from "./Button";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Home", href: "/" },
|
||||
@@ -16,80 +17,90 @@ export default function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white border-b border-zinc-100">
|
||||
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-lg border-b border-zinc-100/50">
|
||||
<nav className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 bg-red-600 rounded-lg flex items-center justify-center">
|
||||
<Link href="/" className="flex items-center gap-2 group">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-red-500 to-red-700 rounded-lg flex items-center justify-center shadow-lg shadow-red-500/20 group-hover:shadow-red-500/40 transition-all duration-300 group-hover:scale-105">
|
||||
<span className="text-white font-bold text-lg">AB</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-zinc-900 hidden sm:block">
|
||||
<span className="text-xl font-bold text-zinc-900 hidden sm:block group-hover:text-red-600 transition-colors duration-300">
|
||||
AB Group
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex md:items-center md:gap-8">
|
||||
<div className="hidden md:flex md:items-center md:gap-1">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="text-sm font-medium text-zinc-600 hover:text-zinc-900 transition-colors"
|
||||
className="relative px-4 py-2 text-sm font-medium text-zinc-600 hover:text-red-600 transition-colors duration-300 group"
|
||||
>
|
||||
{item.name}
|
||||
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-0 h-0.5 bg-gradient-to-r from-red-500 to-red-600 group-hover:w-3/4 transition-all duration-300 rounded-full" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<div className="hidden md:block">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center justify-center px-5 py-2.5 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 transition-colors"
|
||||
>
|
||||
<Button href="/contact" size="sm">
|
||||
Get a Quote
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
type="button"
|
||||
className="md:hidden p-2 text-zinc-600 hover:text-zinc-900"
|
||||
className="md:hidden p-2 text-zinc-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all duration-300"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
{mobileMenuOpen ? (
|
||||
<X className="h-6 w-6" />
|
||||
) : (
|
||||
<Menu className="h-6 w-6" />
|
||||
)}
|
||||
<div className="relative w-6 h-6">
|
||||
<Menu
|
||||
className={`absolute inset-0 h-6 w-6 transition-all duration-300 ${
|
||||
mobileMenuOpen ? "opacity-0 rotate-90" : "opacity-100 rotate-0"
|
||||
}`}
|
||||
/>
|
||||
<X
|
||||
className={`absolute inset-0 h-6 w-6 transition-all duration-300 ${
|
||||
mobileMenuOpen ? "opacity-100 rotate-0" : "opacity-0 -rotate-90"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t border-zinc-100 bg-white">
|
||||
<div className="px-4 py-4 space-y-3">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="block text-base font-medium text-zinc-600 hover:text-zinc-900 transition-colors"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
<div
|
||||
className={`md:hidden overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
mobileMenuOpen ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div className="px-4 py-4 space-y-1 bg-white border-t border-zinc-100">
|
||||
{navigation.map((item, index) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="block px-4 py-3 text-base font-medium text-zinc-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all duration-300"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
href="/contact"
|
||||
className="block w-full text-center px-5 py-2.5 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 transition-colors mt-4"
|
||||
className="w-full"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Get a Quote
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user