Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import AuthShell from "@/components/AuthShell";
|
||||
import { AuthField, AuthPasswordField } from "@/components/AuthFields";
|
||||
import { postLoginHref, useAuth } from "@/components/AuthProvider";
|
||||
import { ApiError, getAccessToken, getRefreshToken } from "@/lib/meshkee-api";
|
||||
import { toE164CellNumber } from "@/lib/cellNumber";
|
||||
import {
|
||||
getAdminDashboardOrigin,
|
||||
getCustomerDashboardOrigin,
|
||||
hostAppFor,
|
||||
} from "@/lib/host";
|
||||
import styles from "@/components/AuthShell.module.css";
|
||||
|
||||
const websiteUrl =
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3000";
|
||||
|
||||
const tenantDomain =
|
||||
process.env.NEXT_PUBLIC_MESHKEE_TENANT_DOMAIN?.trim() ||
|
||||
(() => {
|
||||
try {
|
||||
return new URL(websiteUrl).hostname;
|
||||
} catch {
|
||||
return "iranbay.com";
|
||||
}
|
||||
})();
|
||||
|
||||
type AuthMode = "login" | "register";
|
||||
|
||||
function isSafeWebsiteReturn(next: string) {
|
||||
try {
|
||||
const url = new URL(next);
|
||||
const allowed = new URL(websiteUrl);
|
||||
return url.origin === allowed.origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function websiteHandoffUrl(next: string) {
|
||||
const access = getAccessToken();
|
||||
const refresh = getRefreshToken();
|
||||
if (!access || !refresh) return null;
|
||||
|
||||
const target = new URL(next);
|
||||
const hash = new URLSearchParams({
|
||||
access,
|
||||
refresh,
|
||||
next: `${target.pathname}${target.search}${target.hash}` || "/register",
|
||||
}).toString();
|
||||
|
||||
return `${target.origin}/auth/handoff#${hash}`;
|
||||
}
|
||||
|
||||
function splitName(fullName: string) {
|
||||
const parts = fullName.trim().split(/\s+/).filter(Boolean);
|
||||
const firstName = parts[0] || "";
|
||||
const lastName = parts.length > 1 ? parts.slice(1).join(" ") : firstName;
|
||||
return { firstName, lastName };
|
||||
}
|
||||
|
||||
async function finishAuth(
|
||||
dashboard: "super_admin" | "business" | "customer",
|
||||
returnToWebsite: string | null,
|
||||
router: ReturnType<typeof useRouter>,
|
||||
) {
|
||||
if (returnToWebsite) {
|
||||
const handoff = websiteHandoffUrl(returnToWebsite);
|
||||
if (handoff) {
|
||||
window.location.href = handoff;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const href = postLoginHref(dashboard);
|
||||
if (href.startsWith("http")) {
|
||||
window.location.href = href;
|
||||
} else {
|
||||
router.push(href);
|
||||
}
|
||||
}
|
||||
|
||||
export default function LoginForm() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { login, register } = useAuth();
|
||||
const [error, setError] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [hostReady, setHostReady] = useState(false);
|
||||
const [app, setApp] = useState<"admin" | "customer" | "business">("admin");
|
||||
const [adminLoginHref, setAdminLoginHref] = useState("");
|
||||
|
||||
const nextParam = searchParams.get("next");
|
||||
const returnToWebsite =
|
||||
nextParam && isSafeWebsiteReturn(nextParam) ? nextParam : null;
|
||||
const modeParam = searchParams.get("mode");
|
||||
const [mode, setMode] = useState<AuthMode>(() =>
|
||||
modeParam === "register" ? "register" : "login",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const host = window.location.host;
|
||||
const next = hostAppFor(host);
|
||||
setApp(next);
|
||||
setHostReady(true);
|
||||
setAdminLoginHref(`${getAdminDashboardOrigin()}/login`);
|
||||
if (next === "business") {
|
||||
const loginUrl = new URL(`${getCustomerDashboardOrigin()}/login`);
|
||||
if (returnToWebsite) loginUrl.searchParams.set("next", returnToWebsite);
|
||||
if (mode === "register") loginUrl.searchParams.set("mode", "register");
|
||||
window.location.replace(loginUrl.toString());
|
||||
}
|
||||
}, [returnToWebsite, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modeParam === "register") setMode("register");
|
||||
if (modeParam === "login") setMode("login");
|
||||
}, [modeParam]);
|
||||
|
||||
if (!hostReady || app === "business") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
background: "#f6f6f6",
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.2em",
|
||||
textTransform: "uppercase",
|
||||
color: "#8e8e8e",
|
||||
}}
|
||||
>
|
||||
Redirecting to login…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isRegister = mode === "register" && app === "customer";
|
||||
const titleLine1 = isRegister ? "Create" : "Login To";
|
||||
const titleLine2 = app === "customer" ? "Account" : "IranBay";
|
||||
|
||||
return (
|
||||
<AuthShell
|
||||
titleLine1={titleLine1}
|
||||
titleLine2={titleLine2}
|
||||
footerNote={
|
||||
isRegister ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.noteLink}
|
||||
onClick={() => {
|
||||
setError("");
|
||||
setMode("login");
|
||||
}}
|
||||
>
|
||||
Already have an account? Login
|
||||
</button>
|
||||
) : (
|
||||
<Link href="/forgot-password">Forgot password?</Link>
|
||||
)
|
||||
}
|
||||
>
|
||||
<form
|
||||
className={styles.form}
|
||||
autoComplete="off"
|
||||
key={isRegister ? "register" : "login"}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setIsSubmitting(true);
|
||||
|
||||
const form = new FormData(e.currentTarget);
|
||||
const phone = String(form.get("phone") ?? "");
|
||||
const password = String(form.get("password") ?? "");
|
||||
|
||||
try {
|
||||
if (isRegister) {
|
||||
const name = String(form.get("name") ?? "");
|
||||
const email = String(form.get("email") ?? "").trim();
|
||||
const { firstName, lastName } = splitName(name);
|
||||
|
||||
if (firstName.length < 2) {
|
||||
throw new ApiError("Please enter your full name.", 400);
|
||||
}
|
||||
|
||||
const dashboard = await register({
|
||||
cellNumber: toE164CellNumber(phone),
|
||||
password,
|
||||
firstName,
|
||||
lastName: lastName.length >= 2 ? lastName : firstName,
|
||||
email: email || undefined,
|
||||
domain: tenantDomain,
|
||||
});
|
||||
await finishAuth(dashboard, returnToWebsite, router);
|
||||
} else {
|
||||
const dashboard = await login(toE164CellNumber(phone), password);
|
||||
await finishAuth(dashboard, returnToWebsite, router);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError(
|
||||
isRegister
|
||||
? "Unable to create account. Is Meshkee API running?"
|
||||
: "Unable to sign in. Is Meshkee API running?",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{error ? (
|
||||
<p className={styles.error} role="alert">
|
||||
{error}
|
||||
{app === "customer" &&
|
||||
adminLoginHref &&
|
||||
error.includes("super admin access") ? (
|
||||
<>
|
||||
{" "}
|
||||
<a href={adminLoginHref}>Go to admin login</a>
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{returnToWebsite ? (
|
||||
<p className={styles.hint}>
|
||||
{isRegister
|
||||
? "Create an account to continue business registration"
|
||||
: "Sign in to continue business registration"}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{isRegister ? (
|
||||
<AuthField
|
||||
label="Name"
|
||||
name="name"
|
||||
autoComplete="name"
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<AuthField
|
||||
label={isRegister ? "Cell Number" : "Phone Number"}
|
||||
name="phone"
|
||||
type="tel"
|
||||
autoComplete="off"
|
||||
dir="ltr"
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
{isRegister ? (
|
||||
<AuthField
|
||||
label="Email Address"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<AuthPasswordField
|
||||
label="Password"
|
||||
name="password"
|
||||
autoComplete={isRegister ? "new-password" : "current-password"}
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{app === "customer" ? (
|
||||
isRegister ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryBtn}
|
||||
disabled={isSubmitting}
|
||||
onClick={() => {
|
||||
setError("");
|
||||
setMode("login");
|
||||
}}
|
||||
>
|
||||
Login instead
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryBtn}
|
||||
disabled={isSubmitting}
|
||||
onClick={() => {
|
||||
setError("");
|
||||
setMode("register");
|
||||
}}
|
||||
>
|
||||
Create account
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
<button
|
||||
type="submit"
|
||||
className={`outline-btn ${styles.primaryBtn}`}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "…" : isRegister ? "REGISTER" : "LOGIN"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user