(Feat) Init WebSite
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState } from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { sendContact, type ContactState } from "@/app/actions/contact";
|
||||
|
||||
const initialState: ContactState = { status: "idle", message: "" };
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="px-8 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground hover:scale-[1.03] transition duration-200 disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
>
|
||||
{pending ? "SENDING…" : "SEND MESSAGE"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const inputBase =
|
||||
"w-full bg-surface border px-4 py-3 text-sm text-foreground placeholder:text-foreground-muted focus:outline-none transition-colors";
|
||||
|
||||
export function ContactForm() {
|
||||
const [state, formAction] = useActionState(sendContact, initialState);
|
||||
|
||||
if (state.status === "success") {
|
||||
return (
|
||||
<div className="border border-accent/40 bg-surface p-8 text-center">
|
||||
<p className="font-display text-xs tracking-[0.3em] text-accent mb-3">
|
||||
MESSAGE SENT
|
||||
</p>
|
||||
<p className="text-foreground-muted leading-relaxed">{state.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const err = state.fieldErrors ?? {};
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-5">
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
className="hidden"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contact-name"
|
||||
className="block font-display text-xs tracking-[0.3em] text-foreground-muted mb-2"
|
||||
>
|
||||
NAME
|
||||
</label>
|
||||
<input
|
||||
id="contact-name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Your name"
|
||||
className={`${inputBase} ${err.name ? "border-red-400" : "border-border focus:border-accent"}`}
|
||||
/>
|
||||
{err.name && (
|
||||
<p className="text-xs text-red-400 mt-1.5">{err.name}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contact-email"
|
||||
className="block font-display text-xs tracking-[0.3em] text-foreground-muted mb-2"
|
||||
>
|
||||
EMAIL
|
||||
</label>
|
||||
<input
|
||||
id="contact-email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
className={`${inputBase} ${err.email ? "border-red-400" : "border-border focus:border-accent"}`}
|
||||
/>
|
||||
{err.email && (
|
||||
<p className="text-xs text-red-400 mt-1.5">{err.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contact-subject"
|
||||
className="block font-display text-xs tracking-[0.3em] text-foreground-muted mb-2"
|
||||
>
|
||||
SUBJECT
|
||||
</label>
|
||||
<input
|
||||
id="contact-subject"
|
||||
name="subject"
|
||||
type="text"
|
||||
required
|
||||
placeholder="What is this about?"
|
||||
className={`${inputBase} ${err.subject ? "border-red-400" : "border-border focus:border-accent"}`}
|
||||
/>
|
||||
{err.subject && (
|
||||
<p className="text-xs text-red-400 mt-1.5">{err.subject}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contact-message"
|
||||
className="block font-display text-xs tracking-[0.3em] text-foreground-muted mb-2"
|
||||
>
|
||||
MESSAGE
|
||||
</label>
|
||||
<textarea
|
||||
id="contact-message"
|
||||
name="message"
|
||||
rows={6}
|
||||
required
|
||||
placeholder="Your message…"
|
||||
className={`${inputBase} resize-y ${err.message ? "border-red-400" : "border-border focus:border-accent"}`}
|
||||
/>
|
||||
{err.message && (
|
||||
<p className="text-xs text-red-400 mt-1.5">{err.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pt-2">
|
||||
<SubmitButton />
|
||||
{state.status === "error" && !Object.keys(err).length && (
|
||||
<p className="text-sm text-red-400">{state.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { SocialLinks } from "./SocialLinks";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border mt-24">
|
||||
<div className="max-w-7xl mx-auto px-6 lg:px-10 py-12 grid grid-cols-1 md:grid-cols-4 gap-10">
|
||||
<div className="md:col-span-2">
|
||||
<Link href="/" className="group inline-flex items-center gap-3">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt="Highland Games Studio"
|
||||
width={32}
|
||||
height={32}
|
||||
className="transition-transform duration-300 group-hover:scale-110 group-hover:-translate-y-0.5"
|
||||
/>
|
||||
<span className="font-display text-lg tracking-wider transition-colors duration-300 group-hover:text-accent">
|
||||
Highland Games Studio
|
||||
</span>
|
||||
</Link>
|
||||
<p className="mt-4 text-sm text-foreground-muted max-w-sm">
|
||||
Where summits become worlds. An indie studio crafting survival
|
||||
adventures.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-display text-sm tracking-widest text-foreground-muted mb-4">
|
||||
Explore
|
||||
</h4>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/studio"
|
||||
className="group relative inline-block text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Studio
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/games"
|
||||
className="group relative inline-block text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Games
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/devblog"
|
||||
className="group relative inline-block text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Devblog
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="group relative inline-block text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
Contact
|
||||
<span className="absolute left-0 -bottom-0.5 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-display text-sm tracking-widest text-foreground-muted mb-4">
|
||||
Follow
|
||||
</h4>
|
||||
<SocialLinks />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border">
|
||||
<div className="max-w-7xl mx-auto px-6 lg:px-10 py-6 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs text-foreground-muted">
|
||||
<p>© {new Date().getFullYear()} Highland Games Studio. All rights reserved.</p>
|
||||
<div className="flex items-center gap-5">
|
||||
<Link
|
||||
href="/legal/notice"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
Legal Notice
|
||||
</Link>
|
||||
<Link
|
||||
href="/legal/privacy"
|
||||
className="hover:text-accent transition-colors"
|
||||
>
|
||||
Privacy
|
||||
</Link>
|
||||
</div>
|
||||
<p className="font-display tracking-widest hidden lg:block">WHERE SUMMITS BECOME WORLDS</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
const links = [
|
||||
{ href: "/studio", label: "Studio" },
|
||||
{ href: "/games", label: "Games" },
|
||||
{ href: "/devblog", label: "Devblog" },
|
||||
{ href: "/contact", label: "Contact" },
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 20);
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = prev;
|
||||
};
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
initial={{ y: -20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled || open
|
||||
? "bg-background/80 backdrop-blur-md border-b border-border"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<nav className="max-w-7xl mx-auto flex items-center justify-between px-6 lg:px-10 h-16 lg:h-20">
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<Image
|
||||
src="/image/Studio/Logo Flat.png"
|
||||
alt="Highland Games Studio"
|
||||
width={36}
|
||||
height={36}
|
||||
className="transition-transform group-hover:-translate-y-0.5"
|
||||
priority
|
||||
/>
|
||||
<span className="font-display text-xl tracking-wider hidden sm:block">
|
||||
Highland Games
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<ul className="hidden md:flex items-center gap-1 sm:gap-2">
|
||||
{links.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="px-3 py-2 text-sm tracking-wide text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Mobile toggle */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={open ? "Close menu" : "Open menu"}
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-menu"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="md:hidden relative w-10 h-10 flex items-center justify-center text-foreground hover:text-accent transition-colors"
|
||||
>
|
||||
<span className="sr-only">Menu</span>
|
||||
<span className="relative w-6 h-4 flex flex-col justify-between">
|
||||
<motion.span
|
||||
animate={open ? { rotate: 45, y: 7 } : { rotate: 0, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="block h-px bg-current origin-center"
|
||||
/>
|
||||
<motion.span
|
||||
animate={open ? { opacity: 0 } : { opacity: 1 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="block h-px bg-current"
|
||||
/>
|
||||
<motion.span
|
||||
animate={open ? { rotate: -45, y: -7 } : { rotate: 0, y: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="block h-px bg-current origin-center"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
id="mobile-menu"
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.25, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="md:hidden border-t border-border bg-background/95 backdrop-blur-md"
|
||||
>
|
||||
<ul className="px-6 py-6 flex flex-col gap-1">
|
||||
{links.map((link, i) => (
|
||||
<motion.li
|
||||
key={link.href}
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.05 + i * 0.04, duration: 0.3 }}
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="block py-3 font-display text-2xl tracking-wider text-foreground hover:text-accent transition-colors"
|
||||
>
|
||||
{link.label.toUpperCase()}
|
||||
</Link>
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState } from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { subscribe, type SubscribeState } from "@/app/actions/subscribe";
|
||||
|
||||
const initialState: SubscribeState = { status: "idle", message: "" };
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="px-6 py-3 bg-accent text-background font-display tracking-widest text-sm hover:bg-foreground hover:scale-[1.03] transition duration-200 disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
>
|
||||
{pending ? "SENDING…" : "SUBSCRIBE"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
variant?: "default" | "compact";
|
||||
};
|
||||
|
||||
export function Newsletter({ variant = "default" }: Props) {
|
||||
const [state, formAction] = useActionState(subscribe, initialState);
|
||||
|
||||
const wrapperClass =
|
||||
variant === "compact"
|
||||
? "w-full max-w-lg"
|
||||
: "w-full max-w-lg mx-auto";
|
||||
|
||||
return (
|
||||
<form
|
||||
action={formAction}
|
||||
className={wrapperClass}
|
||||
aria-describedby="newsletter-status"
|
||||
>
|
||||
{/* honeypot for bots */}
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
className="hidden"
|
||||
aria-hidden
|
||||
/>
|
||||
<label htmlFor="newsletter-email" className="sr-only">
|
||||
Email address
|
||||
</label>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<input
|
||||
id="newsletter-email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="your@email.com"
|
||||
className="flex-1 min-w-0 bg-surface border border-border px-4 py-3 text-sm text-foreground placeholder:text-foreground-muted focus:outline-none focus:border-accent transition-colors"
|
||||
/>
|
||||
<SubmitButton />
|
||||
</div>
|
||||
|
||||
<p
|
||||
id="newsletter-status"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className={`text-xs tracking-widest mt-3 ${
|
||||
variant === "compact" ? "" : "text-center"
|
||||
} ${
|
||||
state.status === "success"
|
||||
? "text-accent"
|
||||
: state.status === "error"
|
||||
? "text-red-400"
|
||||
: "text-foreground-muted"
|
||||
}`}
|
||||
>
|
||||
{state.message ||
|
||||
"No spam. One devblog email per major update. Unsubscribe anytime."}
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Social = {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
const socials: Social[] = [
|
||||
{
|
||||
label: "Twitter / X",
|
||||
href: "#",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Discord",
|
||||
href: "#",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "YouTube",
|
||||
href: "#",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M23.498 6.186a3.02 3.02 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.02 3.02 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.02 3.02 0 0 0 2.122 2.136C4.495 20.455 12 20.455 12 20.455s7.505 0 9.377-.505a3.02 3.02 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Steam",
|
||||
href: "#",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.456-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export function SocialLinks() {
|
||||
return (
|
||||
<ul className="space-y-2 text-sm">
|
||||
{socials.map((s) => (
|
||||
<li key={s.label}>
|
||||
<motion.a
|
||||
href={s.href}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
animate="rest"
|
||||
className="group inline-flex items-center text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
<motion.span
|
||||
variants={{
|
||||
rest: { width: 0, opacity: 0, marginRight: 0 },
|
||||
hover: { width: 16, opacity: 1, marginRight: 10 },
|
||||
}}
|
||||
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="inline-flex items-center justify-center overflow-hidden text-accent"
|
||||
>
|
||||
<motion.span
|
||||
variants={{
|
||||
rest: { x: -8 },
|
||||
hover: { x: 0 },
|
||||
}}
|
||||
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="block w-4 h-4"
|
||||
>
|
||||
{s.icon}
|
||||
</motion.span>
|
||||
</motion.span>
|
||||
<span>{s.label}</span>
|
||||
</motion.a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user