Added home page.

This commit is contained in:
Andrés Mora 2025-12-19 04:52:48 -05:00
parent 9af245eadf
commit 8b9721bc5f
11 changed files with 2226 additions and 67 deletions

View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(yarn add:*)",
"Bash(yarn build)"
]
}
}

View File

@ -0,0 +1,116 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
import { ReactNode } from "react";
interface AnimatedButtonProps {
href?: string;
onClick?: () => void;
children: ReactNode;
variant?: "primary" | "secondary" | "accent" | "outline";
size?: "sm" | "md" | "lg";
className?: string;
external?: boolean;
icon?: ReactNode;
iconPosition?: "left" | "right";
}
export default function AnimatedButton({
href,
onClick,
children,
variant = "primary",
size = "md",
className = "",
external = false,
icon,
iconPosition = "right",
}: AnimatedButtonProps) {
const baseStyles =
"relative inline-flex items-center justify-center gap-2 font-medium rounded-full overflow-hidden transition-all duration-300 cursor-pointer";
const variants = {
primary:
"bg-[hsl(0,100%,40%)] text-white hover:bg-[hsl(0,100%,35%)] shadow-lg hover:shadow-xl hover:shadow-[hsl(0,100%,40%)]/25",
secondary:
"bg-neutral-900 text-white hover:bg-neutral-800 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-100",
accent:
"bg-gradient-to-r from-emerald-600 to-emerald-500 text-white hover:from-emerald-700 hover:to-emerald-600 shadow-lg hover:shadow-xl hover:shadow-emerald-500/25",
outline:
"bg-transparent border-2 border-neutral-200 text-neutral-900 hover:border-[hsl(0,100%,40%)] hover:text-[hsl(0,100%,40%)] dark:border-neutral-700 dark:text-white dark:hover:border-[hsl(0,100%,50%)] dark:hover:text-[hsl(0,100%,50%)]",
};
const sizes = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
const buttonContent = (
<>
{icon && iconPosition === "left" && (
<motion.span
className="inline-flex"
whileHover={{ x: -2 }}
transition={{ duration: 0.2 }}
>
{icon}
</motion.span>
)}
<span>{children}</span>
{icon && iconPosition === "right" && (
<motion.span
className="inline-flex"
whileHover={{ x: 2 }}
transition={{ duration: 0.2 }}
>
{icon}
</motion.span>
)}
</>
);
const motionProps = {
whileHover: { scale: 1.02, y: -2 },
whileTap: { scale: 0.98 },
transition: { type: "spring" as const, stiffness: 400, damping: 17 },
};
if (href) {
if (external) {
return (
<motion.a
href={href}
target="_blank"
rel="noopener noreferrer"
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
{...motionProps}
>
{buttonContent}
</motion.a>
);
}
return (
<Link href={href} passHref legacyBehavior>
<motion.a
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
{...motionProps}
>
{buttonContent}
</motion.a>
</Link>
);
}
return (
<motion.button
onClick={onClick}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
{...motionProps}
>
{buttonContent}
</motion.button>
);
}

112
app/components/FadeIn.tsx Normal file
View File

@ -0,0 +1,112 @@
"use client";
import { motion } from "framer-motion";
import { ReactNode } from "react";
interface FadeInProps {
children: ReactNode;
delay?: number;
duration?: number;
direction?: "up" | "down" | "left" | "right" | "none";
distance?: number;
className?: string;
once?: boolean;
}
export default function FadeIn({
children,
delay = 0,
duration = 0.6,
direction = "up",
distance = 30,
className = "",
once = true,
}: FadeInProps) {
const directions = {
up: { y: distance, x: 0 },
down: { y: -distance, x: 0 },
left: { x: distance, y: 0 },
right: { x: -distance, y: 0 },
none: { x: 0, y: 0 },
};
return (
<motion.div
initial={{
opacity: 0,
...directions[direction],
}}
whileInView={{
opacity: 1,
x: 0,
y: 0,
}}
viewport={{ once, margin: "-50px" }}
transition={{
duration,
delay,
ease: [0.16, 1, 0.3, 1],
}}
className={className}
>
{children}
</motion.div>
);
}
interface StaggerContainerProps {
children: ReactNode;
className?: string;
staggerDelay?: number;
}
export function StaggerContainer({
children,
className = "",
staggerDelay = 0.1,
}: StaggerContainerProps) {
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
variants={{
hidden: {},
visible: {
transition: {
staggerChildren: staggerDelay,
},
},
}}
className={className}
>
{children}
</motion.div>
);
}
interface StaggerItemProps {
children: ReactNode;
className?: string;
}
export function StaggerItem({ children, className = "" }: StaggerItemProps) {
return (
<motion.div
variants={{
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.16, 1, 0.3, 1],
},
},
}}
className={className}
>
{children}
</motion.div>
);
}

231
app/components/Footer.tsx Normal file
View File

@ -0,0 +1,231 @@
"use client";
import { motion } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import { MapPinIcon, PhoneIcon, MailIcon } from "./Icons";
const footerLinks = {
company: [
{ href: "/what-we-do", label: "What We Do" },
{ href: "/culture", label: "Our Culture" },
{ href: "/contact", label: "Contact Us" },
],
products: [
{ href: "/products", label: "All Products" },
{ href: "/sustainability", label: "Sustainability" },
{ href: "/state-ordinances", label: "State Ordinances" },
],
categories: [
{ href: "/products#packaging", label: "Food Packaging" },
{ href: "/products#green", label: "Green Products" },
{ href: "/products#janitorial", label: "Janitorial" },
],
};
const territories = [
"Colorado",
"Washington",
"Oregon",
"Idaho",
"Utah",
"Wyoming",
"Montana",
"New Mexico",
"Nebraska",
"Arizona",
];
export default function Footer() {
return (
<footer className="relative bg-neutral-950 text-white overflow-hidden">
{/* Decorative gradient */}
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-[hsl(0,100%,40%)] to-transparent" />
{/* Background decoration */}
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-[hsl(0,100%,40%)] rounded-full filter blur-[200px] opacity-5 -translate-y-1/2 translate-x-1/2" />
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Main Footer Content */}
<div className="py-16 lg:py-20">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 lg:gap-8">
{/* Brand Column */}
<div className="lg:col-span-2">
<Link href="/" className="inline-block">
<motion.div
whileHover={{ scale: 1.05 }}
className="flex items-center gap-3"
>
<Image
src="/assets/brand-logo.png"
alt="TCM Sales & Marketing"
width={50}
height={50}
className="w-12 h-12 object-contain brightness-0 invert"
/>
<div>
<span className="font-bold text-xl">TCM</span>
<span className="text-[hsl(0,100%,50%)] font-bold text-xl">
{" "}
Sales & Marketing
</span>
</div>
</motion.div>
</Link>
<p className="mt-6 text-neutral-400 max-w-md leading-relaxed">
Industry trusted since 2005. Your partner for foodservice and
janitorial solutions across the Rocky Mountain and Northwest
regions.
</p>
{/* Contact Info */}
<div className="mt-8 space-y-4">
<motion.a
href="tel:303-371-2810"
whileHover={{ x: 5 }}
className="flex items-center gap-3 text-neutral-400 hover:text-white transition-colors group"
>
<span className="p-2 rounded-lg bg-white/5 group-hover:bg-[hsl(0,100%,40%)] transition-colors">
<PhoneIcon size={18} />
</span>
<span>303-371-2810</span>
</motion.a>
<motion.a
href="mailto:info@tcm-sales.com"
whileHover={{ x: 5 }}
className="flex items-center gap-3 text-neutral-400 hover:text-white transition-colors group"
>
<span className="p-2 rounded-lg bg-white/5 group-hover:bg-[hsl(0,100%,40%)] transition-colors">
<MailIcon size={18} />
</span>
<span>info@tcm-sales.com</span>
</motion.a>
<motion.div
whileHover={{ x: 5 }}
className="flex items-start gap-3 text-neutral-400 group"
>
<span className="p-2 rounded-lg bg-white/5">
<MapPinIcon size={18} />
</span>
<span className="leading-relaxed">
12424 East Weaver Place
<br />
Centennial, CO 80111
</span>
</motion.div>
</div>
</div>
{/* Links Columns */}
<div>
<h4 className="font-semibold text-white mb-4">Company</h4>
<ul className="space-y-3">
{footerLinks.company.map((link) => (
<li key={link.href}>
<Link href={link.href}>
<motion.span
whileHover={{ x: 5, color: "#ff4d4d" }}
className="text-neutral-400 hover:text-white transition-colors cursor-pointer inline-block"
>
{link.label}
</motion.span>
</Link>
</li>
))}
</ul>
</div>
<div>
<h4 className="font-semibold text-white mb-4">Products</h4>
<ul className="space-y-3">
{footerLinks.products.map((link) => (
<li key={link.href}>
<Link href={link.href}>
<motion.span
whileHover={{ x: 5, color: "#ff4d4d" }}
className="text-neutral-400 hover:text-white transition-colors cursor-pointer inline-block"
>
{link.label}
</motion.span>
</Link>
</li>
))}
</ul>
</div>
<div>
<h4 className="font-semibold text-white mb-4">Categories</h4>
<ul className="space-y-3">
{footerLinks.categories.map((link) => (
<li key={link.href}>
<Link href={link.href}>
<motion.span
whileHover={{ x: 5, color: "#ff4d4d" }}
className="text-neutral-400 hover:text-white transition-colors cursor-pointer inline-block"
>
{link.label}
</motion.span>
</Link>
</li>
))}
</ul>
</div>
</div>
{/* Territory Tags */}
<div className="mt-12 pt-8 border-t border-white/10">
<p className="text-sm text-neutral-500 mb-4">
Proudly serving the Rocky Mountain & Northwest regions:
</p>
<div className="flex flex-wrap gap-2">
{territories.map((territory, index) => (
<motion.span
key={territory}
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ delay: index * 0.05 }}
whileHover={{
scale: 1.05,
backgroundColor: "hsl(0, 100%, 40%)",
}}
className="px-3 py-1 text-xs font-medium text-neutral-400 bg-white/5 rounded-full cursor-default transition-colors hover:text-white"
>
{territory}
</motion.span>
))}
</div>
</div>
</div>
{/* Bottom Bar */}
<div className="py-6 border-t border-white/10 flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-sm text-neutral-500">
&copy; {new Date().getFullYear()} TCM Sales & Marketing. All rights
reserved.
</p>
<div className="flex items-center gap-6">
<Link href="/privacy">
<motion.span
whileHover={{ color: "#fff" }}
className="text-sm text-neutral-500 cursor-pointer"
>
Privacy Policy
</motion.span>
</Link>
<Link href="/terms">
<motion.span
whileHover={{ color: "#fff" }}
className="text-sm text-neutral-500 cursor-pointer"
>
Terms of Service
</motion.span>
</Link>
</div>
</div>
</div>
</footer>
);
}

310
app/components/Icons.tsx Normal file
View File

@ -0,0 +1,310 @@
interface IconProps {
className?: string;
size?: number;
}
export function ArrowRightIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
);
}
export function CheckIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M20 6L9 17l-5-5" />
</svg>
);
}
export function MapPinIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
);
}
export function PhoneIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z" />
</svg>
);
}
export function MailIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
);
}
export function LeafIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M11 20A7 7 0 019.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z" />
<path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12" />
</svg>
);
}
export function PackageIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M16.5 9.4l-9-5.19M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" />
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" y1="22.08" x2="12" y2="12" />
</svg>
);
}
export function UsersIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
</svg>
);
}
export function TruckIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect x="1" y="3" width="15" height="13" />
<polygon points="16 8 20 8 23 11 23 16 16 16 16 8" />
<circle cx="5.5" cy="18.5" r="2.5" />
<circle cx="18.5" cy="18.5" r="2.5" />
</svg>
);
}
export function TargetIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>
);
}
export function HeartIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" />
</svg>
);
}
export function StarIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
);
}
export function BuildingIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect x="4" y="2" width="16" height="20" rx="2" ry="2" />
<path d="M9 22v-4h6v4M8 6h.01M16 6h.01M12 6h.01M12 10h.01M12 14h.01M16 10h.01M16 14h.01M8 10h.01M8 14h.01" />
</svg>
);
}
export function SparklesIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M12 3l1.912 5.813a2 2 0 001.275 1.275L21 12l-5.813 1.912a2 2 0 00-1.275 1.275L12 21l-1.912-5.813a2 2 0 00-1.275-1.275L3 12l5.813-1.912a2 2 0 001.275-1.275L12 3z" />
</svg>
);
}
export function ChevronDownIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<polyline points="6 9 12 15 18 9" />
</svg>
);
}
export function RecycleIcon({ className = "", size = 20 }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M7 19H4.815a1.83 1.83 0 01-1.57-.881 1.785 1.785 0 01-.004-1.784L7.196 9.5" />
<path d="M11 19h8.203a1.83 1.83 0 001.556-.89 1.784 1.784 0 00-.001-1.78l-1.758-3.03" />
<path d="M14 16l3 3-3 3" />
<path d="M8.293 13.596L4.5 9.5l4.5-4" />
<path d="M9.344 5.811l1.093-1.892A1.83 1.83 0 0112 3c.617 0 1.193.31 1.544.837l4.26 7.357" />
<path d="M13.378 9.633L11.5 5.5 16 2" />
</svg>
);
}

View File

@ -0,0 +1,200 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import { useState, useEffect } from "react";
import AnimatedButton from "./AnimatedButton";
const navLinks = [
{ href: "/", label: "Home" },
{ href: "/what-we-do", label: "What We Do" },
{ href: "/products", label: "Products" },
{ href: "/sustainability", label: "Sustainability" },
{ href: "/culture", label: "Our Culture" },
];
export default function Navigation() {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<>
<motion.header
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
isScrolled
? "bg-white/90 dark:bg-neutral-950/90 backdrop-blur-xl shadow-lg shadow-black/5"
: "bg-transparent"
}`}
>
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-20">
{/* Logo */}
<Link href="/" className="relative z-10">
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="flex items-center gap-3"
>
<Image
src="/assets/brand-logo.png"
alt="TCM Sales & Marketing"
width={50}
height={50}
className="w-12 h-12 object-contain"
/>
<div className="hidden sm:block">
<span className="font-bold text-xl text-neutral-900 dark:text-white">
TCM
</span>
<span className="text-[hsl(0,100%,40%)] font-bold text-xl">
{" "}
Sales
</span>
</div>
</motion.div>
</Link>
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center gap-1">
{navLinks.map((link) => (
<Link key={link.href} href={link.href}>
<motion.span
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300 font-medium rounded-full transition-colors hover:text-neutral-900 dark:hover:text-white cursor-pointer"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{link.label}
<motion.span
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1 h-1 bg-[hsl(0,100%,40%)] rounded-full opacity-0"
whileHover={{ opacity: 1, scale: 1.5 }}
/>
</motion.span>
</Link>
))}
</div>
{/* CTA Button */}
<div className="hidden lg:block">
<AnimatedButton href="/contact" size="sm">
Contact Us
</AnimatedButton>
</div>
{/* Mobile Menu Button */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="lg:hidden relative z-10 p-2 text-neutral-900 dark:text-white"
>
<div className="w-6 h-5 flex flex-col justify-between">
<motion.span
animate={{
rotate: isMobileMenuOpen ? 45 : 0,
y: isMobileMenuOpen ? 8 : 0,
}}
className="w-full h-0.5 bg-current origin-left transition-colors"
/>
<motion.span
animate={{
opacity: isMobileMenuOpen ? 0 : 1,
x: isMobileMenuOpen ? -20 : 0,
}}
className="w-full h-0.5 bg-current transition-colors"
/>
<motion.span
animate={{
rotate: isMobileMenuOpen ? -45 : 0,
y: isMobileMenuOpen ? -8 : 0,
}}
className="w-full h-0.5 bg-current origin-left transition-colors"
/>
</div>
</motion.button>
</div>
</nav>
</motion.header>
{/* Mobile Menu */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="fixed inset-0 z-40 lg:hidden"
>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={() => setIsMobileMenuOpen(false)}
/>
{/* Menu Panel */}
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 25, stiffness: 200 }}
className="absolute right-0 top-0 bottom-0 w-full max-w-sm bg-white dark:bg-neutral-950 shadow-2xl"
>
<div className="flex flex-col h-full pt-24 pb-8 px-6">
<nav className="flex-1 space-y-2">
{navLinks.map((link, index) => (
<motion.div
key={link.href}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
>
<Link
href={link.href}
onClick={() => setIsMobileMenuOpen(false)}
className="block py-4 text-2xl font-medium text-neutral-900 dark:text-white hover:text-[hsl(0,100%,40%)] transition-colors border-b border-neutral-100 dark:border-neutral-800"
>
{link.label}
</Link>
</motion.div>
))}
</nav>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<AnimatedButton
href="/contact"
size="lg"
className="w-full justify-center"
onClick={() => setIsMobileMenuOpen(false)}
>
Contact Us
</AnimatedButton>
</motion.div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
}

View File

@ -1,8 +1,75 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
/* Brand colors - TCM Red (hsl(0, 100%, 40%) as base) */
--brand-50: hsl(0, 100%, 97%);
--brand-100: hsl(0, 100%, 94%);
--brand-200: hsl(0, 100%, 87%);
--brand-300: hsl(0, 100%, 75%);
--brand-400: hsl(0, 100%, 60%);
--brand-500: hsl(0, 100%, 50%);
--brand-600: hsl(0, 100%, 40%);
--brand-700: hsl(0, 100%, 33%);
--brand-800: hsl(0, 100%, 27%);
--brand-900: hsl(0, 100%, 22%);
--brand-950: hsl(0, 100%, 12%);
/* Accent colors - Vibrant emerald for sustainability */
--accent-50: #ecfdf5;
--accent-100: #d1fae5;
--accent-200: #a7f3d0;
--accent-300: #6ee7b7;
--accent-400: #34d399;
--accent-500: #10b981;
--accent-600: #059669;
--accent-700: #047857;
--accent-800: #065f46;
--accent-900: #064e3b;
/* Neutral colors */
--neutral-50: #fafafa;
--neutral-100: #f5f5f5;
--neutral-200: #e5e5e5;
--neutral-300: #d4d4d4;
--neutral-400: #a3a3a3;
--neutral-500: #737373;
--neutral-600: #525252;
--neutral-700: #404040;
--neutral-800: #262626;
--neutral-900: #171717;
--neutral-950: #0a0a0a;
/* Semantic colors */
--background: #ffffff; --background: #ffffff;
--foreground: #171717; --foreground: #171717;
--muted: #f5f5f5;
--muted-foreground: #737373;
--border: #e5e5e5;
--ring: hsl(0, 100%, 40%);
/* Gradients */
--gradient-brand: linear-gradient(135deg, hsl(0, 100%, 33%) 0%, hsl(0, 100%, 40%) 50%, hsl(0, 100%, 50%) 100%);
--gradient-brand-reverse: linear-gradient(135deg, hsl(0, 100%, 50%) 0%, hsl(0, 100%, 40%) 100%);
--gradient-accent: linear-gradient(135deg, #047857 0%, #10b981 50%, #34d399 100%);
--gradient-dark: linear-gradient(135deg, #0a0a0a 0%, #262626 100%);
--gradient-hero: linear-gradient(180deg, hsl(0, 50%, 98%) 0%, #ffffff 50%, hsl(0, 20%, 97%) 100%);
--gradient-warm: linear-gradient(135deg, hsl(0, 100%, 40%) 0%, hsl(20, 100%, 50%) 100%);
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--shadow-glow: 0 0 40px -10px hsl(0, 100%, 40%);
--shadow-glow-accent: 0 0 40px -10px rgb(16 185 129 / 0.5);
--shadow-brand: 0 4px 14px 0 hsl(0 100% 40% / 0.35);
/* Animation timing */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
--ease-in-out-circ: cubic-bezier(0.85, 0, 0.15, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
} }
@theme inline { @theme inline {
@ -15,12 +82,373 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background: #0a0a0a; --background: #0a0a0a;
--foreground: #ededed; --foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a3a3a3;
--border: #404040;
} }
} }
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: var(--font-sans), system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* Selection styling */
::selection {
background-color: hsl(0, 100%, 40%);
color: white;
}
/* Focus styling */
:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--muted);
}
::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, hsl(0, 100%, 50%) 0%, hsl(0, 100%, 40%) 100%);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, hsl(0, 100%, 55%) 0%, hsl(0, 100%, 45%) 100%);
}
/* Utility classes for animations */
.animate-fade-in {
animation: fadeIn 0.6s var(--ease-out-expo) forwards;
}
.animate-slide-up {
animation: slideUp 0.6s var(--ease-out-expo) forwards;
}
.animate-scale-in {
animation: scaleIn 0.5s var(--ease-out-expo) forwards;
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-pulse-glow {
animation: pulseGlow 2s ease-in-out infinite;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
@keyframes pulseGlow {
0%, 100% {
box-shadow: 0 0 20px hsl(0 100% 40% / 0.3);
}
50% {
box-shadow: 0 0 40px hsl(0 100% 40% / 0.6);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Gradient text utility */
.gradient-text {
background: var(--gradient-brand);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.gradient-text-accent {
background: var(--gradient-accent);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Glass morphism effect */
.glass {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
@media (prefers-color-scheme: dark) {
.glass {
background: rgba(10, 10, 10, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
/* Noise texture overlay */
.noise-overlay::before {
content: "";
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
opacity: 0.02;
pointer-events: none;
z-index: 1;
}
/* Animated underline for links */
.animated-underline {
position: relative;
display: inline-block;
}
.animated-underline::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--gradient-brand);
transition: width 0.3s var(--ease-out-expo);
}
.animated-underline:hover::after {
width: 100%;
}
/* Shimmer effect for loading states */
.shimmer {
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 50%,
transparent 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
/* Card hover effect */
.card-hover {
transition: all 0.4s var(--ease-out-expo);
}
.card-hover:hover {
transform: translateY(-8px);
box-shadow: var(--shadow-2xl);
}
/* Interactive card with border glow */
.card-glow {
position: relative;
transition: all 0.4s var(--ease-out-expo);
}
.card-glow::before {
content: "";
position: absolute;
inset: -2px;
background: var(--gradient-brand);
border-radius: inherit;
opacity: 0;
z-index: -1;
transition: opacity 0.4s var(--ease-out-expo);
}
.card-glow:hover::before {
opacity: 1;
}
.card-glow:hover {
transform: translateY(-4px);
}
/* Magnetic button effect helper */
.magnetic {
transition: transform 0.3s var(--ease-out-expo);
}
/* Icon hover rotation */
.icon-spin {
transition: transform 0.5s var(--ease-spring);
}
.icon-spin:hover {
transform: rotate(360deg);
}
/* Section padding utility */
.section-padding {
padding: 5rem 1.5rem;
}
@media (min-width: 768px) {
.section-padding {
padding: 7rem 2rem;
}
}
@media (min-width: 1024px) {
.section-padding {
padding: 8rem 4rem;
}
}
/* Container max width */
.container-max {
max-width: 1400px;
margin: 0 auto;
}
/* Decorative blob shapes */
.blob {
position: absolute;
border-radius: 50%;
filter: blur(60px);
opacity: 0.4;
pointer-events: none;
}
.blob-brand {
background: hsl(0, 100%, 70%);
}
.blob-accent {
background: hsl(150, 80%, 60%);
}
/* Line decoration */
.line-decoration {
width: 60px;
height: 4px;
background: var(--gradient-brand);
border-radius: 2px;
}
/* Stagger animation helper */
.stagger > * {
opacity: 0;
animation: slideUp 0.6s var(--ease-out-expo) forwards;
}
.stagger > *:nth-child(1) { animation-delay: 0.1s; }
.stagger > *:nth-child(2) { animation-delay: 0.2s; }
.stagger > *:nth-child(3) { animation-delay: 0.3s; }
.stagger > *:nth-child(4) { animation-delay: 0.4s; }
.stagger > *:nth-child(5) { animation-delay: 0.5s; }
.stagger > *:nth-child(6) { animation-delay: 0.6s; }
/* Text shadow for headings */
.text-shadow {
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
/* Hover lift effect */
.hover-lift {
transition: transform 0.3s var(--ease-out-expo), box-shadow 0.3s var(--ease-out-expo);
}
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
/* Scale on hover */
.hover-scale {
transition: transform 0.3s var(--ease-spring);
}
.hover-scale:hover {
transform: scale(1.05);
}
/* Rotate on hover */
.hover-rotate {
transition: transform 0.4s var(--ease-spring);
}
.hover-rotate:hover {
transform: rotate(3deg);
}
/* Border gradient effect */
.border-gradient {
position: relative;
background: var(--background);
}
.border-gradient::before {
content: "";
position: absolute;
inset: 0;
padding: 2px;
background: var(--gradient-brand);
border-radius: inherit;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
} }

View File

@ -1,20 +1,38 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Inter, Outfit } from "next/font/google";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ const inter = Inter({
variable: "--font-geist-sans", variable: "--font-geist-sans",
subsets: ["latin"], subsets: ["latin"],
display: "swap",
}); });
const geistMono = Geist_Mono({ const outfit = Outfit({
variable: "--font-geist-mono", variable: "--font-outfit",
subsets: ["latin"], subsets: ["latin"],
display: "swap",
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "TCM Sales & Marketing | Foodservice & Janitorial Experts",
description: "Generated by create next app", description:
"With over 100 years of combined experience, TCM Sales & Marketing provides expert foodservice and janitorial solutions across the Rocky Mountain and Northwest regions.",
keywords: [
"foodservice",
"janitorial",
"sales",
"marketing",
"Rocky Mountain",
"food packaging",
"sustainability",
"green products",
],
openGraph: {
title: "TCM Sales & Marketing",
description: "Your Foodservice and Janitorial Sales Experts",
type: "website",
},
}; };
export default function RootLayout({ export default function RootLayout({
@ -23,10 +41,8 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en" className="scroll-smooth">
<body <body className={`${inter.variable} ${outfit.variable} antialiased`}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children} {children}
</body> </body>
</html> </html>

View File

@ -1,65 +1,781 @@
"use client";
import { motion, useScroll, useTransform } from "framer-motion";
import Image from "next/image"; import Image from "next/image";
import { useRef } from "react";
import Navigation from "./components/Navigation";
import Footer from "./components/Footer";
import AnimatedButton from "./components/AnimatedButton";
import FadeIn, { StaggerContainer, StaggerItem } from "./components/FadeIn";
import {
ArrowRightIcon,
PackageIcon,
LeafIcon,
UsersIcon,
TruckIcon,
TargetIcon,
HeartIcon,
SparklesIcon,
MapPinIcon,
CheckIcon,
RecycleIcon,
} from "./components/Icons";
const stats = [
{ value: "100+", label: "Years Combined Experience" },
{ value: "10", label: "States Covered" },
{ value: "2005", label: "Industry Trusted Since" },
{ value: "5", label: "Product Categories" },
];
const productCategories = [
{
icon: PackageIcon,
title: "Food Packaging",
description:
"Premium containers, cutlery, and packaging solutions for every foodservice need.",
color: "from-orange-500 to-red-500",
},
{
icon: LeafIcon,
title: "Green Products",
description:
"Sustainable, compostable, and eco-friendly alternatives for conscious businesses.",
color: "from-emerald-500 to-teal-500",
},
{
icon: SparklesIcon,
title: "Specialty Products",
description:
"Custom printed containers, wraps, and premium catering solutions.",
color: "from-purple-500 to-pink-500",
},
{
icon: TruckIcon,
title: "Janitorial",
description:
"Complete range of cleaning supplies and janitorial essentials.",
color: "from-blue-500 to-cyan-500",
},
];
const values = [
{
icon: HeartIcon,
title: "Family First",
description:
"Building character, strengthening individuals, and nurturing families.",
},
{
icon: UsersIcon,
title: "Integrity",
description: "Selflessness, servanthood, and confident humility in all we do.",
},
{
icon: TargetIcon,
title: "Purpose Driven",
description: "Professional, ethical representation with symbiotic growth.",
},
];
const territories = [
"Colorado",
"Washington",
"Oregon",
"Idaho",
"Utah",
"Wyoming",
"Montana",
"New Mexico",
"Nebraska",
"Arizona",
];
export default function Home() { export default function Home() {
const heroRef = useRef(null);
const { scrollYProgress } = useScroll({
target: heroRef,
offset: ["start start", "end start"],
});
const heroY = useTransform(scrollYProgress, [0, 1], ["0%", "30%"]);
const heroOpacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <main className="relative">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <Navigation />
<Image
className="dark:invert" {/* Hero Section */}
src="/next.svg" <section
alt="Next.js logo" ref={heroRef}
width={100} className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-b from-neutral-50 to-white dark:from-neutral-950 dark:to-neutral-900"
height={20} >
priority {/* Animated background elements */}
<div className="absolute inset-0 overflow-hidden">
<motion.div
className="absolute top-1/4 -left-32 w-96 h-96 bg-[hsl(0,100%,70%)] rounded-full filter blur-[120px] opacity-20"
animate={{
scale: [1, 1.2, 1],
x: [0, 30, 0],
}}
transition={{
duration: 8,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<motion.div
className="absolute bottom-1/4 -right-32 w-96 h-96 bg-[hsl(0,100%,50%)] rounded-full filter blur-[120px] opacity-15"
animate={{
scale: [1.2, 1, 1.2],
x: [0, -30, 0],
}}
transition={{
duration: 10,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<motion.div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-gradient-to-br from-[hsl(0,100%,40%)] to-[hsl(20,100%,50%)] rounded-full filter blur-[200px] opacity-5"
animate={{
rotate: [0, 360],
}}
transition={{
duration: 60,
repeat: Infinity,
ease: "linear",
}}
/>
</div>
{/* Grid pattern overlay */}
<div
className="absolute inset-0 opacity-[0.02]"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000000' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
}}
/> />
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> <motion.div
To get started, edit the page.tsx file. style={{ y: heroY, opacity: heroOpacity }}
</h1> className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center"
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> >
Looking for a starting point or more instructions? Head over to{" "} {/* Badge */}
<a <motion.div
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" initial={{ opacity: 0, y: 20 }}
className="font-medium text-zinc-950 dark:text-zinc-50" animate={{ opacity: 1, y: 0 }}
> transition={{ duration: 0.6, delay: 0.2 }}
Templates className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[hsl(0,100%,40%)]/10 border border-[hsl(0,100%,40%)]/20 mb-8"
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
> >
<Image <span className="w-2 h-2 rounded-full bg-[hsl(0,100%,40%)] animate-pulse" />
className="dark:invert" <span className="text-sm font-medium text-[hsl(0,100%,35%)] dark:text-[hsl(0,100%,60%)]">
src="/vercel.svg" Trusted Since 2005
alt="Vercel logomark" </span>
width={16} </motion.div>
height={16}
{/* Main heading */}
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-neutral-900 dark:text-white mb-6"
>
Your{" "}
<span className="relative inline-block">
<span className="relative z-10 bg-gradient-to-r from-[hsl(0,100%,35%)] via-[hsl(0,100%,45%)] to-[hsl(0,100%,55%)] bg-clip-text text-transparent">
Foodservice
</span>
<motion.span
className="absolute -bottom-2 left-0 right-0 h-3 bg-[hsl(0,100%,40%)]/20 -skew-x-3"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.6, delay: 0.8 }}
/>
</span>{" "}
&<br />
<span className="relative inline-block mt-2">
<span className="relative z-10 bg-gradient-to-r from-[hsl(0,100%,35%)] via-[hsl(0,100%,45%)] to-[hsl(0,100%,55%)] bg-clip-text text-transparent">
Janitorial
</span>
<motion.span
className="absolute -bottom-2 left-0 right-0 h-3 bg-[hsl(0,100%,40%)]/20 skew-x-3"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.6, delay: 1 }}
/>
</span>{" "}
Experts
</motion.h1>
{/* Subheading */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
className="max-w-2xl mx-auto text-lg sm:text-xl text-neutral-600 dark:text-neutral-400 mb-10 leading-relaxed"
>
With over 100 years of combined experience, we deliver exceptional
sales and marketing solutions across the Rocky Mountain and
Northwest regions.
</motion.p>
{/* CTA Buttons */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4"
>
<AnimatedButton
href="/contact"
size="lg"
icon={<ArrowRightIcon size={18} />}
>
Get in Touch
</AnimatedButton>
<AnimatedButton href="/what-we-do" variant="outline" size="lg">
Learn More
</AnimatedButton>
</motion.div>
{/* Scroll indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5 }}
className="absolute bottom-8 left-1/2 -translate-x-1/2"
>
<motion.div
animate={{ y: [0, 10, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}
className="w-6 h-10 rounded-full border-2 border-neutral-300 dark:border-neutral-700 flex items-start justify-center p-2"
>
<motion.div
animate={{ y: [0, 12, 0] }}
transition={{ duration: 1.5, repeat: Infinity }}
className="w-1.5 h-1.5 rounded-full bg-[hsl(0,100%,40%)]"
/>
</motion.div>
</motion.div>
</motion.div>
</section>
{/* Stats Section */}
<section className="relative py-20 bg-neutral-900 dark:bg-neutral-950 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-[hsl(0,100%,40%)]/10 via-transparent to-[hsl(0,100%,40%)]/10" />
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-4">
{stats.map((stat, index) => (
<FadeIn key={stat.label} delay={index * 0.1} className="text-center">
<motion.div
whileHover={{ scale: 1.05 }}
className="group cursor-default"
>
<motion.span
className="block text-4xl sm:text-5xl font-bold text-white mb-2 group-hover:text-[hsl(0,100%,60%)] transition-colors"
initial={{ opacity: 0, scale: 0.5 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{
type: "spring",
stiffness: 200,
damping: 15,
delay: index * 0.1,
}}
>
{stat.value}
</motion.span>
<span className="text-neutral-400 text-sm sm:text-base">
{stat.label}
</span>
</motion.div>
</FadeIn>
))}
</div>
</div>
</section>
{/* About Section */}
<section className="section-padding bg-white dark:bg-neutral-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12 lg:gap-20 items-center">
{/* Content */}
<div>
<FadeIn>
<span className="inline-block px-4 py-1.5 rounded-full bg-[hsl(0,100%,40%)]/10 text-[hsl(0,100%,40%)] text-sm font-medium mb-6">
About Us
</span>
</FadeIn>
<FadeIn delay={0.1}>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-neutral-900 dark:text-white mb-6 leading-tight">
Driving Growth Through{" "}
<span className="text-[hsl(0,100%,40%)]">
Strategic Partnership
</span>
</h2>
</FadeIn>
<FadeIn delay={0.2}>
<p className="text-lg text-neutral-600 dark:text-neutral-400 mb-6 leading-relaxed">
We believe in &ldquo;pull thru&rdquo; selling, using key operators to
direct distribution. Our integrated, end-user focused sales
approach ensures we exceed growth goals for our manufacturers.
</p>
</FadeIn>
<FadeIn delay={0.3}>
<p className="text-lg text-neutral-600 dark:text-neutral-400 mb-8 leading-relaxed">
We maintain continuity with our distributor&apos;s sales force,
providing them with the tools they need to successfully sell
our products day-to-day.
</p>
</FadeIn>
<FadeIn delay={0.4}>
<AnimatedButton
href="/what-we-do"
icon={<ArrowRightIcon size={18} />}
>
Discover Our Approach
</AnimatedButton>
</FadeIn>
</div>
{/* Image/Visual */}
<FadeIn direction="right" delay={0.2}>
<div className="relative">
<div className="relative aspect-square rounded-3xl overflow-hidden bg-gradient-to-br from-neutral-100 to-neutral-200 dark:from-neutral-800 dark:to-neutral-900">
{/* Decorative elements */}
<div className="absolute inset-0 flex items-center justify-center">
<motion.div
className="relative w-64 h-64"
animate={{ rotate: 360 }}
transition={{
duration: 60,
repeat: Infinity,
ease: "linear",
}}
>
{[...Array(8)].map((_, i) => (
<motion.div
key={i}
className="absolute w-4 h-4 rounded-full bg-[hsl(0,100%,40%)]"
style={{
top: "50%",
left: "50%",
transform: `rotate(${i * 45}deg) translateY(-120px)`,
}}
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
transition={{ delay: i * 0.1 }}
viewport={{ once: true }}
/>
))}
</motion.div>
</div>
<div className="absolute inset-0 flex items-center justify-center">
<motion.div
whileHover={{ scale: 1.05, rotate: 5 }}
className="bg-white dark:bg-neutral-800 p-8 rounded-2xl shadow-2xl"
>
<Image
src="/assets/brand-logo.png"
alt="TCM Sales"
width={150}
height={150}
className="w-32 h-32 object-contain"
/>
</motion.div>
</div>
</div>
{/* Floating badge */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
whileHover={{ scale: 1.05, y: -5 }}
className="absolute -bottom-6 -right-6 bg-[hsl(0,100%,40%)] text-white px-6 py-4 rounded-2xl shadow-xl"
>
<span className="block text-3xl font-bold">20+</span>
<span className="text-sm opacity-90">Years of Excellence</span>
</motion.div>
</div>
</FadeIn>
</div>
</div>
</section>
{/* Values Section */}
<section className="section-padding bg-neutral-50 dark:bg-neutral-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<FadeIn>
<span className="inline-block px-4 py-1.5 rounded-full bg-[hsl(0,100%,40%)]/10 text-[hsl(0,100%,40%)] text-sm font-medium mb-6">
Our Philosophy
</span>
</FadeIn>
<FadeIn delay={0.1}>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-neutral-900 dark:text-white mb-6">
Built on{" "}
<span className="text-[hsl(0,100%,40%)]">Strong Values</span>
</h2>
</FadeIn>
<FadeIn delay={0.2}>
<p className="max-w-2xl mx-auto text-lg text-neutral-600 dark:text-neutral-400">
Our mission is to glorify God by being faithful stewards,
serving our employees and families, and making a positive impact.
</p>
</FadeIn>
</div>
<StaggerContainer className="grid md:grid-cols-3 gap-8">
{values.map((value) => (
<StaggerItem key={value.title}>
<motion.div
whileHover={{ y: -10, scale: 1.02 }}
className="group relative bg-white dark:bg-neutral-900 p-8 rounded-3xl shadow-lg shadow-black/5 hover:shadow-xl transition-all duration-500 border border-neutral-100 dark:border-neutral-800"
>
<motion.div
whileHover={{ rotate: 360, scale: 1.1 }}
transition={{ duration: 0.5 }}
className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[hsl(0,100%,45%)] to-[hsl(0,100%,35%)] flex items-center justify-center text-white mb-6"
>
<value.icon size={24} />
</motion.div>
<h3 className="text-xl font-bold text-neutral-900 dark:text-white mb-3 group-hover:text-[hsl(0,100%,40%)] transition-colors">
{value.title}
</h3>
<p className="text-neutral-600 dark:text-neutral-400 leading-relaxed">
{value.description}
</p>
<motion.div
className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-[hsl(0,100%,45%)] to-[hsl(0,100%,35%)] rounded-b-3xl origin-left"
initial={{ scaleX: 0 }}
whileHover={{ scaleX: 1 }}
transition={{ duration: 0.3 }}
/>
</motion.div>
</StaggerItem>
))}
</StaggerContainer>
</div>
</section>
{/* Products Section */}
<section className="section-padding bg-white dark:bg-neutral-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between mb-16 gap-6">
<div>
<FadeIn>
<span className="inline-block px-4 py-1.5 rounded-full bg-[hsl(0,100%,40%)]/10 text-[hsl(0,100%,40%)] text-sm font-medium mb-6">
Our Products
</span>
</FadeIn>
<FadeIn delay={0.1}>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-neutral-900 dark:text-white">
Everything You{" "}
<span className="text-[hsl(0,100%,40%)]">Need</span>
</h2>
</FadeIn>
</div>
<FadeIn delay={0.2}>
<AnimatedButton
href="/products"
variant="outline"
icon={<ArrowRightIcon size={18} />}
>
View All Products
</AnimatedButton>
</FadeIn>
</div>
<StaggerContainer className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{productCategories.map((category) => (
<StaggerItem key={category.title}>
<motion.div
whileHover={{ y: -10 }}
className="group relative bg-neutral-50 dark:bg-neutral-800 p-6 rounded-3xl cursor-pointer overflow-hidden"
>
{/* Gradient overlay on hover */}
<motion.div
className={`absolute inset-0 bg-gradient-to-br ${category.color} opacity-0 group-hover:opacity-10 transition-opacity duration-500`}
/>
<motion.div
whileHover={{ scale: 1.1, rotate: 5 }}
className={`relative w-14 h-14 rounded-2xl bg-gradient-to-br ${category.color} flex items-center justify-center text-white mb-5`}
>
<category.icon size={24} />
</motion.div>
<h3 className="relative text-lg font-bold text-neutral-900 dark:text-white mb-2 group-hover:text-[hsl(0,100%,40%)] transition-colors">
{category.title}
</h3>
<p className="relative text-sm text-neutral-600 dark:text-neutral-400 leading-relaxed">
{category.description}
</p>
<motion.div
initial={{ x: -10, opacity: 0 }}
whileHover={{ x: 0, opacity: 1 }}
className="absolute bottom-6 right-6"
>
<ArrowRightIcon
size={20}
className="text-[hsl(0,100%,40%)]"
/>
</motion.div>
</motion.div>
</StaggerItem>
))}
</StaggerContainer>
</div>
</section>
{/* Territory Section */}
<section className="section-padding bg-neutral-900 dark:bg-neutral-950 text-white relative overflow-hidden">
{/* Background map pattern */}
<div className="absolute inset-0 opacity-5">
<svg className="w-full h-full" viewBox="0 0 800 600">
<path
d="M100 200 Q 200 100 300 200 T 500 200 T 700 200"
stroke="currentColor"
strokeWidth="2"
fill="none"
/> />
Deploy Now <circle cx="150" cy="180" r="8" fill="currentColor" />
</a> <circle cx="300" cy="200" r="8" fill="currentColor" />
<a <circle cx="500" cy="200" r="8" fill="currentColor" />
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" <circle cx="650" cy="220" r="8" fill="currentColor" />
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" </svg>
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div> </div>
</main>
</div> <div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<div>
<FadeIn>
<span className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-white/10 text-white/80 text-sm font-medium mb-6">
<MapPinIcon size={16} />
Our Coverage
</span>
</FadeIn>
<FadeIn delay={0.1}>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-6">
Serving the{" "}
<span className="text-[hsl(0,100%,55%)]">Rocky Mountain</span>{" "}
& <span className="text-[hsl(0,100%,55%)]">Northwest</span>{" "}
Regions
</h2>
</FadeIn>
<FadeIn delay={0.2}>
<p className="text-lg text-neutral-400 mb-8 leading-relaxed">
Our head office is in Denver, Colorado, but our dedicated team
works across ten states to provide exceptional service and
support.
</p>
</FadeIn>
{/* Territory grid */}
<FadeIn delay={0.3}>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{territories.map((territory, index) => (
<motion.div
key={territory}
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ delay: index * 0.05 }}
whileHover={{
scale: 1.05,
backgroundColor: "hsl(0, 100%, 40%)",
}}
className="flex items-center gap-2 px-4 py-3 bg-white/5 rounded-xl cursor-default transition-colors"
>
<CheckIcon size={16} className="text-[hsl(0,100%,55%)]" />
<span className="text-sm font-medium">{territory}</span>
</motion.div>
))}
</div>
</FadeIn>
<FadeIn delay={0.4}>
<div className="mt-8">
<AnimatedButton
href="/contact"
icon={<ArrowRightIcon size={18} />}
>
Contact Us
</AnimatedButton>
</div>
</FadeIn>
</div>
{/* Decorative Map Visual */}
<FadeIn direction="right" delay={0.2}>
<div className="relative aspect-square">
<motion.div
className="absolute inset-0 rounded-3xl bg-gradient-to-br from-[hsl(0,100%,40%)]/20 to-transparent"
animate={{
scale: [1, 1.05, 1],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<div className="absolute inset-4 rounded-2xl border border-white/10 flex items-center justify-center">
<motion.div
animate={{
scale: [1, 1.1, 1],
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
className="w-32 h-32 rounded-full bg-[hsl(0,100%,40%)]/30 flex items-center justify-center"
>
<div className="w-20 h-20 rounded-full bg-[hsl(0,100%,40%)]/50 flex items-center justify-center">
<MapPinIcon size={32} className="text-white" />
</div>
</motion.div>
</div>
{/* Floating location dots */}
{[
{ top: "20%", left: "30%" },
{ top: "40%", left: "60%" },
{ top: "60%", left: "25%" },
{ top: "75%", left: "70%" },
{ top: "30%", left: "75%" },
].map((pos, i) => (
<motion.div
key={i}
className="absolute w-3 h-3 rounded-full bg-[hsl(0,100%,55%)]"
style={{ top: pos.top, left: pos.left }}
animate={{
scale: [1, 1.5, 1],
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
delay: i * 0.3,
}}
/>
))}
</div>
</FadeIn>
</div>
</div>
</section>
{/* Sustainability CTA */}
<section className="section-padding bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-emerald-950/30 dark:to-teal-950/30 relative overflow-hidden">
{/* Background decoration */}
<motion.div
className="absolute top-0 right-0 w-96 h-96 bg-emerald-400 rounded-full filter blur-[150px] opacity-20"
animate={{
scale: [1, 1.2, 1],
x: [0, 50, 0],
}}
transition={{
duration: 8,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto text-center">
<FadeIn>
<motion.div
whileHover={{ rotate: 360 }}
transition={{ duration: 0.5 }}
className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-emerald-500 text-white mb-6"
>
<RecycleIcon size={32} />
</motion.div>
</FadeIn>
<FadeIn delay={0.1}>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-neutral-900 dark:text-white mb-6">
Committed to{" "}
<span className="text-emerald-600">Sustainability</span>
</h2>
</FadeIn>
<FadeIn delay={0.2}>
<p className="text-lg text-neutral-600 dark:text-neutral-400 mb-8 leading-relaxed">
Go green and find the perfect sustainable product options for
your business. We offer a comprehensive range of eco-friendly,
compostable, and recyclable alternatives.
</p>
</FadeIn>
<FadeIn delay={0.3}>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<AnimatedButton
href="/sustainability"
variant="accent"
size="lg"
icon={<LeafIcon size={18} />}
>
Explore Green Products
</AnimatedButton>
<AnimatedButton
href="/state-ordinances"
variant="outline"
size="lg"
>
State Ordinances
</AnimatedButton>
</div>
</FadeIn>
</div>
</div>
</section>
{/* Final CTA */}
<section className="section-padding bg-[hsl(0,100%,40%)] text-white relative overflow-hidden">
{/* Background pattern */}
<div className="absolute inset-0 opacity-10">
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="white" strokeWidth="0.5" />
</pattern>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
<div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<FadeIn>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-6">
Ready to Get Started?
</h2>
</FadeIn>
<FadeIn delay={0.1}>
<p className="text-xl text-white/90 mb-10 leading-relaxed">
Let&apos;s talk about your foodservice and janitorial needs. Our team
is here to help you find the perfect solutions.
</p>
</FadeIn>
<FadeIn delay={0.2}>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-block"
>
<AnimatedButton
href="/contact"
variant="secondary"
size="lg"
icon={<ArrowRightIcon size={18} />}
>
Contact Us Today
</AnimatedButton>
</motion.div>
</FadeIn>
</div>
</section>
<Footer />
</main>
); );
} }

View File

@ -9,6 +9,7 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"framer-motion": "^12.23.26",
"next": "16.1.0", "next": "16.1.0",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"

View File

@ -1713,6 +1713,15 @@ for-each@^0.3.3, for-each@^0.3.5:
dependencies: dependencies:
is-callable "^1.2.7" is-callable "^1.2.7"
framer-motion@^12.23.26:
version "12.23.26"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.26.tgz#2a684e9b156118e1c4989d7fc9327def83480391"
integrity sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==
dependencies:
motion-dom "^12.23.23"
motion-utils "^12.23.6"
tslib "^2.4.0"
function-bind@^1.1.2: function-bind@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
@ -2356,6 +2365,18 @@ minimist@^1.2.0, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
motion-dom@^12.23.23:
version "12.23.23"
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.23.23.tgz#8f874333ea1a04ee3a89eb928f518b463d589e0e"
integrity sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==
dependencies:
motion-utils "^12.23.6"
motion-utils@^12.23.6:
version "12.23.6"
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.23.6.tgz#fafef80b4ea85122dd0d6c599a0c63d72881f312"
integrity sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==
ms@^2.1.1, ms@^2.1.3: ms@^2.1.1, ms@^2.1.3:
version "2.1.3" version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"