feat: add robotics components and data structures
- Introduced RobotProductCard component for displaying robot details. - Added WhyUs component highlighting key reasons for choosing our robotics solutions. - Implemented CursorSpotlight for enhanced user interaction. - Created GlassPanel for a stylish UI element. - Developed MotionSection for animated section visibility. - Added PremiumButton for versatile button options. - Established data structures for industries and robots, including detailed specifications and use cases. - Included utility functions for retrieving robots by slug and category.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Official App Store + Google Play badge anchors.
|
||||
*
|
||||
* Setup steps:
|
||||
* 1. Download official SVGs (use the /show/ path which returns clean SVG):
|
||||
* https://www.svgrepo.com/show/303128/download-on-the-app-store-apple-logo.svg
|
||||
* https://www.svgrepo.com/show/303139/google-play-badge-logo.svg
|
||||
* 2. Save to public/images/app-store-badge.svg + public/images/google-play-badge.svg.
|
||||
* 3. Add to lib/content.ts:
|
||||
* export const app = {
|
||||
* googlePlay: "https://play.google.com/store/apps/details?id=...",
|
||||
* appStore: "https://apps.apple.com/ae/app/.../id...",
|
||||
* };
|
||||
*
|
||||
* next/image blocks raw SVG by default in Next 16 — keep plain <img>.
|
||||
*/
|
||||
import { app } from "@/lib/content";
|
||||
|
||||
type Props = {
|
||||
store: "appstore" | "googleplay";
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
export function AppBadge({ store, small }: Props) {
|
||||
const isApple = store === "appstore";
|
||||
const href = isApple ? app.appStore : app.googlePlay;
|
||||
const src = isApple
|
||||
? "/images/app-store-badge.svg"
|
||||
: "/images/google-play-badge.svg";
|
||||
const alt = isApple ? "Download on the App Store" : "Get it on Google Play";
|
||||
const label = isApple
|
||||
? "Download on the App Store"
|
||||
: "Get it on Google Play";
|
||||
const width = small ? 132 : 160;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={label}
|
||||
className="group inline-flex transition duration-300 hover:-translate-y-0.5 hover:opacity-85"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={small ? 40 : 48}
|
||||
className={
|
||||
small
|
||||
? "h-auto w-[132px] max-w-full select-none"
|
||||
: "h-auto w-[140px] max-w-full select-none sm:w-[160px]"
|
||||
}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
/** Both badges in a vertical stack. Use in footer Legal column. */
|
||||
export function AppBadgeStack({ small }: { small?: boolean }) {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<AppBadge store="appstore" small={small} />
|
||||
<AppBadge store="googleplay" small={small} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 4-image certificate gallery — each tile links to a PDF in a new tab.
|
||||
*
|
||||
* Expected data shape:
|
||||
* export type CertPage = {
|
||||
* src: string; // /images/certifications/page.jpg
|
||||
* alt: string;
|
||||
* title: string; // ISCC Certificate 2025
|
||||
* page: string; // Page 1 · Annex
|
||||
* document: string; // ISCC Certificate
|
||||
* href: string; // /documents/certifications/iscc.pdf OR upstream URL
|
||||
* };
|
||||
* export const certificatePages: CertPage[] = [ ... ];
|
||||
*/
|
||||
import Image from "next/image";
|
||||
import { ArrowUpRight, FileText } from "lucide-react";
|
||||
import { motion, type Variants } from "framer-motion";
|
||||
|
||||
export type CertPage = {
|
||||
src: string;
|
||||
alt: string;
|
||||
title: string;
|
||||
page: string;
|
||||
document: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
const grid: Variants = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.15 } },
|
||||
};
|
||||
const cell: Variants = {
|
||||
hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
filter: "blur(0px)",
|
||||
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] },
|
||||
},
|
||||
};
|
||||
|
||||
export function CertificateGallery({ pages }: { pages: CertPage[] }) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={grid}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.1 }}
|
||||
className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4 lg:gap-6"
|
||||
>
|
||||
{pages.map((p) => (
|
||||
<motion.div key={p.src} variants={cell}>
|
||||
<CertCard {...p} />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function CertCard({ src, alt, title, page, document, href }: CertPage) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${title} — ${page}. Opens PDF in a new tab.`}
|
||||
className="group relative isolate flex h-full flex-col overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/35 hover:bg-graphite/60 focus:outline-none focus-visible:border-gold/60 focus-visible:ring-2 focus-visible:ring-gold/40"
|
||||
>
|
||||
<div className="relative aspect-[3/4] w-full overflow-hidden bg-obsidian">
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||
className="object-cover object-top transition-transform duration-[1200ms] ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:scale-[1.04]"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-20 bg-gradient-to-b from-obsidian/70 via-obsidian/20 to-transparent"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-obsidian/80 via-obsidian/30 to-transparent"
|
||||
/>
|
||||
<span className="absolute left-3 top-3 inline-flex items-center gap-2 rounded-full border border-gold/30 bg-obsidian/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.22em] text-gold-light backdrop-blur">
|
||||
<span className="size-1 rounded-full bg-gold-light shadow-[0_0_8px_rgba(242,194,91,0.8)]" />
|
||||
{page}
|
||||
</span>
|
||||
<span className="absolute right-3 top-3 grid size-9 place-items-center rounded-full border border-bone/20 bg-obsidian/70 text-mist backdrop-blur transition-all duration-500 group-hover:-translate-y-0.5 group-hover:border-gold/50 group-hover:text-gold-light">
|
||||
<ArrowUpRight className="size-4" />
|
||||
</span>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -bottom-16 -right-16 size-48 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(212,164,55,0.32), transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 rounded-t-2xl border border-transparent transition-colors duration-500 group-hover:border-gold/40"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex flex-col gap-2 p-5 md:p-6">
|
||||
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-gold-light">
|
||||
{document}
|
||||
</span>
|
||||
<h4 className="font-display text-lg text-bone text-balance transition-colors duration-500 group-hover:text-gold-light md:text-xl">
|
||||
{title}
|
||||
</h4>
|
||||
<div className="mt-1 inline-flex items-center gap-2 text-xs text-mist">
|
||||
<FileText className="size-3.5 text-gold-light" />
|
||||
<span>Open PDF</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute bottom-0 left-0 h-px w-0 bg-gradient-to-r from-gold via-gold-light to-transparent transition-all duration-700 group-hover:w-full"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Premium horizontal logo marquee.
|
||||
*
|
||||
* Setup:
|
||||
* 1. globals.css must have:
|
||||
* @keyframes marquee {
|
||||
* 0% { transform: translateX(0); }
|
||||
* 100% { transform: translateX(-50%); }
|
||||
* }
|
||||
* 2. Pass clients prop or create lib/clients.ts:
|
||||
* export const clients = [
|
||||
* { src: "/images/clients/brand-a.png", alt: "Brand A" },
|
||||
* ...
|
||||
* ];
|
||||
*/
|
||||
import Image from "next/image";
|
||||
import { useReducedMotion } from "framer-motion";
|
||||
|
||||
export type Client = { src: string; alt: string };
|
||||
|
||||
export function ClientLogoMarquee({
|
||||
clients,
|
||||
eyebrow = "Our Clients",
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
clients: Client[];
|
||||
eyebrow?: string;
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
}) {
|
||||
const reduce = useReducedMotion();
|
||||
const loop = [...clients, ...clients];
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden py-16 md:py-20 lg:py-24">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -left-32 top-1/3 size-[480px] rounded-full radial-gold blur-3xl opacity-30"
|
||||
/>
|
||||
<div className="mx-auto max-w-7xl px-6 md:px-10">
|
||||
<div className="flex flex-col items-center gap-6 text-center">
|
||||
<div className="flex items-center gap-3 text-[11px] font-medium uppercase tracking-[0.22em] text-mist">
|
||||
<span className="h-px w-8 bg-gold/40" />
|
||||
<span>{eyebrow}</span>
|
||||
</div>
|
||||
<h2 className="font-display text-4xl leading-[1.04] text-bone text-balance md:text-5xl">
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle ? (
|
||||
<p className="max-w-2xl text-pretty text-mist md:text-lg">{subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 md:mt-14">
|
||||
{/* edge fades */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r from-obsidian to-transparent md:w-32"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l from-obsidian to-transparent md:w-32"
|
||||
/>
|
||||
<div
|
||||
className="flex overflow-hidden"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(90deg, transparent, black 8%, black 92%, transparent)",
|
||||
}}
|
||||
>
|
||||
{reduce ? (
|
||||
<ClientGrid clients={clients} />
|
||||
) : (
|
||||
<div
|
||||
className="flex shrink-0 gap-4 pr-4 md:gap-6 md:pr-6"
|
||||
style={{
|
||||
animation: "marquee 50s linear infinite",
|
||||
animationPlayState: "running",
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.animationPlayState = "paused")
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.animationPlayState = "running")
|
||||
}
|
||||
>
|
||||
{loop.map((c, i) => (
|
||||
<LogoTile key={`${c.src}-${i}`} client={c} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function LogoTile({ client }: { client: Client }) {
|
||||
return (
|
||||
<div className="group/tile relative grid h-24 w-44 shrink-0 place-items-center overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 px-5 backdrop-blur-sm transition-all duration-500 hover:border-gold/30 hover:bg-graphite/60 md:h-28 md:w-52">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-700 group-hover/tile:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(60% 80% at 50% 50%, rgba(212,164,55,0.18), transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<Image
|
||||
src={client.src}
|
||||
alt={client.alt}
|
||||
width={200}
|
||||
height={100}
|
||||
className="relative max-h-14 w-auto object-contain opacity-70 brightness-[1.05] contrast-[0.9] grayscale transition-all duration-500 group-hover/tile:scale-105 group-hover/tile:opacity-100 group-hover/tile:grayscale-0 md:max-h-16"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientGrid({ clients }: { clients: Client[] }) {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
||||
{clients.map((c) => (
|
||||
<LogoTile key={c.src} client={c} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Premium 4-column footer w/ no duplicated nav links.
|
||||
*
|
||||
* Expected lib/content.ts shape:
|
||||
*
|
||||
* export const brand = {
|
||||
* name: "BRAND",
|
||||
* legalName: "Brand Legal LLC",
|
||||
* phone: "00 000 0000",
|
||||
* email: "info@example.com",
|
||||
* instagram: "@brand_handle",
|
||||
* };
|
||||
* export const socials = {
|
||||
* instagram: "https://www.instagram.com/brand_handle/",
|
||||
* whatsapp: "https://api.whatsapp.com/send/?phone=000000000000&text&type=phone_number&app_absent=0",
|
||||
* whatsappDisplay: "+00 00 000 0000",
|
||||
* };
|
||||
* export const credit = {
|
||||
* agency: "YS Lootah Tech",
|
||||
* url: "https://yslootahtech.com/",
|
||||
* };
|
||||
*/
|
||||
import { Mail, Phone } from "lucide-react";
|
||||
import { brand, socials, credit } from "@/lib/content";
|
||||
import { WhatsAppLink } from "./WhatsAppLink";
|
||||
import { AppBadge } from "./AppBadges";
|
||||
|
||||
function InstagramIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="5" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<circle cx="17.5" cy="6.5" r="1.1" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
type Link = { label: string; href: string };
|
||||
|
||||
type Props = {
|
||||
navigationLinks: Link[];
|
||||
serviceItems: Link[];
|
||||
legalLinks: Link[];
|
||||
/** Brand logo node (provide your own LogoMark) */
|
||||
logo?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Footer({ navigationLinks, serviceItems, legalLinks, logo }: Props) {
|
||||
return (
|
||||
<footer className="relative overflow-hidden border-t border-bone/10 bg-obsidian pb-8 pt-11 md:pb-10 md:pt-14 lg:pb-12 lg:pt-[72px]">
|
||||
<div className="relative z-10 mx-auto max-w-[1480px] px-6 md:px-10">
|
||||
<div className="grid gap-10 lg:grid-cols-12 lg:gap-12">
|
||||
{/* Col 1 — Brand + contact */}
|
||||
<div className="space-y-5 lg:col-span-4">
|
||||
<a href="/" className="inline-flex items-center gap-4">
|
||||
{logo}
|
||||
<span className="hidden font-display text-2xl tracking-[0.22em] text-bone sm:inline">
|
||||
{brand.name}
|
||||
</span>
|
||||
</a>
|
||||
<p className="max-w-md text-sm text-mist text-pretty md:text-base">
|
||||
{brand.legalName}.
|
||||
</p>
|
||||
<div className="metallic-divider" />
|
||||
<div className="space-y-2 text-sm text-mist">
|
||||
<a
|
||||
href={`tel:${brand.phone.replace(/\s/g, "")}`}
|
||||
className="flex items-center gap-3 hover:text-gold-light"
|
||||
>
|
||||
<Phone className="size-4" />
|
||||
{brand.phone}
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${brand.email}`}
|
||||
className="flex items-center gap-3 hover:text-gold-light"
|
||||
>
|
||||
<Mail className="size-4" />
|
||||
{brand.email}
|
||||
</a>
|
||||
<WhatsAppLink
|
||||
href={socials.whatsapp}
|
||||
display={socials.whatsappDisplay}
|
||||
ariaLabel={`Chat with ${brand.name} on WhatsApp`}
|
||||
inline
|
||||
/>
|
||||
<a
|
||||
href={socials.instagram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 hover:text-gold-light"
|
||||
>
|
||||
<InstagramIcon className="size-4" />
|
||||
Instagram · {brand.instagram}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cols 2-4 */}
|
||||
<div className="grid gap-8 sm:grid-cols-3 lg:col-span-8 lg:gap-10">
|
||||
<FooterCol title="Navigation" links={navigationLinks} />
|
||||
<FooterCol title="Services" links={serviceItems} />
|
||||
<div>
|
||||
<h4 className="text-[10px] font-medium uppercase tracking-[0.22em] text-gold-light">
|
||||
Legal & App
|
||||
</h4>
|
||||
<ul className="mt-4 space-y-2.5">
|
||||
{legalLinks.map((l) => (
|
||||
<li key={l.label}>
|
||||
<a
|
||||
href={l.href}
|
||||
className="text-sm text-mist transition-colors hover:text-bone"
|
||||
>
|
||||
{l.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-5 flex flex-col items-start gap-2">
|
||||
<AppBadge store="appstore" small />
|
||||
<AppBadge store="googleplay" small />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-9 flex flex-col items-start justify-between gap-3 border-t border-bone/10 pt-5 text-xs uppercase tracking-[0.22em] text-mist md:mt-10 md:flex-row md:items-center md:gap-6">
|
||||
<span>
|
||||
© {new Date().getFullYear()} {brand.legalName}. All rights reserved.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-[11px] tracking-[0.04em] text-mist/60 md:text-xs">
|
||||
Website designed & developed by{" "}
|
||||
<a
|
||||
href={credit.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-mist/80 underline decoration-gold/30 decoration-1 underline-offset-4 transition-colors duration-500 hover:text-gold-light hover:decoration-gold/70"
|
||||
>
|
||||
{credit.agency}
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Watermark — contained inside footer */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex select-none justify-center overflow-hidden"
|
||||
>
|
||||
<span className="translate-y-[18%] font-display text-[14vw] leading-none tracking-[-0.02em] text-bone/[0.025] md:text-[12vw]">
|
||||
{brand.name}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterCol({ title, links }: { title: string; links: Link[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-medium uppercase tracking-[0.22em] text-gold-light">
|
||||
{title}
|
||||
</h4>
|
||||
<ul className="mt-4 space-y-2.5">
|
||||
{links.map((l) => (
|
||||
<li key={l.label}>
|
||||
<a
|
||||
href={l.href}
|
||||
className="text-sm text-mist transition-colors hover:text-bone"
|
||||
>
|
||||
{l.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
Icon?: LucideIcon;
|
||||
title: ReactNode;
|
||||
body?: ReactNode;
|
||||
href?: string;
|
||||
external?: boolean;
|
||||
className?: string;
|
||||
/** Append custom inner content below the body */
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function GlassCard({
|
||||
Icon,
|
||||
title,
|
||||
body,
|
||||
href,
|
||||
external,
|
||||
className,
|
||||
children,
|
||||
}: Props) {
|
||||
const inner = (
|
||||
<>
|
||||
{/* hover amber radial glow */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -bottom-16 -right-16 size-48 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(212,164,55,0.28), transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
{Icon ? (
|
||||
<div className="grid size-11 place-items-center rounded-full glass-gold transition-all duration-500 group-hover:rotate-[8deg] group-hover:shadow-[0_0_22px_-6px_rgba(212,164,55,0.6)]">
|
||||
<Icon className="size-[18px] text-gold-light" strokeWidth={1.4} />
|
||||
</div>
|
||||
) : null}
|
||||
<h4 className="mt-5 font-display text-xl text-bone transition-colors duration-500 group-hover:text-gold-light md:text-2xl">
|
||||
{title}
|
||||
</h4>
|
||||
{body ? (
|
||||
<p className="mt-2 text-sm text-mist text-pretty leading-relaxed">
|
||||
{body}
|
||||
</p>
|
||||
) : null}
|
||||
{children}
|
||||
{/* gold underline sweep */}
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute bottom-0 left-0 h-px w-0 bg-gradient-to-r from-gold via-gold-light to-transparent transition-all duration-700 group-hover:w-full"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const base = `group relative isolate h-full overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 p-6 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/30 hover:bg-graphite/60 md:p-7 ${className ?? ""}`;
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...(external
|
||||
? { target: "_blank", rel: "noopener noreferrer" }
|
||||
: {})}
|
||||
className={base}
|
||||
>
|
||||
{inner}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <div className={base}>{inner}</div>;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
type Variant = "primary" | "ghost" | "outline";
|
||||
|
||||
type Props = ComponentPropsWithoutRef<"a"> & {
|
||||
variant?: Variant;
|
||||
arrow?: boolean;
|
||||
};
|
||||
|
||||
const base =
|
||||
"relative inline-flex items-center gap-2 rounded-full px-7 py-3.5 text-sm font-medium tracking-wide transition-all duration-500";
|
||||
|
||||
const variants: Record<Variant, string> = {
|
||||
primary:
|
||||
"bg-gradient-to-r from-gold-light via-gold to-gold-deep text-obsidian shadow-[0_10px_40px_-12px_rgba(212,164,55,0.6)] hover:shadow-[0_18px_50px_-12px_rgba(242,194,91,0.8)]",
|
||||
outline:
|
||||
"border border-bone/20 text-bone hover:border-gold/60 hover:text-gold-light",
|
||||
ghost: "text-bone hover:text-gold-light",
|
||||
};
|
||||
|
||||
export function PremiumButton({
|
||||
variant = "primary",
|
||||
className,
|
||||
children,
|
||||
arrow = true,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
<a className={cn(base, variants[variant], className)} {...rest}>
|
||||
<span>{children}</span>
|
||||
{arrow ? (
|
||||
<ArrowUpRight className="size-4 transition-transform duration-500 group-hover:translate-x-0.5" />
|
||||
) : null}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { motion, useReducedMotion, type Variants } from "framer-motion";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
eyebrow?: string;
|
||||
index?: string;
|
||||
title: ReactNode;
|
||||
/** The gold-accent phrase rendered inside the title — pass as JSX or separate prop. */
|
||||
accent?: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
align?: "left" | "center";
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function SectionHeading({
|
||||
eyebrow,
|
||||
index,
|
||||
title,
|
||||
accent,
|
||||
subtitle,
|
||||
align = "left",
|
||||
className,
|
||||
}: Props) {
|
||||
const reduce = useReducedMotion();
|
||||
const v: Variants = {
|
||||
hidden: { opacity: 0, y: reduce ? 0 : 24, filter: reduce ? "blur(0px)" : "blur(6px)" },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
filter: "blur(0px)",
|
||||
transition: { duration: 0.9, ease: [0.16, 1, 0.3, 1] },
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-6",
|
||||
align === "center" ? "items-center text-center" : "items-start",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{eyebrow ? (
|
||||
<div className="flex items-center gap-3 text-[11px] font-medium uppercase tracking-[0.22em] text-mist">
|
||||
{index ? <span className="text-gold/85">{index}</span> : null}
|
||||
<span className="h-px w-8 bg-gold/40" />
|
||||
<span className="text-mist/85">{eyebrow}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<motion.h2
|
||||
variants={v}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="font-display text-[clamp(2.4rem,5vw,4.6rem)] leading-[1.02] tracking-[-0.02em] text-bone text-balance"
|
||||
>
|
||||
{title}
|
||||
{accent ? <> <span className="gold-text">{accent}</span></> : null}
|
||||
</motion.h2>
|
||||
{subtitle ? (
|
||||
<motion.p
|
||||
variants={v}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="max-w-2xl text-pretty text-mist md:text-lg"
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Refined gold WhatsApp link.
|
||||
*
|
||||
* Use 3 surfaces:
|
||||
* - <WhatsAppLink href={...} display="+CC XX XXX XXXX" ariaLabel="..." inline /> (footer row)
|
||||
* - <WhatsAppIconButton href={...} ariaLabel="..." /> (nav)
|
||||
* - <WhatsAppLink href={...} display={...} ariaLabel={...} /> (contact card)
|
||||
*
|
||||
* URL format expected: https://api.whatsapp.com/send/?phone=COUNTRYCODE+DIGITS&text&type=phone_number&app_absent=0
|
||||
* Phone in URL = digits only (no + or spaces). Display string = formatted "+CC XX XXX XXXX".
|
||||
*
|
||||
* lucide-react has no WhatsApp glyph → use this inline SVG.
|
||||
*/
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function WhatsAppIcon({ className, ...rest }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden
|
||||
className={className}
|
||||
{...rest}
|
||||
>
|
||||
<path d="M19.11 4.86A9.94 9.94 0 0 0 12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.33 4.95L2 22l5.27-1.38a9.93 9.93 0 0 0 4.76 1.21h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.84-7.06ZM12.04 20.15c-1.5 0-2.96-.4-4.24-1.17l-.3-.18-3.13.82.83-3.05-.2-.31a8.21 8.21 0 0 1-1.26-4.35c0-4.53 3.69-8.22 8.22-8.22 2.2 0 4.27.86 5.82 2.41a8.16 8.16 0 0 1 2.41 5.81c0 4.53-3.69 8.24-8.15 8.24Zm4.72-6.16c-.26-.13-1.53-.75-1.76-.83-.24-.09-.41-.13-.58.13-.17.26-.66.83-.81 1-.15.17-.3.2-.55.07-.26-.13-1.1-.41-2.1-1.29-.78-.69-1.3-1.55-1.45-1.81-.15-.26-.02-.4.11-.53.11-.11.26-.3.39-.45.13-.15.17-.26.26-.43.09-.17.04-.32-.02-.45-.07-.13-.58-1.4-.79-1.92-.21-.5-.42-.43-.58-.44-.15-.01-.32-.01-.49-.01-.17 0-.45.06-.69.32-.24.26-.9.88-.9 2.15 0 1.27.92 2.5 1.05 2.67.13.17 1.82 2.78 4.42 3.9.62.27 1.1.43 1.48.55.62.2 1.18.17 1.63.1.5-.07 1.53-.62 1.74-1.23.21-.6.21-1.12.15-1.23-.06-.11-.23-.18-.49-.31Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
type LinkProps = {
|
||||
href: string;
|
||||
display: string;
|
||||
ariaLabel: string;
|
||||
inline?: boolean; // true = footer row; false = larger contact-card row
|
||||
};
|
||||
|
||||
export function WhatsAppLink({ href, display, ariaLabel, inline }: LinkProps) {
|
||||
if (inline) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
className="flex items-center gap-3 text-sm text-mist hover:text-gold-light"
|
||||
>
|
||||
<WhatsAppIcon className="size-4" />
|
||||
WhatsApp · {display}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
className="group flex items-center gap-3 rounded-xl border border-bone/8 bg-bone/[0.03] p-3.5 transition-colors duration-500 hover:border-gold/30"
|
||||
>
|
||||
<span className="grid size-9 shrink-0 place-items-center rounded-full border border-gold/30 bg-gold/[0.08] text-gold-light shadow-[0_0_14px_-4px_rgba(212,164,55,0.6)]">
|
||||
<WhatsAppIcon className="size-4" />
|
||||
</span>
|
||||
<span className="text-bone group-hover:text-gold-light">
|
||||
WhatsApp · {display}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
type ButtonProps = {
|
||||
href: string;
|
||||
ariaLabel: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function WhatsAppIconButton({ href, ariaLabel, className }: ButtonProps) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
className={`group grid size-10 place-items-center rounded-full border border-bone/15 text-bone transition-all duration-500 hover:border-gold/40 hover:text-gold-light hover:shadow-[0_0_22px_-6px_rgba(212,164,55,0.6)] hover:scale-[1.04] ${className ?? ""}`}
|
||||
>
|
||||
<WhatsAppIcon className="size-[18px]" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user