247 lines
15 KiB
TypeScript
247 lines
15 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { motion, useInView, useSpring, useTransform, useScroll, useMotionValueEvent } from 'framer-motion';
|
|
import Contact from '../../components/Contact';
|
|
|
|
const Counter = ({ value }: { value: number }) => {
|
|
const ref = useRef(null);
|
|
const isInView = useInView(ref, { once: true, margin: "-20%" });
|
|
const spring = useSpring(0, { mass: 3, stiffness: 75, damping: 30 });
|
|
const display = useTransform(spring, (current) =>
|
|
value % 1 !== 0 ? current.toFixed(1) : Math.round(current).toLocaleString()
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isInView) {
|
|
spring.set(value);
|
|
}
|
|
}, [isInView, value, spring]);
|
|
|
|
return <motion.span ref={ref}>{display}</motion.span>;
|
|
};
|
|
|
|
const AboutPage: React.FC = () => {
|
|
const timelineRef = useRef<HTMLDivElement>(null);
|
|
const [activeTimelineIndex, setActiveTimelineIndex] = useState(0);
|
|
|
|
const { scrollYProgress } = useScroll({
|
|
target: timelineRef,
|
|
offset: ["start end", "end center"]
|
|
});
|
|
|
|
const heightTransform = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
|
|
|
|
useMotionValueEvent(scrollYProgress, "change", (latest) => {
|
|
// Calculate index based on scroll progress with a slight offset to trigger earlier
|
|
const index = Math.floor(latest * 4.5); // 4 items, multiplier slightly > 4 ensures last one gets hit
|
|
setActiveTimelineIndex(Math.min(index, 3));
|
|
});
|
|
|
|
useEffect(() => {
|
|
window.scrollTo(0, 0);
|
|
}, []);
|
|
|
|
const stats = [
|
|
{ label: 'Businesses served', value: '150+' },
|
|
{ label: 'Uptime achieved', value: '99.9%' },
|
|
{ label: 'Years of service', value: '15+' },
|
|
{ label: 'Response time', value: '<2min' },
|
|
];
|
|
|
|
const values = [
|
|
{
|
|
title: 'Security-First',
|
|
desc: 'Every solution we implement prioritizes your data security and business continuity.',
|
|
icon: 'security'
|
|
},
|
|
{
|
|
title: 'Reliability',
|
|
desc: 'We build systems that work consistently, so you can depend on your technology.',
|
|
icon: 'verified'
|
|
},
|
|
{
|
|
title: 'Clarity',
|
|
desc: 'No tech jargon or hidden fees. We explain what we do and why it matters.',
|
|
icon: 'visibility'
|
|
}
|
|
];
|
|
|
|
const timeline = [
|
|
{ year: '2010', title: 'Founded in Corpus Christi', desc: 'Started with a mission to bring enterprise-level IT solutions to local businesses.' },
|
|
{ year: '2015', title: 'Expanded Service Portfolio', desc: 'Added cloud services and advanced networking to serve growing businesses.' },
|
|
{ year: '2020', title: 'Remote Work Transformation', desc: 'Helped 100+ businesses transition to secure remote work during the pandemic.' },
|
|
{ year: '2024', title: 'Leading the Coastal Bend', desc: 'Now serving 150+ businesses with modern, reliable IT infrastructure.' },
|
|
];
|
|
|
|
return (
|
|
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
|
|
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
|
|
|
|
{/* Hero Section */}
|
|
<section className="relative py-20 px-6 overflow-hidden">
|
|
<div className="max-w-7xl mx-auto text-center relative z-10">
|
|
<motion.h1
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="font-display text-4xl md:text-6xl font-bold mb-6 text-gray-900 dark:text-white"
|
|
>
|
|
Local IT expertise for the <br /><span className="text-gray-500 dark:text-gray-400">Coastal Bend</span>
|
|
</motion.h1>
|
|
<motion.p
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.1 }}
|
|
className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed"
|
|
>
|
|
Since 2010, we've been helping businesses in Corpus Christi and surrounding communities build reliable, secure technology foundations that drive growth.
|
|
</motion.p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Our Story */}
|
|
<section className="py-20 px-6 relative bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]">
|
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[100px] pointer-events-none" />
|
|
<div className="max-w-4xl mx-auto relative z-10">
|
|
<h2 className="font-display text-3xl font-bold mb-8 text-gray-900 dark:text-white">Our Story</h2>
|
|
<div className="prose dark:prose-invert max-w-none text-lg text-gray-600 dark:text-gray-300 space-y-6">
|
|
<p>
|
|
Bay Area Affiliates was founded with a simple belief: local businesses deserve the same level of IT expertise and reliability as large corporations, but with the personal touch that only comes from working with your neighbors.
|
|
</p>
|
|
<p>
|
|
Over the years, we've watched the Coastal Bend grow and change. We've helped businesses navigate technology challenges, from the transition to cloud computing to the rapid shift to remote work. Through it all, we've maintained our commitment to clear communication, reliable solutions, and exceptional service.
|
|
</p>
|
|
<p>
|
|
Today, we're proud to serve over 150 businesses across the region, from Corpus Christi to the smallest coastal communities. Our team combines deep technical expertise with real-world business understanding to deliver IT solutions that actually work for our clients.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Stats */}
|
|
<section className="py-16 px-6 bg-white/5 backdrop-blur-sm border-y border-white/5 text-white relative">
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))] pointer-events-none"></div>
|
|
<div className="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-8 text-center relative z-10">
|
|
{[
|
|
{ label: 'Businesses served', value: 150, suffix: '+' },
|
|
{ label: 'Uptime achieved', value: 99.9, suffix: '%' },
|
|
{ label: 'Years of service', value: 15, suffix: '+' },
|
|
{ label: 'Response time', value: 2, prefix: '<', suffix: 'min' },
|
|
].map((stat, index) => (
|
|
<div key={index} className="p-4">
|
|
<div className="text-4xl md:text-5xl font-bold mb-2 flex justify-center items-center gap-1">
|
|
{stat.prefix && <span>{stat.prefix}</span>}
|
|
<Counter value={stat.value} />
|
|
{stat.suffix && <span>{stat.suffix}</span>}
|
|
</div>
|
|
<div className="text-gray-400 font-medium">{stat.label}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Values */}
|
|
<section className="py-24 px-6 bg-gray-50 dark:bg-black/20 relative overflow-hidden">
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))] pointer-events-none"></div>
|
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-blue-500/5 dark:bg-white/5 rounded-full blur-[120px] pointer-events-none" />
|
|
<div className="max-w-7xl mx-auto relative z-10">
|
|
<div className="text-center mb-16">
|
|
<h2 className="font-display text-3xl font-bold mb-4 text-gray-900 dark:text-white">Our Values</h2>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
{values.map((value, index) => (
|
|
<motion.div
|
|
key={index}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: index * 0.1 }}
|
|
whileHover={{ y: -5 }}
|
|
className="bg-white dark:bg-[#161616] p-8 rounded-2xl shadow-lg border border-gray-100 dark:border-white/5"
|
|
>
|
|
<div className="w-12 h-12 bg-gray-100 dark:bg-white/10 rounded-xl flex items-center justify-center mb-6 text-gray-900 dark:text-white">
|
|
<span className="material-symbols-outlined text-2xl">{value.icon}</span>
|
|
</div>
|
|
<h3 className="text-xl font-bold mb-3 text-gray-900 dark:text-white">{value.title}</h3>
|
|
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
|
{value.desc}
|
|
</p>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Timeline */}
|
|
<section className="py-24 px-6 relative" ref={timelineRef}>
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))] pointer-events-none"></div>
|
|
<div className="max-w-4xl mx-auto relative z-10">
|
|
<h2 className="font-display text-3xl font-bold mb-12 text-center text-gray-900 dark:text-white">Our Journey</h2>
|
|
|
|
<div className="space-y-12 relative">
|
|
{/* Base Line */}
|
|
<div className="absolute left-[19px] md:left-1/2 top-0 bottom-0 w-0.5 bg-gray-200 dark:bg-white/10 -translate-x-1/2 md:translate-x-0"></div>
|
|
|
|
{/* Light Beam Line */}
|
|
<motion.div
|
|
style={{ height: heightTransform }}
|
|
className="absolute left-[19px] md:left-1/2 top-[-60px] w-0.5 bg-gradient-to-b from-black via-black to-transparent dark:from-white dark:via-white dark:to-transparent origin-top shadow-[0_0_25px_2px_rgba(0,0,0,0.5)] dark:shadow-[0_0_25px_2px_rgba(255,255,255,0.7)] -translate-x-1/2 md:translate-x-0 z-10"
|
|
></motion.div>
|
|
|
|
{timeline.map((item, index) => (
|
|
<motion.div
|
|
key={index}
|
|
initial={{ opacity: 0, x: index % 2 === 0 ? -50 : 50 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
className={`relative flex flex-col md:flex-row gap-8 ${index % 2 === 0 ? 'md:text-right' : 'md:flex-row-reverse md:text-left'}`}
|
|
>
|
|
<div className="md:w-1/2 pt-1">
|
|
<span className="inline-block px-4 py-1 rounded-full bg-gray-100 dark:bg-white/10 text-gray-900 dark:text-white font-bold text-sm mb-3">
|
|
{item.year}
|
|
</span>
|
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">{item.title}</h3>
|
|
<p className="text-gray-600 dark:text-gray-400">{item.desc}</p>
|
|
</div>
|
|
|
|
{/* Timeline Dot */}
|
|
<div className="absolute left-0 md:left-1/2 -translate-x-[5px] md:-translate-x-1/2 w-10 h-10 flex items-center justify-center z-20">
|
|
<motion.div
|
|
className="w-4 h-4 rounded-full border-2 transition-colors duration-500"
|
|
animate={index <= activeTimelineIndex ? {
|
|
backgroundColor: "#000000", // Will be white in dark mode via class if needed, checking global styles
|
|
scale: 1.5,
|
|
borderColor: "#000000",
|
|
boxShadow: "0 0 20px rgba(0,0,0,0.5)"
|
|
} : {
|
|
backgroundColor: "transparent",
|
|
scale: 1,
|
|
borderColor: "rgba(156, 163, 175, 0.5)", // gray-400
|
|
boxShadow: "none"
|
|
}}
|
|
style={{
|
|
// Manual dark mode override since animate doesn't support class names easily without variants
|
|
// Using CSS variable or just forcing white for dark mode if we knew the context,
|
|
// but better to rely on simpler style.
|
|
// We will assume 'bg-black dark:bg-white' equivalent behavior is desired.
|
|
// To do this simply, we'll set colors that work on both or use a conditional standard div.
|
|
// Let's use standard classes for the base and animate scale/shadow.
|
|
}}
|
|
>
|
|
<div className={`w-full h-full rounded-full ${index <= activeTimelineIndex ? 'bg-black dark:bg-white' : 'bg-gray-200 dark:bg-white/20'}`} />
|
|
</motion.div>
|
|
</div>
|
|
<div className="md:w-1/2"></div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<Contact />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
|
|
export default AboutPage;
|