forked from UKSOURCE/ipv6
init
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type IndustryOption = { value: string; label: string };
|
||||
|
||||
export default function RegistrationForm({
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
sections: {
|
||||
personal: { title: string; fullName: string; phone: string; email: string };
|
||||
professional: { title: string; company: string; jobTitle: string };
|
||||
industry: { title: string; label: string; placeholder: string; options: IndustryOption[] };
|
||||
additional: { title: string; label: string; placeholder: string };
|
||||
};
|
||||
submit: { label: string; disclaimer: string };
|
||||
};
|
||||
}) {
|
||||
const industries = useMemo(() => data.sections.industry.options, [data.sections.industry.options]);
|
||||
const [form, setForm] = useState({
|
||||
fullName: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
company: "",
|
||||
jobTitle: "",
|
||||
industry: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
className="space-y-[32px]"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
// frontend-only scope
|
||||
console.log("[registration]", form);
|
||||
alert("Saved locally (frontend-only).");
|
||||
}}
|
||||
>
|
||||
<div className="space-y-[16px]">
|
||||
<h2 className="font-[var(--font-display-lg)] text-[20px] text-primary border-l-2 border-primary pl-3">
|
||||
{data.sections.personal.title}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-[16px]">
|
||||
<div className="space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider">{data.sections.personal.fullName}</label>
|
||||
<input
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
|
||||
placeholder="John Doe"
|
||||
value={form.fullName}
|
||||
onChange={(e) => setForm((s) => ({ ...s, fullName: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider">{data.sections.personal.phone}</label>
|
||||
<input
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
|
||||
placeholder="+84 ..."
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm((s) => ({ ...s, phone: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider">{data.sections.personal.email}</label>
|
||||
<input
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
|
||||
placeholder="john.doe@company.com"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm((s) => ({ ...s, email: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-white/5" />
|
||||
|
||||
<div className="space-y-[16px]">
|
||||
<h2 className="font-[var(--font-display-lg)] text-[20px] text-primary border-l-2 border-primary pl-3">
|
||||
{data.sections.professional.title}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-[16px]">
|
||||
<div className="space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider">{data.sections.professional.company}</label>
|
||||
<input
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
|
||||
placeholder="Company"
|
||||
value={form.company}
|
||||
onChange={(e) => setForm((s) => ({ ...s, company: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider">{data.sections.professional.jobTitle}</label>
|
||||
<input
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
|
||||
placeholder="Network Architect"
|
||||
value={form.jobTitle}
|
||||
onChange={(e) => setForm((s) => ({ ...s, jobTitle: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-white/5" />
|
||||
|
||||
<div className="space-y-[16px]">
|
||||
<h2 className="font-[var(--font-display-lg)] text-[20px] text-primary border-l-2 border-primary pl-3">
|
||||
{data.sections.industry.title}
|
||||
</h2>
|
||||
<div className="space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider">{data.sections.industry.label}</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface appearance-none transition-all"
|
||||
value={form.industry}
|
||||
onChange={(e) => setForm((s) => ({ ...s, industry: e.target.value }))}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{data.sections.industry.placeholder}
|
||||
</option>
|
||||
{industries.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="material-symbols-outlined absolute right-4 top-1/2 -translate-y-1/2 text-outline pointer-events-none">
|
||||
expand_more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-white/5" />
|
||||
|
||||
<div className="space-y-[16px]">
|
||||
<h2 className="font-[var(--font-display-lg)] text-[20px] text-primary border-l-2 border-primary pl-3">
|
||||
{data.sections.additional.title}
|
||||
</h2>
|
||||
<div className="space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider">{data.sections.additional.label}</label>
|
||||
<textarea
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all resize-none"
|
||||
placeholder={data.sections.additional.placeholder}
|
||||
rows={4}
|
||||
value={form.notes}
|
||||
onChange={(e) => setForm((s) => ({ ...s, notes: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-[16px]">
|
||||
<button
|
||||
className="w-full gold-gradient-bg py-5 rounded-lg text-on-primary font-[var(--font-display-lg)] text-[18px] font-bold shadow-[0_10px_30px_rgba(197,160,89,0.15)] hover:shadow-[0_15px_40px_rgba(197,160,89,0.25)] hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] transition-all"
|
||||
type="submit"
|
||||
>
|
||||
{data.submit.label}
|
||||
</button>
|
||||
<p className="mt-[16px] text-center text-outline text-[11px] tracking-widest opacity-60 uppercase">
|
||||
{data.submit.disclaimer}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
export default function AgendaSection({
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
id: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
items: { order: string; time: string; title: string; description: string }[];
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<section className="py-16 lg:py-32 max-w-hd mx-auto px-[var(--spacing-gutter)]" id={data.id}>
|
||||
<div className="flex flex-col xl:flex-row justify-between xl:items-end mb-12 lg:mb-24 gap-8">
|
||||
<div className="max-w-3xl">
|
||||
<span className="font-[var(--font-label-caps)] text-xs lg:text-sm text-primary block mb-4 lg:mb-8 tracking-[0.25em]">
|
||||
{data.eyebrow}
|
||||
</span>
|
||||
<h2 className="font-[var(--font-display-lg)] text-on-surface uppercase tracking-tight text-[clamp(2rem,4vw+1rem,4.5rem)]">
|
||||
{data.title}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-on-surface-variant/60 text-base lg:text-[20px] max-w-md xl:text-right">
|
||||
{data.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 lg:gap-10">
|
||||
{data.items.map((item) => (
|
||||
<div
|
||||
key={item.order}
|
||||
className="glass-panel p-8 lg:p-12 rounded-3xl relative overflow-hidden group hover:border-primary/60 transition-colors"
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-6 lg:p-8">
|
||||
<span className="font-[var(--font-display-lg)] text-2xl lg:text-[40px] text-primary/20 group-hover:text-primary transition-colors">
|
||||
{item.order}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-6 lg:mb-10">
|
||||
<span className="font-[var(--font-display-lg)] text-2xl lg:text-[36px] text-primary">
|
||||
{item.time}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-[var(--font-display-lg)] text-2xl lg:text-[32px] text-on-surface mb-4 lg:mb-6">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed mb-6 lg:mb-8">
|
||||
{item.description}
|
||||
</p>
|
||||
<div className="w-full h-1 bg-primary/10 group-hover:bg-primary transition-colors duration-500" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HeroSection({
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
badgeIcon: string;
|
||||
badgeText: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
primaryCta: { label: string; href: string };
|
||||
secondaryCta: { label: string; href: string };
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<section className="relative min-h-[90vh] lg:min-h-screen flex items-center overflow-hidden bg-background hero-glow px-[var(--spacing-gutter)]">
|
||||
<div className="relative z-10 max-w-hd mx-auto w-full">
|
||||
<div className="max-w-7xl">
|
||||
<div className="inline-flex items-center gap-3 lg:gap-4 px-4 lg:px-8 py-2 lg:py-3 rounded-full bg-surface-container-high border border-primary/30 mb-8 lg:mb-16">
|
||||
<span className="material-symbols-outlined text-primary text-[20px] lg:text-[26px]">
|
||||
{data.badgeIcon}
|
||||
</span>
|
||||
<span className="font-[var(--font-label-caps)] text-xs lg:text-[16px] text-on-surface">
|
||||
{data.badgeText}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="font-[var(--font-display-lg)] text-on-surface mb-6 lg:mb-10 leading-[1.1] text-[clamp(2.25rem,5vw+1rem,6.875rem)]">
|
||||
{data.title}
|
||||
</h1>
|
||||
<p className="text-on-surface-variant text-lg lg:text-[28px] mb-10 lg:mb-20 max-w-4xl leading-relaxed opacity-90">
|
||||
{data.subtitle}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 lg:gap-10">
|
||||
<Link
|
||||
href={data.primaryCta.href}
|
||||
className="gold-gradient-bg text-on-primary px-8 lg:px-16 py-5 lg:py-7 font-[var(--font-display-lg)] text-sm lg:text-[20px] font-bold uppercase tracking-widest rounded-xl hover:shadow-[0_0_50px_rgba(197,160,89,0.4)] transition-all text-center"
|
||||
>
|
||||
{data.primaryCta.label}
|
||||
</Link>
|
||||
<Link
|
||||
href={data.secondaryCta.href}
|
||||
className="bg-surface-container-high border border-primary/40 text-on-surface px-8 lg:px-16 py-5 lg:py-7 font-[var(--font-display-lg)] text-sm lg:text-[20px] font-bold uppercase tracking-widest rounded-xl hover:bg-surface-container-highest transition-all text-center"
|
||||
>
|
||||
{data.secondaryCta.label}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export default function StatsSection({
|
||||
data,
|
||||
}: {
|
||||
data: { id: string; items: { value: string; label: string }[] };
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
className="py-16 lg:py-32 bg-surface-container-low border-y border-primary/10"
|
||||
id={data.id}
|
||||
>
|
||||
<div className="max-w-hd mx-auto px-[var(--spacing-gutter)]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 lg:gap-32 text-center">
|
||||
{data.items.map((x) => (
|
||||
<div key={x.label} className="space-y-3 lg:space-y-6">
|
||||
<div className="font-[var(--font-display-lg)] text-5xl lg:text-[96px] text-primary">
|
||||
{x.value}
|
||||
</div>
|
||||
<div className="font-[var(--font-label-caps)] text-sm lg:text-[18px] text-on-surface/80 tracking-widest uppercase">
|
||||
{x.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
export default function VisionSection({
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
paragraphs: string[];
|
||||
cards: { icon: string; title: string; description: string }[];
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<section className="py-16 lg:py-32 max-w-hd mx-auto px-[var(--spacing-gutter)]">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-32 items-start">
|
||||
<div className="lg:col-span-5">
|
||||
<span className="font-[var(--font-label-caps)] text-xs lg:text-sm text-primary mb-6 lg:mb-10 block tracking-[0.25em]">
|
||||
{data.eyebrow}
|
||||
</span>
|
||||
<h2 className="font-[var(--font-display-lg)] text-on-surface mb-8 lg:mb-12 leading-tight text-[clamp(2rem,4vw+1rem,4.5rem)]">
|
||||
{data.title}
|
||||
</h2>
|
||||
<div className="space-y-6 lg:space-y-10 text-on-surface-variant text-lg lg:text-[22px] leading-relaxed">
|
||||
{data.paragraphs.map((p) => (
|
||||
<p key={p}>{p}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 lg:gap-10">
|
||||
{data.cards.map((c) => (
|
||||
<div
|
||||
key={c.title}
|
||||
className="glass-panel p-8 lg:p-12 rounded-2xl group transition-all duration-500 hover:lg:-translate-y-4 hover:shadow-2xl hover:shadow-primary/5"
|
||||
>
|
||||
<span className="material-symbols-outlined text-primary text-[40px] lg:text-[56px] mb-8 lg:mb-12 block">
|
||||
{c.icon}
|
||||
</span>
|
||||
<h3 className="font-[var(--font-display-lg)] text-xl lg:text-[28px] text-on-surface mb-4 lg:mb-6">
|
||||
{c.title}
|
||||
</h3>
|
||||
<p className="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed">
|
||||
{c.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import Link from "next/link";
|
||||
import siteData from "@/app/data/site.json";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="w-full bg-surface-container-low border-t border-white/5 py-12 lg:py-20">
|
||||
<div className="flex flex-col xl:flex-row justify-between items-center px-[var(--spacing-gutter)] gap-12 w-full max-w-hd mx-auto">
|
||||
<div className="font-[var(--font-label-caps)] text-xl lg:text-[24px] text-primary tracking-[0.4em] font-bold uppercase">
|
||||
{siteData.brand.name}
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center gap-8 lg:gap-16">
|
||||
{siteData.footer.links.map((l) => (
|
||||
<Link
|
||||
key={l.label}
|
||||
className="text-on-surface-variant font-[var(--font-label-caps)] text-xs lg:text-sm hover:text-primary transition-colors"
|
||||
href={l.href}
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[14px] lg:text-[16px] text-on-surface-variant/40 text-center xl:text-right max-w-sm leading-relaxed uppercase tracking-widest">
|
||||
{siteData.footer.copyright}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import HeaderClient from "./HeaderClient";
|
||||
import siteData from "@/app/data/site.json";
|
||||
|
||||
export default function Header() {
|
||||
return <HeaderClient brand={siteData.brand} nav={siteData.nav} />;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
type NavItem = { label: string; href: string };
|
||||
|
||||
export default function HeaderClient({
|
||||
brand,
|
||||
nav,
|
||||
}: {
|
||||
brand: { name: string };
|
||||
nav: NavItem[];
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const isHome = pathname === "/";
|
||||
const navItems = useMemo(() => nav.filter((x) => x.href !== "/"), [nav]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mobileOpen) return;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [mobileOpen]);
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 w-full z-50 bg-surface/80 backdrop-blur-xl border-b border-white/5">
|
||||
<div className="flex justify-between items-center px-[var(--spacing-gutter)] py-4 lg:py-8 w-full max-w-hd mx-auto">
|
||||
<Link
|
||||
href="/"
|
||||
className="font-[var(--font-display-lg)] text-[20px] sm:text-[22px] lg:text-[36px] text-primary tracking-tighter uppercase font-bold"
|
||||
>
|
||||
{brand.name}
|
||||
</Link>
|
||||
|
||||
<nav className="hidden xl:flex items-center gap-10 2xl:gap-16">
|
||||
<Link
|
||||
className={`font-[var(--font-label-caps)] text-sm ${
|
||||
isHome ? "text-primary border-b-2 border-primary pb-1" : "text-on-surface-variant hover:text-primary"
|
||||
} transition-colors duration-300`}
|
||||
href="/"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
className="text-on-surface-variant font-[var(--font-label-caps)] text-sm hover:text-primary transition-colors duration-300"
|
||||
href={item.href}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-4 lg:gap-10">
|
||||
<span className="hidden sm:block text-on-surface-variant/60 font-[var(--font-label-caps)] text-xs">
|
||||
EN
|
||||
</span>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="hidden sm:inline-flex gold-gradient-bg text-on-primary px-6 lg:px-10 py-3 lg:py-4 font-[var(--font-body-base)] text-sm lg:text-[18px] font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="xl:hidden text-primary p-2"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={mobileOpen}
|
||||
onClick={() => setMobileOpen((v) => !v)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-3xl">{mobileOpen ? "close" : "menu"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`fixed inset-0 top-[72px] lg:top-[104px] bg-surface z-40 flex-col p-[var(--spacing-gutter)] gap-8 xl:hidden border-t border-white/5 ${
|
||||
mobileOpen ? "flex" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<Link className="text-2xl font-[var(--font-display-lg)] text-on-surface" href="/" onClick={() => setMobileOpen(false)}>
|
||||
Home
|
||||
</Link>
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
className="text-2xl font-[var(--font-display-lg)] text-on-surface"
|
||||
href={item.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
<div className="mt-auto flex flex-col gap-6">
|
||||
<div className="text-on-surface-variant/60 font-[var(--font-label-caps)] text-sm">Language: EN</div>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="gold-gradient-bg text-on-primary w-full py-5 font-[var(--font-body-base)] text-lg font-bold uppercase tracking-wider rounded-lg text-center"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
Register Now
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user