Temuulen Ankhbayar 174ffd8fe3
Some checks failed
Pull Request Checks / Code Quality & Security (push) Has been cancelled
Security Audit / Security Vulnerability Audit (push) Has been cancelled
Security Audit / Dependency Review (push) Has been cancelled
Security Audit / CodeQL Security Analysis (push) Has been cancelled
Security Audit / Check Outdated Dependencies (push) Has been cancelled
refactor: unify service color system into single source of truth
Consolidate 3 separate color systems (ServiceCardAccentColor,
CarouselAccent, serviceAccents) into one canonical SERVICE_COLORS
map at shared/constants/service-colors.ts.

Palette: internet=blue, sim=emerald, vpn=violet, onsite=amber,
all business=slate. Removes 3 unused colors (indigo, cyan, rose)
that added visual noise without aiding recognition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:35:59 +09:00

395 lines
14 KiB
TypeScript

"use client";
import { useRef } from "react";
import { LayoutGroup, motion, useInView, useScroll, useTransform } from "framer-motion";
import { ArrowRight } from "lucide-react";
import { Button } from "@/components/atoms/button";
import TextRotate from "@/components/fancy/text/text-rotate";
const SERVICE_WORDS = ["Internet", "Phone Plans", "VPN", "IT Support", "Business"];
/* ------------------------------------------------------------------ */
/* Placeholder silhouettes — replace with final illustrations later */
/* ------------------------------------------------------------------ */
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>
);
}
function PersonWithPhoneSilhouette({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 120 240"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden="true"
>
{/* 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 (
<motion.div
className={className}
style={{ y: parallaxY }}
animate={{ y: [0, -floatDistance, 0] }}
transition={{
y: {
duration: floatDuration,
repeat: Infinity,
ease: "easeInOut",
delay: floatDelay,
},
}}
>
{children}
</motion.div>
);
}
/* ------------------------------------------------------------------ */
/* Hero Section */
/* ------------------------------------------------------------------ */
interface HeroSectionProps {
heroCTARef: React.RefObject<HTMLDivElement | null>;
}
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}
initial={{ opacity: 0, y: 32 }}
animate={heroInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 32 }}
transition={{ duration: 0.7, ease: "easeOut" as const }}
className="relative flex-1 flex items-center py-16 sm:py-20 lg:py-24 overflow-hidden"
>
{/* Gradient Background */}
<div className="absolute inset-0 bg-gradient-to-br from-surface-sunken via-background to-info-bg/80" />
{/* Dot Grid Pattern Overlay */}
<div
className="absolute inset-0 pointer-events-none"
aria-hidden="true"
style={{
backgroundImage: `radial-gradient(circle at center, color-mix(in oklch, var(--primary) 15%, transparent) 1px, transparent 1px)`,
backgroundSize: "24px 24px",
}}
/>
{/* Subtle gradient accent in corner */}
<div
className="absolute -top-32 -right-32 w-96 h-96 rounded-full pointer-events-none"
aria-hidden="true"
style={{
background:
"radial-gradient(circle, color-mix(in oklch, var(--info) 25%, transparent) 0%, transparent 70%)",
}}
/>
{/* Split Layout Container */}
<div className="relative mx-auto max-w-7xl w-full px-6 sm:px-10 lg:px-14 flex flex-col lg:flex-row items-center gap-10 lg:gap-16">
{/* Left — Text Content */}
<div className="flex-1 text-center lg:text-left">
<LayoutGroup>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold leading-tight text-foreground font-heading">
<span className="block">Seamless IT Solutions</span>
<motion.span
className="flex items-center justify-center lg:justify-start gap-2 sm:gap-3 mt-2"
layout
transition={{ type: "spring", damping: 30, stiffness: 400 }}
>
<span className="text-foreground">for</span>
<TextRotate
texts={SERVICE_WORDS}
mainClassName="text-white px-3 sm:px-4 bg-primary overflow-hidden py-1 sm:py-1.5 justify-center rounded-lg"
staggerFrom="last"
initial={{ y: "100%" }}
animate={{ y: 0 }}
exit={{ y: "-120%" }}
staggerDuration={0.025}
splitLevelClassName="overflow-hidden pb-0.5 sm:pb-1"
transition={{ type: "spring", damping: 30, stiffness: 400 }}
rotationInterval={2500}
/>
</motion.span>
</h1>
</LayoutGroup>
<p className="mt-6 text-base sm:text-lg text-muted-foreground leading-relaxed font-semibold max-w-2xl mx-auto lg:mx-0">
From connectivity to communication, we handle the complexity so you can focus on what
matters with dedicated English support across Japan.
</p>
<div
ref={heroCTARef}
className="mt-8 flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-3 sm:gap-4"
>
<Button
as="a"
href="/services"
variant="pill"
size="lg"
rightIcon={<ArrowRight className="h-5 w-5" />}
>
Find Your Plan
</Button>
<Button as="a" href="#contact" variant="pillOutline" size="lg">
Talk to Us
</Button>
</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 }}
/>
</div>
</div>
</motion.div>
);
}