diff --git a/microservices/apps/tpos-mvp-next/src/app/register/page.tsx b/microservices/apps/tpos-mvp-next/src/app/register/page.tsx index 55ae21cb..b9639bd4 100644 --- a/microservices/apps/tpos-mvp-next/src/app/register/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/register/page.tsx @@ -76,14 +76,14 @@ export default function RegisterPage() { -
+
TPOS TRIAL

Tạo tài khoản doanh nghiệp

Bắt đầu dùng thử miễn phí cho merchant, cửa hàng hoặc chuỗi vận hành. Không cần thẻ tín dụng.

-
+
@@ -98,53 +98,55 @@ export default function RegisterPage() { Thiết lập trong vài phút, có thể thêm cửa hàng và nhân viên sau khi đăng nhập.
- +
+ - + - + - + - + - + +
{message ?
{message}
: null} diff --git a/microservices/apps/tpos-mvp-next/src/app/styles/globals-part-10.css b/microservices/apps/tpos-mvp-next/src/app/styles/globals-part-10.css index 33863600..d89b53e4 100644 --- a/microservices/apps/tpos-mvp-next/src/app/styles/globals-part-10.css +++ b/microservices/apps/tpos-mvp-next/src/app/styles/globals-part-10.css @@ -1,4 +1,9 @@ +.login-portal__grid { + max-width: 780px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + .login-role-card:hover { border-color: var(--role-color); transform: translateY(-2px); @@ -51,6 +56,10 @@ display: none; } + .login-portal__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .tpos-mobile-nav { display: block; } @@ -156,8 +165,56 @@ min-height: calc(100vh - 72px); } +.auth-card { + min-width: 0; +} + +.auth-screen--merchant-register { + width: 100%; + max-width: 1180px; + margin: 0 auto; + grid-template-columns: minmax(280px, 360px) minmax(0, 760px); + align-items: center; + justify-content: center; +} + +.auth-screen--merchant-register .auth-copy { + max-width: 360px; + text-align: left; +} + +.auth-screen--merchant-register .auth-copy h1 { + font-size: 34px; +} + +.auth-screen--merchant-register .auth-copy p { + max-width: 340px; + margin: 0; +} + +.auth-card--register { + max-width: 760px; + padding: 28px; +} + +.auth-card--register .auth-card__head { + max-width: 560px; + margin-inline: auto; +} + +.auth-register-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px 16px; + margin-bottom: 16px; +} + +.auth-register-grid .auth-field { + margin-bottom: 0; +} + .auth-screen--split { - grid-template-columns: minmax(520px, 720px) minmax(360px, 440px); + grid-template-columns: minmax(0, 1fr) minmax(520px, 1fr); justify-content: stretch; align-content: stretch; gap: 0; @@ -169,7 +226,7 @@ display: flex; flex-direction: column; justify-content: center; - padding: 64px; + padding: clamp(48px, 6vw, 120px); background: radial-gradient(circle at 20% 10%, rgba(255, 255, 255, 0.16), transparent 24%), linear-gradient(180deg, #ff5c00 0%, #ff8a4c 52%, #ffb347 100%); @@ -224,6 +281,9 @@ .auth-screen--split .auth-card { align-self: center; justify-self: center; + width: min(100% - 96px, 480px); + max-width: 480px; + min-width: 0; } .auth-card__head { @@ -251,21 +311,25 @@ letter-spacing: 0.03em; } +.auth-card--blue, .auth-role-badge--blue, .auth-card--blue .auth-submit { --auth-tone: #3b82f6; } +.auth-card--green, .auth-role-badge--green, .auth-card--green .auth-submit { --auth-tone: #22c55e; } +.auth-card--pink, .auth-role-badge--pink, .auth-card--pink .auth-submit { --auth-tone: #ec4899; } +.auth-card--orange, .auth-role-badge--orange, .auth-card--orange .auth-submit { --auth-tone: #ff5c00; @@ -284,6 +348,25 @@ background: var(--auth-tone); } +.auth-field > div:focus-within { + border-color: color-mix(in srgb, var(--auth-tone, #ff5c00) 54%, #2a2a2e); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--auth-tone, #ff5c00) 16%, transparent); +} + +.auth-field svg, +.auth-phone-prefix { + flex: 0 0 auto; +} + +.auth-submit { + transition: transform 160ms ease, filter 160ms ease; +} + +.auth-submit:hover:not(:disabled) { + transform: translateY(-1px); + filter: brightness(1.06); +} + .auth-security-hint { width: 100%; display: flex; @@ -298,6 +381,112 @@ font-size: 13px; } +.auth-demo-strip { + width: 100%; + min-width: 0; + display: grid; + gap: 10px; + margin: 0 0 16px; + padding: 12px; + overflow: hidden; + border: 1px solid #2a2a2e; + border-radius: 12px; + background: #0a0a0b; +} + +.auth-demo-strip__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.auth-demo-strip__head span { + color: #ffffff; + font-size: 12px; + font-weight: 800; +} + +.auth-demo-strip__head small { + color: #8b8b90; + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} + +.auth-demo-list { + min-width: 0; + max-width: 100%; + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(200px, 1fr); + gap: 8px; + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-x: contain; + padding-bottom: 2px; + scrollbar-width: thin; +} + +.auth-demo-account { + width: 100%; + min-width: 0; + min-height: 74px; + display: grid; + align-content: center; + justify-items: start; + gap: 3px; + padding: 8px 10px; + border: 1px solid #2a2a2e; + border-radius: 10px; + background: #141416; + color: #ffffff; + cursor: pointer; + font: inherit; + text-align: left; + text-decoration: none; +} + +.auth-demo-account:hover, +.auth-demo-account--active { + border-color: color-mix(in srgb, var(--auth-tone, #ff5c00) 58%, #2a2a2e); + background: color-mix(in srgb, var(--auth-tone, #ff5c00) 12%, #141416); +} + +.auth-demo-account span { + color: var(--auth-tone, #ff5c00); + font-size: 11px; + font-weight: 800; + text-transform: uppercase; +} + +.auth-demo-account strong { + display: block; + width: 100%; + min-width: 0; + overflow: hidden; + color: #f4f4f5; + font-size: 12px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.auth-demo-account code { + display: inline-block; + max-width: 100%; + overflow: hidden; + padding: 4px 7px; + border-radius: 7px; + background: #050506; + color: #d4d4d8; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 11px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + .auth-alt-links { display: flex; justify-content: center; @@ -362,11 +551,95 @@ padding-left: 12px !important; } +.auth-screen--split .auth-card { + padding: 24px 32px; +} + +.auth-screen--split .auth-card__head { + gap: 8px; + margin-bottom: 16px; +} + +.auth-screen--split .auth-card__head h2 { + font-size: 24px; +} + +.auth-screen--split .auth-card__head p { + font-size: 13px; + line-height: 1.4; +} + +.auth-screen--split .auth-security-hint { + margin-bottom: 12px; + padding: 9px 14px; +} + +.auth-screen--split .auth-demo-strip { + gap: 8px; + margin-bottom: 12px; + padding: 10px; +} + +.auth-screen--split .auth-demo-account { + min-height: 34px; + padding: 7px 10px; +} + +.auth-screen--split .auth-field { + margin-bottom: 12px; +} + +.auth-screen--split .auth-field input, +.auth-screen--split .auth-phone-prefix { + min-height: 44px; +} + +.auth-screen--split .auth-social-row { + gap: 8px; + margin: 8px 0 10px; +} + +.auth-screen--split .auth-social-row button { + min-height: 34px; +} + +.auth-screen--split .auth-alt-links { + margin-top: 12px; +} + @media (max-width: 820px) { .auth-top-nav > div a:not(.btn-accent) { display: none; } + .auth-screen--merchant-register { + max-width: 100%; + grid-template-columns: minmax(0, 440px); + } + + .auth-screen--merchant-register .auth-copy { + max-width: 440px; + text-align: center; + } + + .auth-screen--merchant-register .auth-copy h1 { + font-size: 28px; + } + + .auth-screen--merchant-register .auth-copy p { + max-width: 360px; + margin: 0 auto; + } + + .auth-card--register { + max-width: 440px; + padding: 28px; + } + + .auth-register-grid { + grid-template-columns: 1fr; + } + .auth-screen--split { grid-template-columns: 1fr; } @@ -376,7 +649,174 @@ } .auth-screen--split .auth-card { - margin: 24px; + width: calc(100% - 48px); + margin: 24px auto; + } +} + +@media (max-width: 520px) { + .login-portal { + justify-content: flex-start; + padding: 88px 24px 48px; + } + + .login-portal__grid { + margin-top: 28px; + } + + .login-role-card { + min-height: 150px; + padding: 22px 18px; + } + + .login-role-card p { + font-size: 11px; + } + + .auth-top-nav { + height: 64px; + padding: 0 16px; + } + + .auth-top-nav > div { + gap: 0; + } + + .auth-top-nav .btn-accent { + min-height: 40px; + padding: 0 14px; + font-size: 13px; + } + + .auth-screen { + min-height: calc(100vh - 64px); + align-content: start; + padding: 32px 24px 42px; + } + + .auth-screen--merchant-register { + gap: 16px; + padding-top: 24px; + } + + .auth-card { + padding: 24px; + border-radius: 18px; + } + + .auth-demo-strip__head { + align-items: flex-start; + flex-direction: column; + gap: 4px; + } + + .auth-demo-strip__head small { + white-space: normal; + } + + .auth-demo-account { + grid-template-columns: 1fr; + align-items: start; + gap: 4px; + } + + .auth-demo-account strong, + .auth-demo-account code { + max-width: 100%; + overflow-wrap: anywhere; + white-space: normal; + } + + .auth-card__head h2 { + font-size: 24px; + } + + .auth-security-hint { + align-items: flex-start; + } + + .auth-screen--split { + padding: 0; + } + + .auth-screen--split .auth-card { + width: calc(100% - 48px); + max-width: calc(100% - 48px); + margin: 24px auto; + } + + .auth-screen--merchant-register .auth-copy h1 { + margin-bottom: 0; + font-size: 26px; + } + + .auth-screen--merchant-register .auth-copy p { + display: none; + } + + .auth-card--register { + padding: 24px; + } + + .auth-card--register .auth-card__head { + margin-bottom: 16px; + } + + .auth-card--register .auth-card__head p { + font-size: 13px; + } + + .auth-card--register .auth-security-hint { + margin-bottom: 14px; + padding: 10px 12px; + } + + .auth-register-grid { + gap: 12px; + } + + .auth-card--register .auth-field > div { + min-height: 44px; + } + + .auth-card--register .auth-field input { + min-height: 44px; + } + + .auth-demo-strip { + padding: 10px; + } + + .auth-demo-list { + grid-auto-columns: minmax(92px, 1fr); + } + + .auth-demo-account { + min-height: 38px; + justify-items: center; + text-align: center; + } + + .auth-demo-account strong, + .auth-demo-account code { + display: none; + } +} + +@media (min-width: 360px) and (max-width: 520px) { + .login-portal__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + } + + .login-role-card { + min-height: 142px; + padding: 18px 12px; + } + + .login-role-card span { + width: 42px; + height: 42px; } } diff --git a/microservices/apps/tpos-mvp-next/src/components/TposAuth.tsx b/microservices/apps/tpos-mvp-next/src/components/TposAuth.tsx index 76973eae..7071ba1d 100644 --- a/microservices/apps/tpos-mvp-next/src/components/TposAuth.tsx +++ b/microservices/apps/tpos-mvp-next/src/components/TposAuth.tsx @@ -1,13 +1,34 @@ "use client"; import type { FormEvent } from "react"; -import { useMemo, useState, useTransition } from "react"; +import { useEffect, useMemo, useState, useTransition } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { ArrowRight, Heart, Lock, Mail, MessageSquare, ShieldAlert, ShieldCheck, Store, UserCheck } from "lucide-react"; +const seedAccounts = { + admin: { label: "Admin", email: "admin@goodgo.vn", password: "Admin@123" }, + staff: { label: "Staff", email: "staff@goodgo.vn", password: "Staff@123" }, + marketing: { label: "Marketing", email: "marketing@goodgo.vn", password: "Marketing@123" }, + customer: { label: "Customer", email: "customer@goodgo.vn", password: "Customer@123" }, + superadmin: { label: "Superadmin", email: "superadmin@goodgo.vn", password: "SuperAdmin@123" } +} as const; + +type SeedRole = keyof typeof seedAccounts; +type AuthRole = SeedRole | "branch"; + +const demoAccounts = [ + { role: "admin", ...seedAccounts.admin }, + { role: "staff", ...seedAccounts.staff }, + { role: "marketing", ...seedAccounts.marketing }, + { role: "customer", ...seedAccounts.customer }, + { role: "superadmin", ...seedAccounts.superadmin } +] satisfies Array<{ role: SeedRole; label: string; email: string; password: string }>; + +type DemoAccount = (typeof demoAccounts)[number]; + type RoleCard = { - role: string; + role: AuthRole; title: string; heading: string; subtitle: string; @@ -38,8 +59,8 @@ const roleCards: RoleCard[] = [ submit: "Đăng nhập bảo mật", href: "/auth/login/admin", icon: ShieldCheck, - email: "", - password: "", + email: seedAccounts.admin.email, + password: seedAccounts.admin.password, tone: "blue", links: [{ label: "Chi nhánh", href: "/auth/login/branch" }, { label: "Nhân viên", href: "/auth/login/staff" }] }, @@ -73,8 +94,8 @@ const roleCards: RoleCard[] = [ submit: "Đăng nhập ca làm việc", href: "/auth/login/staff", icon: UserCheck, - email: "", - password: "", + email: seedAccounts.staff.email, + password: seedAccounts.staff.password, tone: "green", links: [{ label: "Liên hệ quản lý", href: "/#contact" }, { label: "Admin", href: "/auth/login/admin" }] }, @@ -89,8 +110,8 @@ const roleCards: RoleCard[] = [ submit: "Tiếp tục", href: "/auth/login/customer", icon: Heart, - email: "", - password: "", + email: seedAccounts.customer.email, + password: seedAccounts.customer.password, tone: "pink", split: true, brandTitle: "aPOS Loyalty", @@ -108,8 +129,8 @@ const roleCards: RoleCard[] = [ submit: "Vào marketing", href: "/auth/login/marketing", icon: MessageSquare, - email: "", - password: "", + email: seedAccounts.marketing.email, + password: seedAccounts.marketing.password, tone: "pink", links: [{ label: "Admin", href: "/auth/login/admin" }, { label: "Nhân viên", href: "/auth/login/staff" }] }, @@ -124,8 +145,8 @@ const roleCards: RoleCard[] = [ submit: "Vào platform", href: "/auth/login/superadmin", icon: ShieldAlert, - email: "", - password: "", + email: seedAccounts.superadmin.email, + password: seedAccounts.superadmin.password, tone: "orange", links: [{ label: "Admin", href: "/auth/login/admin" }] } @@ -136,15 +157,33 @@ export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; ro const searchParams = useSearchParams(); const selected = useMemo(() => roleCards.find((card) => card.role === role) ?? roleCards[0], [role]); const isCustomerLogin = selected.role === "customer"; + const demoRole = searchParams.get("demo"); const [email, setEmail] = useState(selected.email); const [password, setPassword] = useState(selected.password); const [displayName, setDisplayName] = useState(""); - const [phone, setPhone] = useState(""); + const [phone, setPhone] = useState(isCustomerLogin ? selected.email : ""); const [otp, setOtp] = useState(""); const [message, setMessage] = useState(null); const [isPending, startTransition] = useTransition(); const SelectedIcon = selected.icon; + useEffect(() => { + const requestedDemo = demoAccounts.find((account) => account.role === demoRole); + const roleDemo = demoAccounts.find((account) => account.role === selected.role); + const account = requestedDemo?.role === selected.role ? requestedDemo : roleDemo; + setEmail(account?.email ?? selected.email); + setPassword(account?.password ?? selected.password); + setPhone(account?.role === "customer" ? account.email : ""); + setMessage(null); + }, [demoRole, selected.email, selected.password, selected.role]); + + function demoLoginHref(account: DemoAccount) { + const params = new URLSearchParams(searchParams.toString()); + params.set("demo", account.role); + if (account.role !== selected.role) params.delete("returnUrl"); + return `/auth/login/${account.role}?${params.toString()}`; + } + async function submit(event?: FormEvent) { event?.preventDefault(); setMessage(null); @@ -283,6 +322,26 @@ export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; ro {selected.hint} +
+
+ Tài khoản seed demo + Nhấn để tự điền +
+
+ {demoAccounts.map((account) => ( + + {account.label} + {account.email} + {account.password} + + ))} +
+
{isCustomerLogin ? ( <>