React/Vite/shadcn-ui site for Gigafibre ISP. Address qualification via PostgreSQL (5.2M AQ addresses, pg_trgm fuzzy search). No Supabase dependency for address search. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
375 lines
17 KiB
TypeScript
375 lines
17 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { Link, useLocation } from "react-router-dom";
|
|
import {
|
|
Menu, X, Phone, Mail, ChevronDown, Search, User,
|
|
Wifi, PhoneCall, Building2, Signal, Server, Network,
|
|
Cable, Cloud, Shield, Settings, Laptop, Globe
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { SearchDialog } from "@/components/SearchDialog";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import targoLogo from "@/assets/targo-logo.png";
|
|
|
|
// Business menu items with icons and descriptions
|
|
const businessMenuItems = [
|
|
{
|
|
category: "CONNECTIVITÉ",
|
|
items: [
|
|
{ label: "Internet affaires", href: "/business/internet", icon: Globe, description: "Connexion fibre haute vitesse pour entreprises" },
|
|
{ label: "SD-WAN", href: "/business/sd-wan", icon: Network, description: "Réseau intelligent multi-sites" },
|
|
{ label: "Réseaux inter-sites", href: "/business/reseau-inter-sites", icon: Cable, description: "Liaisons dédiées entre vos succursales" },
|
|
]
|
|
},
|
|
{
|
|
category: "COMMUNICATIONS",
|
|
items: [
|
|
{ label: "Téléphonie IP", href: "/business/telephonie", icon: PhoneCall, description: "Système téléphonique cloud complet" },
|
|
{ label: "Liaisons SIP", href: "/business/sip", icon: Phone, description: "Trunks SIP pour votre PBX" },
|
|
{ label: "Amplification cellulaire", href: "/business/amplification-cellulaire", icon: Signal, description: "Signal mobile amplifié partout" },
|
|
]
|
|
},
|
|
{
|
|
category: "INFRASTRUCTURE",
|
|
items: [
|
|
{ label: "Services cloud", href: "/business/cloud", icon: Cloud, description: "Hébergement et sauvegarde sécurisés" },
|
|
{ label: "Virtualisation", href: "/business/virtualisation", icon: Server, description: "Infrastructure virtualisée sur mesure" },
|
|
{ label: "Microsoft 365", href: "/business/microsoft-365", icon: Laptop, description: "Suite Microsoft pour entreprises" },
|
|
]
|
|
},
|
|
{
|
|
category: "SERVICES",
|
|
items: [
|
|
{ label: "WiFi commercial", href: "/business/wifi", icon: Wifi, description: "WiFi haute densité pour commerces" },
|
|
{ label: "Câblage structuré", href: "/business/cablage", icon: Cable, description: "Installation réseau professionnelle" },
|
|
{ label: "Services gérés", href: "/business/services-geres", icon: Settings, description: "Gestion TI complète externalisée" },
|
|
{ label: "Sécurité DDoS", href: "/business/securite", icon: Shield, description: "Protection contre les cyberattaques" },
|
|
{ label: "Multilogement", href: "/business/multilogement", icon: Building2, description: "Solutions pour immeubles résidentiels" },
|
|
]
|
|
}
|
|
];
|
|
|
|
const Header = () => {
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
const location = useLocation();
|
|
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
setIsScrolled(window.scrollY > 20);
|
|
};
|
|
window.addEventListener("scroll", handleScroll);
|
|
return () => window.removeEventListener("scroll", handleScroll);
|
|
}, []);
|
|
|
|
// Lock body scroll when mobile menu is open
|
|
useEffect(() => {
|
|
if (isMenuOpen) {
|
|
document.body.style.overflow = "hidden";
|
|
} else {
|
|
document.body.style.overflow = "";
|
|
}
|
|
return () => { document.body.style.overflow = ""; };
|
|
}, [isMenuOpen]);
|
|
|
|
useEffect(() => {
|
|
const down = (e: KeyboardEvent) => {
|
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
setIsSearchOpen((open) => !open);
|
|
}
|
|
};
|
|
document.addEventListener("keydown", down);
|
|
return () => document.removeEventListener("keydown", down);
|
|
}, []);
|
|
|
|
const navLinks = [
|
|
{ label: "Internet", href: "/internet" },
|
|
{ label: "Télévision", href: "/television" },
|
|
{ label: "Téléphone", href: "/telephone" },
|
|
{ label: "Affaires", href: "/business", isMegaMenu: true },
|
|
{
|
|
label: "À propos",
|
|
href: "/support",
|
|
submenu: [
|
|
{ label: "Actualités", href: "/actualites" },
|
|
{ label: "Support", href: "/support" },
|
|
]
|
|
},
|
|
];
|
|
|
|
return (
|
|
<header className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${isScrolled ? "bg-white/90 backdrop-blur-xl shadow-2xl py-0" : "bg-white py-0"
|
|
}`}>
|
|
<SearchDialog open={isSearchOpen} onOpenChange={setIsSearchOpen} />
|
|
{/* Top bar */}
|
|
<motion.div
|
|
initial={false}
|
|
animate={{ height: isScrolled ? 0 : "auto", opacity: isScrolled ? 0 : 1 }}
|
|
transition={{ duration: 0.3 }}
|
|
className="bg-targo-green text-white py-2 px-4 shadow-md z-50 relative overflow-hidden hidden md:block"
|
|
>
|
|
<div className="container flex items-center justify-between text-xs font-semibold uppercase tracking-wider">
|
|
<div className="flex items-center gap-8">
|
|
<a href="tel:514.448.0773" className="flex items-center gap-2 hover:text-white/80 transition-colors">
|
|
<Phone className="w-3.5 h-3.5" />
|
|
<span>514.448.0773</span>
|
|
</a>
|
|
<a href="mailto:support@targo.ca" className="flex items-center gap-2 hover:text-white/80 transition-colors">
|
|
<Mail className="w-3.5 h-3.5" />
|
|
<span>support@targo.ca</span>
|
|
</a>
|
|
</div>
|
|
<span className="opacity-90">La puissance de la fibre locale</span>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Main nav */}
|
|
<nav className={`container py-4 transition-all duration-300 ${isScrolled ? "py-2" : "py-4"}`}>
|
|
<div className="flex items-center justify-between">
|
|
{/* Logo */}
|
|
<Link to="/" className="flex items-center group">
|
|
<img
|
|
src={targoLogo}
|
|
alt="Targo - 100% Fibre"
|
|
className="transition-transform group-hover:scale-105 flex-shrink-0 object-contain"
|
|
style={{ height: isScrolled ? 32 : 40, width: 'auto' }}
|
|
/>
|
|
</Link>
|
|
|
|
{/* Desktop nav */}
|
|
<div className="hidden md:flex items-center gap-8">
|
|
{navLinks.map((link) => (
|
|
link.isMegaMenu ? (
|
|
<Popover key={link.href}>
|
|
<PopoverTrigger className="flex items-center gap-1 text-gray-700 hover:text-targo-green transition-colors font-medium text-sm lg:text-base outline-none">
|
|
{link.label}
|
|
<ChevronDown className="h-4 w-4" />
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="w-[800px] p-6 bg-white border-gray-100 shadow-2xl rounded-xl"
|
|
align="center"
|
|
sideOffset={20}
|
|
>
|
|
<div className="grid grid-cols-4 gap-6">
|
|
{businessMenuItems.map((category) => (
|
|
<div key={category.category} className="space-y-3">
|
|
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider">
|
|
{category.category}
|
|
</h4>
|
|
<div className="space-y-1">
|
|
{category.items.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
to={item.href}
|
|
className="group flex items-start gap-3 p-2 rounded-lg hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div className="w-8 h-8 rounded-md bg-targo-green/10 flex items-center justify-center flex-shrink-0 group-hover:bg-targo-green/20 transition-colors">
|
|
<item.icon className="w-4 h-4 text-targo-green" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="font-medium text-gray-900 text-sm group-hover:text-targo-green transition-colors">
|
|
{item.label}
|
|
</div>
|
|
<div className="text-xs text-gray-500 line-clamp-2">
|
|
{item.description}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-6 pt-4 border-t border-gray-100">
|
|
<Link
|
|
to="/business"
|
|
className="inline-flex items-center text-sm font-medium text-targo-green hover:text-targo-green/80 transition-colors"
|
|
>
|
|
Voir toutes les solutions affaires
|
|
<ChevronDown className="w-4 h-4 ml-1 rotate-[-90deg]" />
|
|
</Link>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
) : link.submenu ? (
|
|
<DropdownMenu key={link.href}>
|
|
<DropdownMenuTrigger className="flex items-center gap-1 text-gray-700 hover:text-targo-green transition-colors font-medium text-sm lg:text-base outline-none">
|
|
{link.label}
|
|
<ChevronDown className="h-4 w-4" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="bg-white border-gray-100 text-gray-800 shadow-xl">
|
|
{link.submenu.map((sublink) => (
|
|
<DropdownMenuItem key={sublink.href} className="focus:bg-gray-50 focus:text-targo-green cursor-pointer" asChild>
|
|
<Link to={sublink.href} className="w-full">
|
|
{sublink.label}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : (
|
|
<Link
|
|
key={link.href}
|
|
to={link.href}
|
|
className="text-gray-700 hover:text-targo-green transition-colors font-medium text-sm lg:text-base"
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
)
|
|
))}
|
|
</div>
|
|
|
|
{/* CTA */}
|
|
<div className="hidden md:flex items-center gap-4">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-gray-700 hover:text-black hover:bg-black/5"
|
|
onClick={() => setIsSearchOpen(true)}
|
|
>
|
|
<Search className="w-5 h-5" />
|
|
</Button>
|
|
<Button variant="outline" className="border-gray-200 text-gray-700 bg-transparent hover:bg-gray-100 hover:text-black backdrop-blur-sm" asChild>
|
|
<a href="https://store.targo.ca/clients" target="_blank" rel="noopener noreferrer">Mon compte</a>
|
|
</Button>
|
|
<Button className="gradient-targo text-white shadow-lg shadow-targo-green/20 glow-targo border-none" asChild>
|
|
<a href="/#contact">Nous joindre</a>
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Mobile menu button */}
|
|
<div className="flex items-center gap-2 md:hidden">
|
|
<button
|
|
className="p-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
|
|
onClick={() => setIsSearchOpen(true)}
|
|
aria-label="Search"
|
|
>
|
|
<Search className="w-6 h-6" />
|
|
</button>
|
|
<button
|
|
className="p-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
|
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
aria-label="Toggle menu"
|
|
>
|
|
{isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile menu */}
|
|
<AnimatePresence>
|
|
{isMenuOpen && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: "100vh", opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
className="md:hidden overflow-y-auto bg-white absolute left-0 right-0 top-full px-6 shadow-2xl border-t border-gray-100 z-50"
|
|
>
|
|
<div className="flex flex-col gap-6 py-10">
|
|
{navLinks.map((link, idx) => (
|
|
<motion.div
|
|
initial={{ x: -20, opacity: 0 }}
|
|
animate={{ x: 0, opacity: 1 }}
|
|
transition={{ delay: idx * 0.05 }}
|
|
key={link.href}
|
|
>
|
|
{link.isMegaMenu ? (
|
|
<div className="flex flex-col gap-4">
|
|
<span className="text-gray-900 font-bold text-lg">{link.label}</span>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{businessMenuItems.flatMap(cat => cat.items).map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
to={item.href}
|
|
className="flex items-center gap-2 text-gray-600 hover:text-targo-green transition-colors text-sm"
|
|
onClick={() => setIsMenuOpen(false)}
|
|
>
|
|
<item.icon className="w-4 h-4 text-targo-green" />
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : link.submenu ? (
|
|
<div className="flex flex-col gap-4">
|
|
<span className="text-gray-900 font-bold text-lg">{link.label}</span>
|
|
<div className="pl-4 flex flex-col gap-4 py-2 border-l-2 border-targo-green/20">
|
|
{link.submenu.map((sublink) => (
|
|
<Link
|
|
key={sublink.href}
|
|
to={sublink.href}
|
|
className="text-gray-600 hover:text-targo-green transition-colors text-base"
|
|
onClick={() => setIsMenuOpen(false)}
|
|
>
|
|
{sublink.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Link
|
|
key={link.href}
|
|
to={link.href}
|
|
className="text-gray-900 hover:text-targo-green transition-colors font-bold text-lg"
|
|
onClick={() => setIsMenuOpen(false)}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
)}
|
|
</motion.div>
|
|
))}
|
|
|
|
<motion.div
|
|
initial={{ y: 20, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
transition={{ delay: 0.3 }}
|
|
className="flex flex-col gap-4 pt-10 border-t border-gray-100"
|
|
>
|
|
<Button variant="outline" className="w-full border-gray-200 text-gray-700 hover:bg-gray-50 bg-white h-14 text-lg font-semibold shadow-sm" asChild>
|
|
<a href="https://store.targo.ca/clients" target="_blank" rel="noopener noreferrer">
|
|
<User className="w-5 h-5 mr-2" />
|
|
Mon compte
|
|
</a>
|
|
</Button>
|
|
<Button className="w-full gradient-targo text-white h-14 text-lg font-bold glow-targo border-none" asChild>
|
|
<a href="/#contact">Nous joindre</a>
|
|
</Button>
|
|
|
|
<div className="flex flex-col gap-3 mt-4 text-gray-500">
|
|
<a href="tel:514.448.0773" className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-targo-green/10 flex items-center justify-center">
|
|
<Phone className="w-5 h-5 text-targo-green" />
|
|
</div>
|
|
<span className="font-semibold text-gray-900">514.448.0773</span>
|
|
</a>
|
|
<a href="mailto:support@targo.ca" className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-targo-green/10 flex items-center justify-center">
|
|
<Mail className="w-5 h-5 text-targo-green" />
|
|
</div>
|
|
<span className="font-semibold text-gray-900">support@targo.ca</span>
|
|
</a>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</nav>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
export default Header;
|