refactor: replace hero SVG illustrations with services showcase card
Some checks are pending
Pull Request Checks / Code Quality & Security (push) Waiting to run
Security Audit / Security Vulnerability Audit (push) Waiting to run
Security Audit / Dependency Review (push) Waiting to run
Security Audit / CodeQL Security Analysis (push) Waiting to run
Security Audit / Check Outdated Dependencies (push) Waiting to run
Some checks are pending
Pull Request Checks / Code Quality & Security (push) Waiting to run
Security Audit / Security Vulnerability Audit (push) Waiting to run
Security Audit / Dependency Review (push) Waiting to run
Security Audit / CodeQL Security Analysis (push) Waiting to run
Security Audit / Check Outdated Dependencies (push) Waiting to run
Remove hand-drawn SVG silhouettes (house, person with phone, person with router) and floating parallax animations. Replace with a tabbed services showcase card showing Personal/Business services in a 2-column grid with service-colored icons and links. Cleaner, more functional, and directly guides users to services. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
174ffd8fe3
commit
10d1b8a52d
@ -1,242 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { LayoutGroup, motion, useInView, useScroll, useTransform } from "framer-motion";
|
||||
import { useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { AnimatePresence, LayoutGroup, motion, useInView } from "framer-motion";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Button } from "@/components/atoms/button";
|
||||
import TextRotate from "@/components/fancy/text/text-rotate";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { SERVICE_COLORS } from "@/shared/constants/service-colors";
|
||||
import { personalServices, businessServices } from "@/features/landing-page/data";
|
||||
|
||||
const SERVICE_WORDS = ["Internet", "Phone Plans", "VPN", "IT Support", "Business"];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Placeholder silhouettes — replace with final illustrations later */
|
||||
/* ------------------------------------------------------------------ */
|
||||
type Tab = "personal" | "business";
|
||||
|
||||
function HouseSilhouette({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 200 220"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Roof */}
|
||||
<polygon points="100,10 10,100 190,100" className="fill-primary/80" />
|
||||
{/* Chimney */}
|
||||
<rect x="145" y="40" width="20" height="45" rx="2" className="fill-primary/60" />
|
||||
{/* Body */}
|
||||
<rect x="30" y="100" width="140" height="110" rx="4" className="fill-primary/70" />
|
||||
{/* Door */}
|
||||
<rect x="80" y="140" width="40" height="70" rx="3" className="fill-background" />
|
||||
<circle cx="112" cy="178" r="3" className="fill-primary/50" />
|
||||
{/* Window left */}
|
||||
<rect x="45" y="115" width="28" height="28" rx="2" className="fill-info-bg" />
|
||||
<line x1="59" y1="115" x2="59" y2="143" className="stroke-primary/30" strokeWidth="2" />
|
||||
<line x1="45" y1="129" x2="73" y2="129" className="stroke-primary/30" strokeWidth="2" />
|
||||
{/* Window right */}
|
||||
<rect x="127" y="115" width="28" height="28" rx="2" className="fill-info-bg" />
|
||||
<line x1="141" y1="115" x2="141" y2="143" className="stroke-primary/30" strokeWidth="2" />
|
||||
<line x1="127" y1="129" x2="155" y2="129" className="stroke-primary/30" strokeWidth="2" />
|
||||
{/* WiFi signal on roof */}
|
||||
<path
|
||||
d="M100 30 Q100 20 108 16"
|
||||
className="stroke-info"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M100 30 Q100 14 114 8"
|
||||
className="stroke-info"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M100 30 Q100 8 120 0"
|
||||
className="stroke-info"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
/* ─── Service icon color lookup ─── */
|
||||
|
||||
const SERVICE_ICON_COLORS: Record<string, string> = {
|
||||
"/services/internet": `${SERVICE_COLORS.internet.iconBg} ${SERVICE_COLORS.internet.iconText}`,
|
||||
"/services/sim": `${SERVICE_COLORS.sim.iconBg} ${SERVICE_COLORS.sim.iconText}`,
|
||||
"/services/vpn": `${SERVICE_COLORS.vpn.iconBg} ${SERVICE_COLORS.vpn.iconText}`,
|
||||
"/services/onsite": `${SERVICE_COLORS.onsite.iconBg} ${SERVICE_COLORS.onsite.iconText}`,
|
||||
"/services/business": `${SERVICE_COLORS.business.iconBg} ${SERVICE_COLORS.business.iconText}`,
|
||||
};
|
||||
|
||||
function getIconColor(href: string) {
|
||||
return SERVICE_ICON_COLORS[href] ?? "bg-primary/10 text-primary";
|
||||
}
|
||||
|
||||
function PersonWithPhoneSilhouette({ className }: { className?: string }) {
|
||||
/* ─── Services Showcase Card ─── */
|
||||
|
||||
function ServicesShowcase() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("personal");
|
||||
const items = activeTab === "personal" ? personalServices : businessServices;
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 120 240"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
<div className="w-full max-w-md mx-auto lg:mx-0">
|
||||
{/* Glass card container */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/80 backdrop-blur-sm shadow-xl overflow-hidden">
|
||||
{/* Tab bar */}
|
||||
<div className="flex bg-muted/50 p-1.5 m-3 rounded-xl gap-1">
|
||||
{(["personal", "business"] as const).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={cn(
|
||||
"flex-1 relative rounded-lg px-4 py-2.5 text-sm font-semibold transition-colors",
|
||||
activeTab === tab
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground/80"
|
||||
)}
|
||||
>
|
||||
{/* Head */}
|
||||
<circle cx="60" cy="30" r="24" className="fill-primary/75" />
|
||||
{/* Body */}
|
||||
<path
|
||||
d="M36 60 C36 54 84 54 84 60 L88 160 C88 164 32 164 32 160 Z"
|
||||
className="fill-primary/65"
|
||||
/>
|
||||
{/* Left arm (down) */}
|
||||
<path d="M36 70 L14 130 L22 134 L40 80" className="fill-primary/55" />
|
||||
{/* Right arm (holding phone up to ear) */}
|
||||
<path d="M84 70 L96 60 L100 40 L92 38 L88 56 L80 66" className="fill-primary/55" />
|
||||
{/* Phone */}
|
||||
<rect x="90" y="28" width="14" height="24" rx="3" className="fill-foreground/80" />
|
||||
<rect x="92" y="32" width="10" height="16" rx="1" className="fill-info/40" />
|
||||
{/* Legs */}
|
||||
<path d="M44 158 L38 230 L52 230 L54 158" className="fill-primary/60" />
|
||||
<path d="M66 158 L68 230 L82 230 L76 158" className="fill-primary/60" />
|
||||
{/* Shoes */}
|
||||
<ellipse cx="45" cy="232" rx="14" ry="6" className="fill-primary/80" />
|
||||
<ellipse cx="75" cy="232" rx="14" ry="6" className="fill-primary/80" />
|
||||
{/* Signal waves from phone */}
|
||||
<path
|
||||
d="M104 30 Q112 26 112 18"
|
||||
className="stroke-info"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M106 30 Q118 24 118 12"
|
||||
className="stroke-info"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PersonWithRouterSilhouette({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 140 240"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Head */}
|
||||
<circle cx="70" cy="30" r="24" className="fill-primary/75" />
|
||||
{/* Body */}
|
||||
<path
|
||||
d="M46 60 C46 54 94 54 94 60 L98 160 C98 164 42 164 42 160 Z"
|
||||
className="fill-primary/65"
|
||||
/>
|
||||
{/* Left arm (holding router) */}
|
||||
<path d="M46 70 L18 110 L22 116 L46 82" className="fill-primary/55" />
|
||||
{/* Right arm */}
|
||||
<path d="M94 70 L116 130 L108 134 L90 80" className="fill-primary/55" />
|
||||
{/* Router box */}
|
||||
<rect x="2" y="100" width="44" height="16" rx="3" className="fill-foreground/80" />
|
||||
{/* Router antenna 1 */}
|
||||
<line
|
||||
x1="12"
|
||||
y1="100"
|
||||
x2="8"
|
||||
y2="78"
|
||||
className="stroke-foreground/70"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="8" cy="76" r="3" className="fill-info" />
|
||||
{/* Router antenna 2 */}
|
||||
<line
|
||||
x1="36"
|
||||
y1="100"
|
||||
x2="40"
|
||||
y2="78"
|
||||
className="stroke-foreground/70"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="40" cy="76" r="3" className="fill-info" />
|
||||
{/* Router LEDs */}
|
||||
<circle cx="14" cy="108" r="2" className="fill-green-400" />
|
||||
<circle cx="22" cy="108" r="2" className="fill-green-400" />
|
||||
<circle cx="30" cy="108" r="2" className="fill-info" />
|
||||
{/* WiFi waves from router */}
|
||||
<path
|
||||
d="M24 76 Q24 66 16 60"
|
||||
className="stroke-info"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M24 76 Q24 60 12 50"
|
||||
className="stroke-info"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M24 76 Q24 54 8 40"
|
||||
className="stroke-info"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
{/* Legs */}
|
||||
<path d="M54 158 L48 230 L62 230 L64 158" className="fill-primary/60" />
|
||||
<path d="M76 158 L78 230 L92 230 L86 158" className="fill-primary/60" />
|
||||
{/* Shoes */}
|
||||
<ellipse cx="55" cy="232" rx="14" ry="6" className="fill-primary/80" />
|
||||
<ellipse cx="85" cy="232" rx="14" ry="6" className="fill-primary/80" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Floating wrapper — ambient bob + scroll parallax */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function FloatingIllustration({
|
||||
children,
|
||||
floatDelay,
|
||||
floatDuration,
|
||||
floatDistance,
|
||||
parallaxRange,
|
||||
scrollProgress,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
floatDelay: number;
|
||||
floatDuration: number;
|
||||
floatDistance: number;
|
||||
parallaxRange: [number, number];
|
||||
scrollProgress: import("framer-motion").MotionValue<number>;
|
||||
className?: string;
|
||||
}) {
|
||||
const parallaxY = useTransform(scrollProgress, [0, 1], parallaxRange);
|
||||
|
||||
return (
|
||||
{activeTab === tab && (
|
||||
<motion.div
|
||||
className={className}
|
||||
style={{ y: parallaxY }}
|
||||
animate={{ y: [0, -floatDistance, 0] }}
|
||||
transition={{
|
||||
y: {
|
||||
duration: floatDuration,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: floatDelay,
|
||||
},
|
||||
}}
|
||||
layoutId="hero-tab-bg"
|
||||
className="absolute inset-0 rounded-lg bg-background shadow-sm"
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10">
|
||||
{tab === "personal" ? "Personal Services" : "Business Services"}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Service grid */}
|
||||
<div className="px-5 pb-5 pt-2">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="grid grid-cols-2 gap-3"
|
||||
>
|
||||
{children}
|
||||
{items.map((service, i) => (
|
||||
<motion.div
|
||||
key={service.title}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: i * 0.05, duration: 0.2 }}
|
||||
>
|
||||
<Link
|
||||
href={service.href}
|
||||
className="group flex flex-col items-center gap-2.5 rounded-xl bg-muted/30 hover:bg-muted/60 border border-transparent hover:border-border/40 p-4 transition-all duration-200 hover:-translate-y-0.5"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-12 w-12 items-center justify-center rounded-xl transition-transform group-hover:scale-110",
|
||||
getIconColor(service.href)
|
||||
)}
|
||||
>
|
||||
{service.icon}
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-foreground text-center leading-tight">
|
||||
{service.title}
|
||||
</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Hero Section */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* ─── Hero Section ─── */
|
||||
|
||||
interface HeroSectionProps {
|
||||
heroCTARef: React.RefObject<HTMLDivElement | null>;
|
||||
@ -246,11 +120,6 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
|
||||
const heroRef = useRef<HTMLDivElement>(null);
|
||||
const heroInView = useInView(heroRef, { once: true, amount: 0.1 });
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: heroRef,
|
||||
offset: ["start start", "end start"],
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={heroRef}
|
||||
@ -333,60 +202,9 @@ export function HeroSection({ heroCTARef }: HeroSectionProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right — Floating Illustrations */}
|
||||
<div className="flex-1 relative min-h-[300px] sm:min-h-[360px] lg:min-h-[420px] w-full max-w-lg lg:max-w-none">
|
||||
{/* House — center back, largest */}
|
||||
<FloatingIllustration
|
||||
floatDelay={0}
|
||||
floatDuration={5}
|
||||
floatDistance={8}
|
||||
parallaxRange={[0, -30]}
|
||||
scrollProgress={scrollYProgress}
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-0 w-40 sm:w-48 lg:w-56 drop-shadow-lg"
|
||||
>
|
||||
<HouseSilhouette />
|
||||
</FloatingIllustration>
|
||||
|
||||
{/* Person with phone — left front */}
|
||||
<FloatingIllustration
|
||||
floatDelay={0.8}
|
||||
floatDuration={4.2}
|
||||
floatDistance={12}
|
||||
parallaxRange={[0, -50]}
|
||||
scrollProgress={scrollYProgress}
|
||||
className="absolute left-2 sm:left-4 lg:left-0 bottom-0 w-20 sm:w-24 lg:w-28 drop-shadow-md"
|
||||
>
|
||||
<PersonWithPhoneSilhouette />
|
||||
</FloatingIllustration>
|
||||
|
||||
{/* Person with router — right front */}
|
||||
<FloatingIllustration
|
||||
floatDelay={1.5}
|
||||
floatDuration={4.8}
|
||||
floatDistance={10}
|
||||
parallaxRange={[0, -40]}
|
||||
scrollProgress={scrollYProgress}
|
||||
className="absolute right-2 sm:right-4 lg:right-0 bottom-0 w-24 sm:w-28 lg:w-32 drop-shadow-md"
|
||||
>
|
||||
<PersonWithRouterSilhouette />
|
||||
</FloatingIllustration>
|
||||
|
||||
{/* Decorative floating dots */}
|
||||
<motion.div
|
||||
className="absolute top-8 left-10 w-3 h-3 rounded-full bg-primary/30"
|
||||
animate={{ y: [0, -10, 0], opacity: [0.3, 0.6, 0.3] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute top-16 right-16 w-2 h-2 rounded-full bg-info/40"
|
||||
animate={{ y: [0, -8, 0], opacity: [0.4, 0.7, 0.4] }}
|
||||
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute top-4 right-1/3 w-2.5 h-2.5 rounded-full bg-primary/20"
|
||||
animate={{ y: [0, -12, 0], opacity: [0.2, 0.5, 0.2] }}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
|
||||
/>
|
||||
{/* Right — Services Showcase */}
|
||||
<div className="flex-1 w-full max-w-md lg:max-w-none">
|
||||
<ServicesShowcase />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user