feat: add Ina Trading portal flows and API integration

This commit is contained in:
Wira Basalamah
2026-04-24 05:19:05 +07:00
parent d98b4769f0
commit e08f2f9286
97 changed files with 18889 additions and 110 deletions

View File

@ -0,0 +1,128 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { Suspense } from "react";
import { LanguageToggle } from "@/components/language-toggle";
import { useLanguage } from "@/lib/i18n-context";
function AccountNotFoundContent() {
const searchParams = useSearchParams();
const email = searchParams.get("email") || "";
const { t } = useLanguage();
const a = t.auth.accountNotFound;
return (
<main className="flex flex-col md:flex-row min-h-screen md:h-screen md:overflow-hidden">
{/* Left Side */}
<section className="hidden md:flex md:w-1/2 lg:w-3/5 bg-secondary relative items-end p-12 lg:p-20 overflow-hidden">
<div className="absolute inset-0 z-0 bg-gradient-to-t from-secondary via-secondary/80 to-secondary/60" />
<div className="relative z-10 max-w-xl">
<div className="mb-8 flex items-center gap-3">
<span className="h-1 w-12 bg-primary" />
<span className="text-on-secondary font-headline font-bold uppercase tracking-widest text-xs">
{a.editorialIntelligence}
</span>
</div>
<h1 className="text-on-secondary font-headline text-5xl lg:text-7xl font-black leading-none tracking-tighter mb-6 whitespace-pre-line">
{a.tradePrecision}
</h1>
<p className="text-on-secondary/80 text-xl font-medium max-w-md leading-relaxed">
{a.tradeSubtitle}
</p>
</div>
</section>
{/* Right Side */}
<section className="flex-1 bg-surface-container-lowest flex flex-col p-8 md:p-12 lg:p-20 relative md:overflow-y-auto">
<div className="flex justify-between items-center mb-16">
<Image src="/ina_logo.png" alt="Ina Trading" width={160} height={48} priority />
<div className="flex items-center gap-3">
<LanguageToggle />
<Link
href="/login"
className="hidden sm:flex text-secondary font-semibold hover:text-primary transition-colors items-center gap-2"
>
<span className="material-symbols-outlined text-lg">help_outline</span>
{a.helpLink}
</Link>
</div>
</div>
<div className="flex-1 flex flex-col justify-center max-w-md w-full mx-auto">
<div className="mb-8 h-16 w-16 bg-error-container rounded-xl flex items-center justify-center">
<span className="material-symbols-outlined text-error text-3xl">person_off</span>
</div>
<div className="mb-10">
<h2 className="font-headline text-4xl font-extrabold tracking-tight text-on-surface mb-3">
{a.title}
</h2>
<div className="flex items-center gap-2 p-3 bg-surface-container-low rounded-lg border-l-4 border-tertiary">
<p className="text-on-surface-variant font-medium truncate">{email}</p>
<Link
href="/login"
className="text-tertiary font-bold hover:underline ml-auto flex items-center gap-1 shrink-0"
>
{a.change}
</Link>
</div>
</div>
<div className="space-y-6">
<Link
href={`/register?email=${encodeURIComponent(email)}`}
className="block w-full text-center signature-cta-gradient text-on-primary font-headline font-bold py-5 rounded-xl shadow-xl shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all text-xl uppercase tracking-wider"
>
{a.createAccount}
</Link>
<div className="relative py-4">
<div aria-hidden="true" className="absolute inset-0 flex items-center">
<div className="w-full border-t border-outline-variant/30" />
</div>
<div className="relative flex justify-center">
<span className="bg-surface-container-lowest px-4 text-sm font-medium text-outline">
{t.common.or}
</span>
</div>
</div>
<Link
href="/login"
className="flex items-center justify-center gap-3 w-full border-2 border-outline-variant/30 text-on-surface font-bold py-4 rounded-xl hover:bg-surface-container transition-colors group"
>
<span className="material-symbols-outlined text-secondary group-hover:text-primary transition-colors">
login
</span>
{a.loginOther}
</Link>
</div>
</div>
<footer className="mt-auto pt-12">
<div className="flex flex-col gap-6">
<div className="flex gap-4">
<a href="#" className="text-outline text-xs font-semibold hover:text-on-surface uppercase tracking-widest transition-colors">
{t.common.privacy}
</a>
<a href="#" className="text-outline text-xs font-semibold hover:text-on-surface uppercase tracking-widest transition-colors">
{t.common.terms}
</a>
</div>
<p className="text-[10px] leading-relaxed text-outline/60 max-w-sm">{a.disclaimer}</p>
</div>
</footer>
</section>
</main>
);
}
export default function AccountNotFoundPage() {
return (
<Suspense>
<AccountNotFoundContent />
</Suspense>
);
}

View File

@ -0,0 +1,152 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { FormEvent, useState } from "react";
import { LanguageToggle } from "@/components/language-toggle";
import { useLanguage } from "@/lib/i18n-context";
export default function ForgotPasswordPage() {
const { t } = useLanguage();
const f = t.auth.forgotPassword;
const [contact, setContact] = useState("");
const [submitted, setSubmitted] = useState(false);
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!contact.trim()) return;
setSubmitted(true);
}
return (
<>
<main className="min-h-screen flex flex-col md:flex-row bg-surface overflow-x-hidden">
<section className="hidden md:flex md:w-1/2 lg:w-3/5 relative overflow-hidden flex-col justify-end p-16 bg-primary">
<div className="absolute inset-0 z-0">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.22),transparent_30%),radial-gradient(circle_at_bottom_left,rgba(255,255,255,0.14),transparent_25%),linear-gradient(160deg,#b7131a_0%,#8d1116_55%,#57070a_100%)]" />
<div className="absolute inset-0 bg-gradient-to-t from-black/25 via-transparent to-transparent" />
</div>
<div className="relative z-10 space-y-6">
<div className="flex items-center gap-4">
<div className="h-1 w-12 bg-white rounded-full" />
<span className="font-headline font-extrabold text-white tracking-widest text-sm">
{f.securityFirst}
</span>
</div>
<h1 className="text-6xl lg:text-7xl font-headline font-black text-white tracking-tighter leading-none whitespace-pre-line">
{f.heroTitle}
</h1>
<p className="text-xl text-white/80 max-w-md font-light leading-relaxed">
{f.heroSubtitle}
</p>
</div>
<div className="relative z-10 mt-12 pt-8 border-t border-white/20 flex gap-12">
<div>
<span className="block text-white font-headline font-bold text-3xl">99.9%</span>
<span className="text-white/60 text-xs font-label uppercase tracking-widest">{f.uptime}</span>
</div>
<div>
<span className="block text-white font-headline font-bold text-3xl">24/7</span>
<span className="text-white/60 text-xs font-label uppercase tracking-widest">{f.fraudMonitoring}</span>
</div>
</div>
</section>
<section className="flex-1 bg-surface flex flex-col justify-center items-center p-8 md:p-16 lg:p-24 relative">
<div className="absolute top-12 left-8 md:static md:mb-20 md:self-start flex items-center gap-4">
<Image src="/ina_logo.png" alt="Ina Trading" width={160} height={48} priority />
</div>
<div className="absolute top-12 right-8">
<LanguageToggle />
</div>
<div className="w-full max-w-md space-y-10">
<header className="space-y-4">
<h2 className="text-4xl font-headline font-extrabold text-on-surface tracking-tight">
{f.title}
</h2>
<p className="text-on-surface-variant leading-relaxed text-lg">{f.subtitle}</p>
</header>
<form className="space-y-8" onSubmit={handleSubmit}>
{submitted && (
<div className="rounded-xl border border-tertiary/10 bg-tertiary/5 p-4 text-sm text-on-tertiary-fixed-variant">
{f.apiNotReady} <strong>{contact}</strong>.
</div>
)}
<div className="relative group">
<label
className="block text-xs font-headline font-bold uppercase tracking-wider text-outline mb-2 group-focus-within:text-primary transition-colors"
htmlFor="recovery-contact"
>
{f.emailOrPhone}
</label>
<div className="flex items-center rounded-xl border border-outline-variant/60 bg-surface-container-low px-4 py-4 transition-all group-focus-within:border-primary group-focus-within:bg-surface-container-lowest">
<span className="material-symbols-outlined text-outline group-focus-within:text-primary mr-3">
alternate_email
</span>
<input
id="recovery-contact"
name="recovery-contact"
type="text"
value={contact}
onChange={(e) => setContact(e.target.value)}
placeholder="name@company.com"
required
className="w-full bg-transparent border-none p-0 text-lg font-medium text-on-surface placeholder:text-outline/50 focus:ring-0"
/>
</div>
<p className="mt-3 text-sm text-on-surface-variant/70 italic">
{/* privacy note kept short */}
</p>
</div>
<div className="pt-4">
<button
type="submit"
className="w-full py-5 px-8 rounded-xl editorial-gradient text-on-primary font-headline font-bold text-xl tracking-tight magazine-shadow active:scale-[0.98] transition-all hover:brightness-110 flex items-center justify-center gap-3"
>
{f.submit}
<span className="material-symbols-outlined">arrow_forward</span>
</button>
</div>
</form>
<footer className="pt-8 border-t border-outline-variant/30 flex flex-col gap-4">
<Link
href="/login"
className="flex items-center gap-2 text-secondary font-semibold hover:text-primary transition-colors group"
>
<span className="material-symbols-outlined text-xl group-hover:-translate-x-1 transition-transform">
chevron_left
</span>
{f.backToLogin}
</Link>
<div className="flex items-center gap-2 mt-4 p-4 rounded-xl bg-tertiary/5 border border-tertiary/10">
<span className="material-symbols-outlined text-tertiary">contact_support</span>
<p className="text-sm text-on-surface-variant">
{f.havingTrouble}{" "}
<a href="#" className="text-tertiary font-bold hover:underline">{f.supportLink}</a>
</p>
</div>
</footer>
</div>
<div className="absolute bottom-0 right-0 w-64 h-64 bg-primary-container/5 rounded-full blur-3xl -z-10 pointer-events-none" />
</section>
</main>
<div className="fixed bottom-8 right-8 z-50">
<button className="bg-surface-container-lowest text-secondary p-4 rounded-full magazine-shadow border border-outline-variant/20 hover:bg-secondary hover:text-on-secondary transition-all active:scale-95 flex items-center justify-center">
<span className="material-symbols-outlined">help_center</span>
</button>
</div>
</>
);
}

View File

@ -0,0 +1,231 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { LanguageToggle } from "@/components/language-toggle";
import { useLanguage } from "@/lib/i18n-context";
const authFieldWrapperClass =
"relative rounded-xl border border-outline-variant/60 bg-surface-container-high px-0 transition-all duration-300 focus-within:border-primary focus-within:bg-surface-container-lowest";
export default function LoginPage() {
const router = useRouter();
const { t } = useLanguage();
const l = t.auth.login;
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [remember, setRemember] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (res.status === 404 && data.error === "ACCOUNT_NOT_FOUND") {
router.push(`/account-not-found?email=${encodeURIComponent(email)}`);
return;
}
if (!res.ok) {
setError(data.error || l.errorGeneric);
return;
}
if (remember) {
localStorage.setItem("token", data.token);
localStorage.setItem("role", data.role);
} else {
sessionStorage.setItem("token", data.token);
sessionStorage.setItem("role", data.role);
}
if (data.role === "admin") {
router.push("/admin/dashboard");
return;
}
if (data.role === "seller") {
// Check seller profile completeness
try {
const profileRes = await fetch("/api/seller/profile", {
headers: { "x-auth-token": data.token },
});
const profileData = await profileRes.json();
const profile = profileData?.data || profileData;
const isIncomplete =
!profile?.storeName ||
!profile?.biography ||
!profile?.sellerImageUrl;
if (isIncomplete || data.onboardingRequired) {
router.push("/onboarding/business");
return;
}
} catch {
// If profile check fails, still proceed to dashboard
}
router.push("/dashboard");
return;
}
router.push("/dashboard");
} catch {
setError(l.errorConnection);
} finally {
setLoading(false);
}
}
return (
<main className="flex min-h-screen md:h-screen md:overflow-hidden">
{/* Left Side */}
<section className="hidden lg:flex lg:w-1/2 bg-primary-editorial relative overflow-hidden flex-col justify-end p-16">
<div className="absolute inset-0 z-0">
<div className="absolute -top-24 -left-24 w-96 h-96 bg-white/10 rounded-full blur-3xl" />
<div className="absolute -bottom-48 -right-48 w-[40rem] h-[40rem] bg-black/10 rounded-full blur-3xl" />
</div>
<div className="relative z-10">
<h1 className="text-7xl font-extrabold text-white font-headline leading-[0.95] tracking-tighter mb-8 max-w-lg whitespace-pre-line">
{l.heroTitle}
</h1>
<p className="text-white/80 text-xl font-medium max-w-md leading-relaxed">
{l.heroSubtitle}
</p>
</div>
</section>
{/* Right Side */}
<section className="w-full lg:w-1/2 flex items-center justify-center p-8 lg:p-24 bg-surface-container-lowest md:overflow-y-auto">
<div className="w-full max-w-md">
{/* Mobile Logo + Language Toggle */}
<div className="lg:hidden mb-12 flex justify-between items-center">
<Image src="/ina_logo.png" alt="Ina Trading" width={160} height={48} priority />
<LanguageToggle />
</div>
{/* Desktop language toggle */}
<div className="hidden lg:flex items-start justify-between mb-10">
<Image src="/ina_logo.png" alt="Ina Trading" width={180} height={54} priority />
<LanguageToggle />
</div>
<header className="mb-12">
<h2 className="text-4xl font-extrabold text-on-surface font-headline tracking-tight mb-2">
{l.title}
</h2>
<p className="text-on-surface-variant font-medium">{l.subtitle}</p>
</header>
<form className="space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="p-3 bg-error-container text-on-error-container rounded-lg text-sm font-medium">
{error}
</div>
)}
<div className="space-y-1">
<label className="text-xs font-bold uppercase tracking-wider text-outline mb-2 block">
{l.emailOrPhone}
</label>
<div className={authFieldWrapperClass}>
<span className="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-outline-variant">
person
</span>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="name@business.com"
required
className="w-full rounded-xl border-none bg-transparent py-4 pl-12 pr-4 font-medium placeholder:text-outline-variant focus:outline-none"
/>
</div>
</div>
<div className="space-y-1">
<div className="flex justify-between items-center mb-2">
<label className="text-xs font-bold uppercase tracking-wider text-outline block">
Password
</label>
<Link href="/forgot-password" className="text-xs font-bold text-tertiary hover:underline">
{l.forgotPassword}
</Link>
</div>
<div className={authFieldWrapperClass}>
<span className="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-outline-variant">
lock
</span>
<input
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full rounded-xl border-none bg-transparent py-4 pl-12 pr-12 font-medium placeholder:text-outline-variant focus:outline-none"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-outline-variant hover:text-on-surface"
>
<span className="material-symbols-outlined">
{showPassword ? "visibility_off" : "visibility"}
</span>
</button>
</div>
</div>
<div className="flex items-center gap-3 py-2">
<input
type="checkbox"
id="remember"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary"
/>
<label htmlFor="remember" className="text-sm font-medium text-on-surface-variant">
{l.rememberDevice}
</label>
</div>
<div className="pt-4 space-y-4">
<button
type="submit"
disabled={loading}
className="w-full bg-primary text-white font-bold py-5 rounded-xl shadow-lg shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all duration-200 text-lg disabled:opacity-70 disabled:scale-100"
>
{loading ? l.submitting : l.submit}
</button>
</div>
</form>
<footer className="mt-12 text-center">
<p className="text-on-surface-variant font-medium">
{l.noAccount}{" "}
<Link href="/register" className="text-primary font-bold hover:underline ml-1">
{l.registerFree}
</Link>
</p>
</footer>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,103 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { LanguageToggle } from "@/components/language-toggle";
import { useLanguage } from "@/lib/i18n-context";
export default function CompleteRegisterPage() {
const router = useRouter();
const { t } = useLanguage();
const c = t.auth.complete;
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
const raw = sessionStorage.getItem("registerData");
const otpVerified = sessionStorage.getItem("otpVerified");
if (!raw || otpVerified !== "true") {
router.replace("/register");
}
}, [router]);
async function handleComplete() {
const raw = sessionStorage.getItem("registerData");
if (!raw) {
setError(c.noData);
return;
}
setLoading(true);
setError("");
try {
const registerData = JSON.parse(raw);
const res = await fetch("/api/auth/finalize-register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: registerData.role, registerData }),
});
const data = await res.json();
if (!res.ok) {
setError(data?.error || c.registerFail);
return;
}
sessionStorage.removeItem("registerData");
sessionStorage.removeItem("otpVerified");
sessionStorage.removeItem("otpVerifiedEmail");
router.push("/login");
} catch {
setError(t.common.connectionError);
} finally {
setLoading(false);
}
}
return (
<main className="flex min-h-screen items-center justify-center bg-surface p-6">
<div className="w-full max-w-xl rounded-2xl bg-surface-container-lowest p-10 shadow-lg">
<div className="flex justify-end mb-4">
<LanguageToggle />
</div>
<div className="mb-8">
<div className="mb-4 inline-flex items-center rounded-full bg-primary/10 px-4 py-1.5 text-xs font-black uppercase tracking-widest text-primary">
{c.finalStep}
</div>
<h1 className="font-headline text-4xl font-black tracking-tight text-on-surface">
{c.title}
</h1>
<p className="mt-3 text-on-surface-variant">{c.subtitle}</p>
</div>
{error ? (
<div className="mb-6 rounded-xl bg-error-container p-4 text-sm font-medium text-on-error-container">
{error}
</div>
) : null}
<div className="space-y-4">
<button
type="button"
onClick={handleComplete}
disabled={loading}
className="w-full rounded-xl bg-primary px-6 py-4 text-lg font-bold text-white shadow-lg shadow-primary/20 transition-all hover:opacity-90 disabled:opacity-60"
>
{loading ? c.submitting : c.submit}
</button>
<Link
href="/register"
className="block text-center text-sm font-semibold text-on-surface-variant hover:text-primary"
>
{c.backToRegister}
</Link>
</div>
</div>
</main>
);
}

View File

@ -0,0 +1,337 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useState, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { LanguageToggle } from "@/components/language-toggle";
import { useLanguage } from "@/lib/i18n-context";
type Role = "seller" | "buyer";
const authInputClass =
"w-full rounded-xl border border-outline-variant/60 bg-surface-container-highest px-4 py-3 text-on-surface placeholder:text-surface-dim focus:border-primary focus:bg-surface-container-lowest focus:outline-none transition-all duration-300";
const authInputWithIconClass =
"w-full rounded-xl border border-outline-variant/60 bg-surface-container-highest px-4 py-3 pr-10 text-on-surface placeholder:text-surface-dim focus:border-primary focus:bg-surface-container-lowest focus:outline-none transition-all duration-300";
function RegisterContent() {
const router = useRouter();
const searchParams = useSearchParams();
const emailFromQuery = searchParams.get("email") || "";
const { t } = useLanguage();
const r = t.auth.register;
const [role, setRole] = useState<Role>("seller");
const [email, setEmail] = useState(emailFromQuery);
const [name, setName] = useState("");
const [mobile, setMobile] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
if (password !== confirmPassword) {
setError(r.passwordMismatch);
return;
}
if (password.length < 6) {
setError(r.passwordTooShort);
return;
}
setLoading(true);
try {
const res = await fetch("/api/auth/send-otp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || r.otpError);
return;
}
sessionStorage.setItem(
"registerData",
JSON.stringify({ name, mobile, password, role, email })
);
router.push(`/register/verify?email=${encodeURIComponent(email)}`);
} catch {
setError(t.common.connectionError);
} finally {
setLoading(false);
}
}
return (
<main className="flex flex-col md:flex-row min-h-screen md:h-screen md:overflow-hidden">
{/* Left Side */}
<section className="relative w-full md:w-5/12 lg:w-1/2 min-h-[280px] md:h-full flex items-end p-8 lg:p-16 overflow-hidden bg-on-surface">
<div className="absolute inset-0 z-0">
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 via-primary/60 to-primary-container/40" />
<div className="absolute inset-0 bg-gradient-to-t from-on-surface via-transparent to-transparent opacity-80" />
</div>
<div className="relative z-10 max-w-lg">
<h1 className="text-white font-headline text-5xl lg:text-7xl font-extrabold tracking-tight leading-[1.1] mb-6 whitespace-pre-line">
{r.joinNetwork}
</h1>
<p className="text-surface-container-low text-lg lg:text-xl font-medium leading-relaxed opacity-90 max-w-md">
{r.joinSubtitle}
</p>
<div className="mt-12 pt-12 border-t border-white/20 flex gap-12">
<div>
<div className="text-white font-headline text-4xl font-bold">98%</div>
<div className="text-white/60 text-sm uppercase tracking-widest font-bold mt-1">{r.successRate}</div>
</div>
<div>
<div className="text-white font-headline text-4xl font-bold">24/7</div>
<div className="text-white/60 text-sm uppercase tracking-widest font-bold mt-1">{r.marketPulse}</div>
</div>
</div>
</div>
</section>
{/* Right Side */}
<section className="w-full md:w-7/12 lg:w-1/2 bg-surface-container-lowest flex flex-col overflow-y-auto">
<div className="w-full h-1.5 bg-primary" />
<div className="w-full max-w-2xl mx-auto p-8 lg:p-10 xl:p-12 flex-1 flex flex-col">
<div className="mb-8">
<div className="mb-8 flex items-start justify-between gap-4">
<Image src="/ina_logo.png" alt="Ina Trading" width={180} height={54} priority />
<LanguageToggle />
</div>
<h2 className="font-headline text-3xl font-extrabold tracking-tight text-on-surface mb-2 text-center">
{r.title}
</h2>
<p className="text-on-surface-variant font-medium text-center">{r.subtitle}</p>
</div>
<form className="space-y-5" onSubmit={handleSubmit}>
{error && (
<div className="p-3 bg-error-container text-on-error-container rounded-lg text-sm font-medium">
{error}
</div>
)}
{/* Role Selector */}
<div className="space-y-1">
<label className="block text-xs font-bold uppercase tracking-widest text-outline">
{r.registerAs}
</label>
<div className="grid grid-cols-2 gap-3 mt-2">
<button
type="button"
onClick={() => setRole("seller")}
className={`flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all ${
role === "seller"
? "border-primary bg-primary/5 text-primary"
: "border-outline-variant/30 text-on-surface-variant hover:border-outline/40"
}`}
>
<span
className="material-symbols-outlined text-2xl"
style={{ fontVariationSettings: role === "seller" ? "'FILL' 1" : "'FILL' 0" }}
>
storefront
</span>
<span className="text-sm font-bold">Seller</span>
<span className="text-[10px] text-center opacity-70 leading-tight">
{r.sellerDesc}
</span>
</button>
<button
type="button"
onClick={() => setRole("buyer")}
className={`flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all ${
role === "buyer"
? "border-primary bg-primary/5 text-primary"
: "border-outline-variant/30 text-on-surface-variant hover:border-outline/40"
}`}
>
<span
className="material-symbols-outlined text-2xl"
style={{ fontVariationSettings: role === "buyer" ? "'FILL' 1" : "'FILL' 0" }}
>
shopping_bag
</span>
<span className="text-sm font-bold">Buyer</span>
<span className="text-[10px] text-center opacity-70 leading-tight">
{r.buyerDesc}
</span>
</button>
</div>
</div>
{/* Email */}
<div className="space-y-1">
<label className="block text-xs font-bold uppercase tracking-widest text-outline" htmlFor="email">
{r.email}
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="name@business.com"
required
readOnly={!!emailFromQuery}
className={`${authInputClass} ${
emailFromQuery ? "bg-surface-container text-on-surface-variant cursor-not-allowed" : ""
}`}
/>
{emailFromQuery && (
<p className="text-[10px] text-on-surface-variant mt-1">{r.emailFromPrevious}</p>
)}
</div>
{/* Name */}
<div className="space-y-1">
<label className="block text-xs font-bold uppercase tracking-widest text-outline" htmlFor="name">
{r.fullName}
</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
required
className={authInputClass}
/>
</div>
{/* Mobile */}
<div className="space-y-1">
<label className="block text-xs font-bold uppercase tracking-widest text-outline" htmlFor="mobile">
{r.phone}
</label>
<input
id="mobile"
type="tel"
value={mobile}
onChange={(e) => setMobile(e.target.value)}
placeholder="+62 812 3456 789"
required
className={authInputClass}
/>
</div>
{/* Password */}
<div className="space-y-1">
<label className="block text-xs font-bold uppercase tracking-widest text-outline" htmlFor="password">
{r.password}
</label>
<div className="relative">
<input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={r.passwordPlaceholder}
required
className={authInputWithIconClass}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-0 top-3 text-outline-variant hover:text-on-surface"
>
<span className="material-symbols-outlined text-xl">
{showPassword ? "visibility_off" : "visibility"}
</span>
</button>
</div>
</div>
{/* Confirm Password */}
<div className="space-y-1">
<label className="block text-xs font-bold uppercase tracking-widest text-outline" htmlFor="confirm-password">
{r.confirmPassword}
</label>
<div className="relative">
<input
id="confirm-password"
type={showConfirm ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder={r.confirmPasswordPlaceholder}
required
className={authInputWithIconClass}
/>
<button
type="button"
onClick={() => setShowConfirm(!showConfirm)}
className="absolute right-0 top-3 text-outline-variant hover:text-on-surface"
>
<span className="material-symbols-outlined text-xl">
{showConfirm ? "visibility_off" : "visibility"}
</span>
</button>
</div>
</div>
<div className="pt-4 space-y-6">
<button
type="submit"
disabled={loading}
className="signature-cta-gradient w-full py-4 text-on-primary font-headline font-bold text-lg rounded-xl magazine-shadow hover:scale-[1.02] active:scale-95 transition-all duration-200 disabled:opacity-70 disabled:scale-100"
>
{loading ? r.submitting : r.submit}
</button>
<div className="text-center">
<p className="text-on-surface-variant font-medium">
{r.haveAccount}{" "}
<Link href="/login" className="text-tertiary font-bold hover:text-primary transition-colors ml-1">
{r.signIn}
</Link>
</p>
</div>
</div>
</form>
{/* Step Progress */}
<div className="mt-10 flex justify-between items-center bg-surface-container-low p-4 rounded-xl">
<div className="flex gap-2">
<div className="w-8 h-1.5 rounded-full bg-primary" />
<div className="w-8 h-1.5 rounded-full bg-outline-variant" />
<div className="w-8 h-1.5 rounded-full bg-outline-variant" />
</div>
<span className="text-[10px] font-black uppercase tracking-widest text-outline">
{r.stepOf}
</span>
</div>
</div>
<div className="mt-8 pb-10 text-center">
<p className="text-[10px] text-outline leading-tight uppercase tracking-tighter">
{r.termsAgreement}{" "}
<a href="#" className="underline">{t.common.terms}</a> {r.and}{" "}
<a href="#" className="underline">{t.common.privacy}</a> {r.inaTrading}
</p>
</div>
</section>
</main>
);
}
export default function RegisterPage() {
return (
<Suspense>
<RegisterContent />
</Suspense>
);
}

View File

@ -0,0 +1,321 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useState, useRef, Suspense, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { LanguageToggle } from "@/components/language-toggle";
import { useLanguage } from "@/lib/i18n-context";
function VerifyContent() {
const router = useRouter();
const searchParams = useSearchParams();
const email = searchParams.get("email") || "";
const { t } = useLanguage();
const v = t.auth.verify;
const [otp, setOtp] = useState(["", "", "", "", "", ""]);
const [loading, setLoading] = useState(false);
const [resending, setResending] = useState(false);
const [error, setError] = useState("");
const [errorStep, setErrorStep] = useState("");
const [success, setSuccess] = useState("");
const [countdown, setCountdown] = useState(0);
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
useEffect(() => {
inputRefs.current[0]?.focus();
}, []);
useEffect(() => {
if (countdown <= 0) return;
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
return () => clearTimeout(timer);
}, [countdown]);
function handleOtpChange(index: number, value: string) {
if (!/^\d*$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value.slice(-1);
setOtp(newOtp);
if (value && index < 5) {
inputRefs.current[index + 1]?.focus();
}
}
function handleKeyDown(index: number, e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Backspace" && !otp[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
}
function handlePaste(e: React.ClipboardEvent) {
e.preventDefault();
const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, 6);
if (!pasted) return;
const newOtp = [...otp];
pasted.split("").forEach((char, i) => { if (i < 6) newOtp[i] = char; });
setOtp(newOtp);
const nextIndex = Math.min(pasted.length, 5);
inputRefs.current[nextIndex]?.focus();
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setErrorStep("");
const otpCode = otp.join("");
if (otpCode.length < 6) {
setError(v.otpTooShort);
return;
}
const rawData = sessionStorage.getItem("registerData");
if (!rawData) {
setError(v.noData);
return;
}
const { role } = JSON.parse(rawData);
setLoading(true);
try {
const res = await fetch("/api/auth/verify-otp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, otp: otpCode }),
});
const data = await res.json();
if (!res.ok) {
const backendMessage = data?.error || data?.responseDesc || data?.message || v.verifyFail;
setError(backendMessage);
setErrorStep(data?.step || "");
return;
}
const parsedData = JSON.parse(rawData);
if (role === "seller") {
const registerData = { ...parsedData, email, otpVerified: true };
const registerRes = await fetch("/api/auth/finalize-register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: "seller", registerData }),
});
const registerResult = await registerRes.json();
if (!registerRes.ok) {
setError(registerResult?.error || v.registerFail);
setErrorStep(registerResult?.step || "");
return;
}
if (registerResult?.token) {
sessionStorage.setItem("token", registerResult.token);
sessionStorage.setItem("role", "seller");
}
sessionStorage.removeItem("registerData");
sessionStorage.removeItem("otpVerified");
sessionStorage.removeItem("otpVerifiedEmail");
setSuccess(v.successSeller);
setTimeout(() => { router.push("/onboarding/business"); }, 1000);
return;
}
sessionStorage.setItem("registerData", JSON.stringify({ ...parsedData, email, otpVerified: true }));
sessionStorage.setItem("otpVerified", "true");
sessionStorage.setItem("otpVerifiedEmail", email);
setSuccess(v.successBuyer);
setTimeout(() => { router.push("/register/complete"); }, 1000);
} catch {
setError(t.common.connectionError);
} finally {
setLoading(false);
}
}
async function handleResend() {
if (countdown > 0) return;
setError("");
setErrorStep("");
setResending(true);
try {
const res = await fetch("/api/auth/send-otp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
if (res.ok) {
setOtp(["", "", "", "", "", ""]);
inputRefs.current[0]?.focus();
setCountdown(60);
} else {
const data = await res.json();
setError(data.error || t.common.connectionError);
}
} catch {
setError(t.common.connectionError);
} finally {
setResending(false);
}
}
return (
<main className="flex flex-col md:flex-row min-h-screen md:h-screen md:overflow-hidden">
{/* Left Side */}
<section className="hidden md:flex md:w-5/12 lg:w-1/2 relative bg-primary overflow-hidden items-center justify-center p-12">
<div className="absolute inset-0 z-0 opacity-20 bg-gradient-to-br from-primary-container to-primary" />
<div className="absolute inset-0 bg-gradient-to-br from-primary via-primary to-primary-container opacity-90 z-10" />
<div className="relative z-20 max-w-lg">
<h1 className="text-5xl lg:text-7xl font-headline font-extrabold text-on-primary leading-none tracking-tight mb-8 whitespace-pre-line">
{v.secureYourFuture}
</h1>
<div className="h-1 w-24 bg-on-primary mb-8" />
<p className="text-on-primary/80 text-xl font-medium leading-relaxed max-w-sm">
{v.verifyIdentity}
</p>
<div className="mt-16 grid grid-cols-2 gap-8">
<div>
<p className="text-on-primary font-bold text-3xl font-headline tracking-tighter">99.9%</p>
<p className="text-on-primary/60 text-sm">{v.transactionSecurity}</p>
</div>
<div>
<p className="text-on-primary font-bold text-3xl font-headline tracking-tighter">256-bit</p>
<p className="text-on-primary/60 text-sm">{v.bankLevelEncryption}</p>
</div>
</div>
</div>
</section>
{/* Right Side */}
<section className="flex-1 bg-surface flex items-center justify-center p-6 md:p-12 lg:p-24 relative md:overflow-y-auto">
{/* Logo */}
<div className="absolute top-8 left-8">
<Image src="/ina_logo.png" alt="Ina Trading" width={160} height={48} priority />
</div>
<div className="w-full max-w-md">
<header className="mb-12">
<div className="inline-flex items-center justify-center w-12 h-12 bg-tertiary/10 rounded-xl mb-6">
<span className="material-symbols-outlined text-tertiary" style={{ fontVariationSettings: "'FILL' 1" }}>
mark_email_read
</span>
</div>
<h2 className="text-4xl font-headline font-extrabold text-on-surface tracking-tight mb-4">
{v.title}
</h2>
<p className="text-on-surface-variant leading-relaxed">
{v.subtitle}{" "}
<span className="font-semibold text-on-surface">{email}</span>
{v.subtitleSuffix}
</p>
</header>
<form className="space-y-8" onSubmit={handleSubmit}>
{error && (
<div className="p-3 bg-error-container text-on-error-container rounded-lg text-sm font-medium">
<div>{error}</div>
{errorStep ? (
<div className="mt-1 text-[11px] font-semibold uppercase tracking-wider opacity-70">
Step: {errorStep}
</div>
) : null}
</div>
)}
{success && (
<div className="p-3 bg-tertiary/10 text-tertiary rounded-lg text-sm font-medium">
{success}
</div>
)}
{/* OTP Grid */}
<div className="flex gap-3 justify-between" onPaste={handlePaste}>
{otp.map((digit, index) => (
<input
key={index}
ref={(el) => { inputRefs.current[index] = el; }}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleOtpChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
placeholder="•"
className="otp-input w-14 h-16 text-center text-2xl font-bold rounded-xl border-none bg-surface-container-highest text-on-surface transition-all focus:bg-surface-container-lowest focus:ring-0"
/>
))}
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-on-surface-variant">{v.noCode}</span>
<button
type="button"
onClick={handleResend}
disabled={countdown > 0 || resending}
className="text-tertiary font-bold hover:underline transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
>
{resending
? v.resending
: countdown > 0
? `${v.resendCountdown} (${countdown}s)`
: v.resend}
</button>
</div>
<div className="pt-4">
<button
type="submit"
disabled={loading || otp.join("").length < 6}
className="w-full bg-gradient-to-br from-primary to-primary-container text-on-primary font-headline font-bold py-5 px-8 rounded-xl shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.98] transition-all flex items-center justify-center gap-3 disabled:opacity-60 disabled:scale-100"
>
<span>{loading ? v.submitting : v.submit}</span>
{!loading && <span className="material-symbols-outlined text-lg">arrow_forward</span>}
</button>
</div>
</form>
<footer className="mt-16 pt-8 border-t border-outline-variant/20">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-outline text-sm">shield</span>
<span className="text-xs text-on-surface-variant uppercase tracking-widest font-semibold">
{v.securityTitle}
</span>
</div>
<p className="text-xs text-on-surface-variant/70 leading-relaxed">{v.securityDesc}</p>
</div>
</footer>
</div>
<Link
href="/register"
className="absolute top-8 right-8 flex items-center gap-2 text-on-surface-variant hover:text-primary transition-colors font-medium"
>
<span className="material-symbols-outlined text-lg">close</span>
<span className="hidden md:inline font-label">{t.common.cancel}</span>
</Link>
<div className="absolute top-8 right-24">
<LanguageToggle />
</div>
</section>
</main>
);
}
export default function VerifyPage() {
return (
<Suspense>
<VerifyContent />
</Suspense>
);
}

View File

@ -0,0 +1,261 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { FormEvent, useState } from "react";
import { LanguageToggle } from "@/components/language-toggle";
const passwordFieldWrapperClass =
"relative rounded-xl border border-outline-variant/60 bg-surface-container-highest transition-all duration-300 focus-within:border-primary focus-within:bg-surface-container-lowest";
function getPasswordChecks(password: string) {
return {
minLength: password.length >= 12,
uppercase: /[A-Z]/.test(password),
number: /\d/.test(password),
special: /[^A-Za-z0-9]/.test(password),
};
}
export default function ResetPasswordPage() {
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [error, setError] = useState("");
const [submitted, setSubmitted] = useState(false);
const checks = getPasswordChecks(newPassword);
const isStrongPassword = Object.values(checks).every(Boolean);
const passwordsMatch = newPassword === confirmPassword;
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
setSubmitted(false);
if (!isStrongPassword) {
setError("Password baru belum memenuhi requirement minimum.");
return;
}
if (!passwordsMatch) {
setError("Konfirmasi password tidak sama.");
return;
}
// API reset password belum tersedia.
setSubmitted(true);
}
return (
<>
<div className="flex min-h-screen w-full bg-background">
<div className="hidden lg:flex lg:w-7/12 relative overflow-hidden editorial-gradient flex-col justify-between p-16">
<div className="z-10">
<Image
src="/ina_logo.png"
alt="Ina Trading"
width={180}
height={54}
priority
/>
</div>
<div className="z-10 max-w-xl">
<h1 className="font-headline text-white text-7xl font-extrabold leading-[1.05] tracking-tight mb-8">
Secure. <br />
Refined. <br />
Absolute.
</h1>
<p className="text-white/80 text-xl font-medium leading-relaxed">
Protecting your financial ecosystem with world-class encryption
and institutional-grade security protocols.
</p>
</div>
<div className="z-10 grid grid-cols-2 gap-8 border-t border-white/20 pt-12">
<div>
<div className="text-white/60 text-xs font-bold uppercase tracking-widest mb-2">
Security Status
</div>
<div className="text-white text-3xl font-bold font-headline">
99.9% Up
</div>
</div>
<div>
<div className="text-white/60 text-xs font-bold uppercase tracking-widest mb-2">
Data Protection
</div>
<div className="text-white text-3xl font-bold font-headline">
AES-256
</div>
</div>
</div>
<div className="absolute inset-0 z-0">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.15),transparent_28%),linear-gradient(140deg,rgba(0,0,0,0.12),rgba(0,0,0,0.32))]" />
<div className="absolute inset-0 opacity-40 mix-blend-overlay bg-[linear-gradient(120deg,transparent_0%,rgba(255,255,255,0.06)_35%,transparent_60%),radial-gradient(circle_at_70%_30%,rgba(255,255,255,0.12),transparent_24%)]" />
</div>
<div className="absolute -bottom-24 -left-24 w-96 h-96 bg-tertiary/30 blur-[120px] rounded-full" />
</div>
<div className="w-full lg:w-5/12 bg-surface-container-lowest flex flex-col justify-center px-8 sm:px-16 lg:px-24">
<div className="max-w-md w-full mx-auto">
<div className="lg:hidden mb-12 flex justify-between items-center">
<Image
src="/ina_logo.png"
alt="Ina Trading"
width={150}
height={45}
priority
/>
<LanguageToggle />
</div>
<div className="hidden lg:flex justify-end mb-4">
<LanguageToggle />
</div>
<header className="mb-12">
<div className="h-1 w-12 bg-primary mb-6" />
<h2 className="font-headline text-4xl font-extrabold text-on-surface tracking-tight leading-tight">
Reset password
</h2>
<p className="mt-4 text-on-surface-variant font-medium leading-relaxed">
Enter your new credentials to regain access to your trading
dashboard.
</p>
</header>
<form className="space-y-8" onSubmit={handleSubmit}>
{error && (
<div className="rounded-xl border border-error/10 bg-error-container p-4 text-sm text-on-error-container">
{error}
</div>
)}
{submitted && (
<div className="rounded-xl border border-tertiary/10 bg-tertiary/5 p-4 text-sm text-on-tertiary-fixed-variant">
Reset password API belum tersedia. Screen ini sudah siap dan
submit reset password berhasil disimulasikan.
</div>
)}
<div className="space-y-2 group">
<label
htmlFor="new-password"
className="text-xs font-bold uppercase tracking-widest text-on-surface-variant group-focus-within:text-primary transition-colors"
>
New Password
</label>
<div className={passwordFieldWrapperClass}>
<input
id="new-password"
name="new-password"
type={showNewPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full rounded-xl bg-transparent border-none py-4 px-4 pr-12 text-on-surface font-medium placeholder:text-outline-variant focus:ring-0"
/>
<button
type="button"
onClick={() => setShowNewPassword((prev) => !prev)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-outline hover:text-on-surface transition-colors"
>
<span className="material-symbols-outlined">
{showNewPassword ? "visibility_off" : "visibility"}
</span>
</button>
</div>
</div>
<div className="space-y-2 group">
<label
htmlFor="confirm-password"
className="text-xs font-bold uppercase tracking-widest text-on-surface-variant group-focus-within:text-primary transition-colors"
>
Confirm New Password
</label>
<div className={passwordFieldWrapperClass}>
<input
id="confirm-password"
name="confirm-password"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full rounded-xl bg-transparent border-none py-4 px-4 pr-12 text-on-surface font-medium placeholder:text-outline-variant focus:ring-0"
/>
<button
type="button"
onClick={() => setShowConfirmPassword((prev) => !prev)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-outline hover:text-on-surface transition-colors"
>
<span className="material-symbols-outlined">
{showConfirmPassword ? "visibility_off" : "visibility"}
</span>
</button>
</div>
</div>
<div className="p-4 bg-surface-container rounded-lg border-l-4 border-tertiary">
<p className="text-xs font-medium text-on-surface-variant leading-relaxed">
<span className="text-tertiary font-bold">Requirement:</span>{" "}
Minimum 12 characters, including one uppercase letter, one
special character, and one numeric value.
</p>
<div className="mt-3 grid grid-cols-2 gap-2 text-[11px] font-medium">
<span className={checks.minLength ? "text-tertiary" : "text-outline"}>
12+ characters
</span>
<span className={checks.uppercase ? "text-tertiary" : "text-outline"}>
Uppercase letter
</span>
<span className={checks.number ? "text-tertiary" : "text-outline"}>
Numeric value
</span>
<span className={checks.special ? "text-tertiary" : "text-outline"}>
Special character
</span>
</div>
</div>
<div className="pt-4">
<button
type="submit"
className="editorial-gradient w-full py-5 rounded-xl shadow-2xl shadow-primary/20 text-on-primary font-headline font-bold text-lg hover:scale-[1.02] active:scale-95 transition-all duration-200 disabled:opacity-60 disabled:scale-100"
disabled={!newPassword || !confirmPassword}
>
Reset
</button>
</div>
</form>
<footer className="mt-12 pt-8 border-t border-surface-container text-center">
<p className="text-sm text-on-surface-variant font-medium">
Remember your password?
<Link
href="/login"
className="text-secondary font-bold hover:text-primary transition-colors ml-1"
>
Go back to Login
</Link>
</p>
</footer>
</div>
<div className="fixed bottom-8 right-8">
<button className="bg-surface-container-high text-on-surface w-14 h-14 rounded-full shadow-lg flex items-center justify-center hover:bg-surface-dim transition-colors group">
<span className="material-symbols-outlined text-2xl group-hover:scale-110 transition-transform">
help_outline
</span>
</button>
</div>
</div>
</div>
</>
);
}