Refactor TPOS shop and POS routes for parity

This commit is contained in:
Ho Ngoc Hai
2026-05-24 00:48:06 +07:00
parent 7f9434347f
commit 481f07b31a
12 changed files with 1335 additions and 257 deletions

View File

@@ -0,0 +1,5 @@
import { TposPublicLanding } from "@/components/TposPublicLanding";
export default function AboutPage() {
return <TposPublicLanding variant="project" />;
}

View File

@@ -1,7 +1,9 @@
import { TposAuthBoundary } from "@/components/TposAuthBoundary"; import { TposAuthBoundary } from "@/components/TposAuthBoundary";
import { TposLoginPortal } from "@/components/TposLoginPortal";
export default async function AuthCatchAllPage({ params }: { params: Promise<{ path?: string[] }> }) { export default async function AuthCatchAllPage({ params }: { params: Promise<{ path?: string[] }> }) {
const path = (await params).path ?? []; const path = (await params).path ?? [];
if (path.length === 0 || (path[0] === "login" && path.length === 1)) return <TposLoginPortal />;
const mode = path.includes("register") ? "register" : path.includes("forgot-password-new") || path.includes("password-reset") || path.includes("otp-verify") || path.includes("two-factor") || path.includes("email-sent") ? "recover" : "login"; const mode = path.includes("register") ? "register" : path.includes("forgot-password-new") || path.includes("password-reset") || path.includes("otp-verify") || path.includes("two-factor") || path.includes("email-sent") ? "recover" : "login";
const role = path[path.length - 1] ?? "admin"; const role = path[path.length - 1] ?? "admin";
return <TposAuthBoundary mode={mode} role={role} />; return <TposAuthBoundary mode={mode} role={role} />;

View File

@@ -0,0 +1 @@
export { default } from "../about/page";

View File

@@ -3615,3 +3615,870 @@ textarea {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
/* Public landing and login portal parity */
.public-shell {
min-height: 100vh;
background:
radial-gradient(circle at 50% 0%, rgba(255, 92, 0, 0.14), transparent 34%),
#0a0a0b;
color: #ffffff;
overflow-x: hidden;
}
.tpos-navbar {
position: sticky;
top: 0;
z-index: 80;
padding: 16px 0;
border-bottom: 1px solid #1f1f23;
background: rgba(10, 10, 11, 0.85);
backdrop-filter: blur(12px);
}
.tpos-navbar-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.tpos-logo {
color: #ff5c00;
font-size: 24px;
font-weight: 800;
text-decoration: none;
}
.tpos-nav-links {
display: flex;
align-items: center;
gap: 32px;
}
.tpos-nav-link {
color: #adadb0;
font-size: 14px;
font-weight: 500;
text-decoration: none;
}
.tpos-nav-link:hover {
color: #ffffff;
}
.btn-accent {
min-height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 24px;
border-radius: 10px;
background: #ff5c00;
color: #ffffff;
font-size: 15px;
font-weight: 600;
text-decoration: none;
}
.landing-hero {
position: relative;
min-height: 70vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 24px 48px;
text-align: center;
overflow: hidden;
}
.landing-hero--project {
min-height: 62vh;
}
.landing-terminal-preview {
position: absolute;
inset: auto 5% 28px auto;
width: min(560px, 44vw);
min-height: 300px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
background: rgba(17, 17, 19, 0.5);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.36);
opacity: 0.42;
transform: rotate(-2deg);
pointer-events: none;
}
.terminal-bar {
height: 46px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid #1f1f23;
}
.terminal-bar span {
color: #ff5c00;
font-weight: 800;
}
.terminal-bar b {
color: #22c55e;
font-size: 12px;
}
.terminal-body {
display: grid;
grid-template-columns: 70px 1fr 160px;
gap: 14px;
padding: 14px;
}
.terminal-sidebar,
.terminal-order,
.terminal-grid i {
border-radius: 12px;
background: #1a1a1d;
}
.terminal-sidebar {
min-height: 220px;
}
.terminal-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.terminal-grid i {
min-height: 104px;
border: 1px solid #2a2a2e;
}
.terminal-order {
display: grid;
align-content: end;
min-height: 220px;
padding: 14px;
color: #adadb0;
}
.terminal-order b {
color: #ff5c00;
}
.home-hero__badge {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 32px;
padding: 8px 20px;
border: 1px solid rgba(255, 92, 0, 0.25);
border-radius: 999px;
background: rgba(255, 92, 0, 0.15);
color: #ff8a4c;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.03em;
}
.home-hero__title {
position: relative;
z-index: 1;
max-width: 800px;
margin: 0 0 24px;
font-size: 56px;
font-weight: 800;
line-height: 1.08;
letter-spacing: -0.03em;
color: #ffffff;
}
.home-hero__subtitle {
position: relative;
z-index: 1;
max-width: 640px;
margin: 0 auto 40px;
color: #adadb0;
font-size: 18px;
line-height: 1.7;
}
.home-hero__actions {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 48px;
}
.home-hero__btn {
min-height: 54px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 32px;
border-radius: 14px;
font-size: 16px;
font-weight: 700;
text-decoration: none;
}
.home-hero__btn--primary {
background: linear-gradient(135deg, #ff5c00 0%, #ff8a4c 100%);
color: #ffffff;
box-shadow: 0 4px 24px rgba(255, 92, 0, 0.3);
}
.home-hero__btn--secondary {
border: 1px solid #2a2a2e;
background: transparent;
color: #ffffff;
}
.home-trust {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.home-trust__label {
color: #8b8b90;
font-size: 13px;
text-transform: uppercase;
}
.home-trust__stats {
display: flex;
gap: 16px;
color: #adadb0;
font-size: 14px;
font-weight: 600;
}
.home-verticals {
max-width: 900px;
margin: 0 auto;
padding: 48px 24px 64px;
}
.home-verticals__grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 16px;
}
.home-vertical-card {
min-height: 128px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 24px 16px;
border: 1px solid #1f1f23;
border-radius: 14px;
background: #111113;
color: #adadb0;
font-size: 13px;
font-weight: 600;
text-align: center;
text-decoration: none;
}
.home-vertical-card svg {
color: #ff5c00;
}
.home-vertical-card:hover {
border-color: #ff5c00;
background: rgba(255, 92, 0, 0.15);
color: #ffffff;
}
.tpos-section,
.project-intro,
.landing-cta {
max-width: 1200px;
margin: 0 auto;
padding: 72px 24px;
}
.tpos-section-header {
max-width: 680px;
margin: 0 auto 36px;
text-align: center;
}
.tpos-section-title,
.project-intro h2,
.landing-cta h2 {
margin: 0 0 14px;
color: #ffffff;
font-size: 36px;
font-weight: 800;
line-height: 1.15;
}
.tpos-section-desc,
.project-intro p,
.landing-cta p {
color: #adadb0;
font-size: 16px;
line-height: 1.7;
}
.tpos-feature-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
}
.tpos-feature-card,
.tpos-pricing-card {
border: 1px solid #1f1f23;
border-radius: 16px;
background: #111113;
padding: 22px;
}
.tpos-feature-icon {
width: 44px;
height: 44px;
display: grid;
place-items: center;
margin-bottom: 18px;
border-radius: 12px;
background: rgba(255, 92, 0, 0.15);
color: #ff5c00;
}
.tpos-feature-card h3,
.tpos-pricing-card h3 {
margin: 0 0 10px;
font-size: 18px;
}
.tpos-feature-card p,
.tpos-pricing-card p {
margin: 0;
color: #adadb0;
font-size: 14px;
line-height: 1.65;
}
.project-intro {
display: grid;
grid-template-columns: minmax(0, 1.05fr) 420px;
gap: 36px;
align-items: center;
}
.project-stack {
display: grid;
gap: 12px;
}
.project-stack div {
min-height: 54px;
display: flex;
align-items: center;
gap: 12px;
padding: 0 16px;
border: 1px solid #2a2a2e;
border-radius: 12px;
background: #1a1a1d;
color: #ffffff;
font-weight: 600;
}
.project-stack svg {
color: #22c55e;
}
.tpos-pricing-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
.tpos-pricing-card {
position: relative;
display: grid;
gap: 14px;
}
.tpos-pricing-card.featured {
border-color: #ff5c00;
box-shadow: 0 0 0 1px rgba(255, 92, 0, 0.35);
}
.tpos-pricing-badge {
width: fit-content;
padding: 4px 10px;
border-radius: 999px;
background: rgba(255, 92, 0, 0.16);
color: #ff8a4c;
font-size: 12px;
font-weight: 700;
}
.tpos-pricing-card strong {
color: #ffffff;
font-size: 30px;
}
.tpos-pricing-card a {
display: inline-flex;
align-items: center;
gap: 6px;
color: #ff5c00;
font-weight: 700;
text-decoration: none;
}
.landing-cta {
display: grid;
justify-items: center;
text-align: center;
}
.landing-cta svg {
color: #ff5c00;
}
.landing-cta div {
display: flex;
gap: 12px;
margin-top: 18px;
}
.login-portal {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 24px 60px;
background: #0a0a0b;
color: #ffffff;
font-family: var(--tpos-font);
}
.login-portal__brand {
position: absolute;
top: 24px;
left: 24px;
color: #ff5c00;
font-size: 24px;
font-weight: 800;
text-decoration: none;
}
.login-portal__head {
text-align: center;
}
.login-portal__head h1 {
margin: 0 0 8px;
font-size: 28px;
font-weight: 800;
}
.login-portal__head p,
.login-portal__footer {
color: #adadb0;
font-size: 15px;
}
.login-portal__grid {
width: 100%;
max-width: 560px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin-top: 40px;
}
.login-role-card {
min-height: 176px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 28px 20px;
border: 2px solid #2a2a2e;
border-radius: 12px;
background: #1a1a1d;
color: #ffffff;
text-align: center;
text-decoration: none;
}
.login-role-card:hover {
border-color: var(--role-color);
transform: translateY(-2px);
}
.login-role-card span {
width: 48px;
height: 48px;
display: grid;
place-items: center;
border-radius: 12px;
background: color-mix(in srgb, var(--role-color) 16%, transparent);
color: var(--role-color);
}
.login-role-card h2 {
margin: 0;
font-size: 15px;
font-weight: 700;
}
.login-role-card p {
margin: 0;
color: #adadb0;
font-size: 12px;
line-height: 1.4;
}
.login-portal__footer {
margin: 32px 0 0;
}
.login-portal__footer a,
.login-portal__super {
color: #ff5c00;
font-weight: 600;
text-decoration: none;
}
.login-portal__super {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 14px;
font-size: 12px;
}
@media (max-width: 820px) {
.tpos-nav-links {
gap: 16px;
}
.tpos-nav-link:nth-child(2),
.tpos-nav-link:nth-child(3),
.tpos-nav-link:nth-child(4) {
display: none;
}
.landing-terminal-preview {
display: none;
}
.home-hero__title {
font-size: 40px;
}
.home-verticals__grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.tpos-feature-grid,
.project-intro,
.tpos-pricing-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
.tpos-navbar-inner {
padding: 0 16px;
}
.btn-accent {
display: none;
}
.home-hero__title {
font-size: 34px;
}
.home-hero__subtitle {
font-size: 15px;
}
.home-hero__actions,
.landing-cta div {
width: 100%;
flex-direction: column;
}
.home-hero__btn {
width: 100%;
}
.home-verticals__grid,
.login-portal__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Auth role pages parity */
.auth-top-nav {
position: sticky;
top: 0;
z-index: 90;
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
border-bottom: 1px solid #1f1f23;
background: rgba(10, 10, 11, 0.88);
backdrop-filter: blur(12px);
}
.auth-top-nav > div {
display: flex;
align-items: center;
gap: 24px;
}
.auth-top-nav a:not(.btn-accent) {
color: #adadb0;
font-size: 14px;
font-weight: 500;
text-decoration: none;
}
.auth-top-nav .tpos-logo {
color: #ff5c00 !important;
font-size: 24px;
font-weight: 800;
}
.auth-top-nav a:hover {
color: #ffffff;
}
.auth-screen {
min-height: calc(100vh - 72px);
}
.auth-screen--split {
grid-template-columns: minmax(420px, 0.95fr) minmax(360px, 440px);
justify-content: stretch;
align-content: stretch;
gap: 0;
padding: 0;
}
.auth-brand-panel {
min-height: calc(100vh - 72px);
display: flex;
flex-direction: column;
justify-content: center;
padding: 64px;
background:
radial-gradient(circle at 20% 10%, rgba(255, 255, 255, 0.16), transparent 24%),
linear-gradient(180deg, #ff5c00 0%, #ff8a4c 52%, #ffb347 100%);
color: #ffffff;
}
.auth-screen--customer .auth-brand-panel {
background:
radial-gradient(circle at 80% 20%, rgba(255, 92, 0, 0.16), transparent 30%),
linear-gradient(135deg, #0a0a0b 0%, #1a1a1d 50%, #0a0a0b 100%);
}
.auth-brand-mark {
width: 72px;
height: 72px;
display: grid;
place-items: center;
margin-bottom: 24px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.18);
}
.auth-brand-panel h1 {
max-width: 460px;
margin: 0 0 16px;
font-size: 42px;
font-weight: 800;
line-height: 1.08;
}
.auth-brand-panel p {
max-width: 480px;
color: rgba(255, 255, 255, 0.86);
font-size: 16px;
line-height: 1.7;
}
.auth-brand-stats {
display: flex;
gap: 12px;
margin-top: 28px;
}
.auth-brand-stats span {
padding: 8px 12px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.18);
font-size: 12px;
font-weight: 700;
}
.auth-screen--split .auth-card {
align-self: center;
justify-self: center;
}
.auth-card__head {
flex-direction: column;
align-items: center;
gap: 10px;
text-align: center;
}
.auth-card__head p {
margin: 0;
color: #adadb0;
font-size: 14px;
line-height: 1.5;
}
.auth-role-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 14px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.03em;
}
.auth-role-badge--blue,
.auth-card--blue .auth-submit {
--auth-tone: #3b82f6;
}
.auth-role-badge--green,
.auth-card--green .auth-submit {
--auth-tone: #22c55e;
}
.auth-role-badge--pink,
.auth-card--pink .auth-submit {
--auth-tone: #ec4899;
}
.auth-role-badge--orange,
.auth-card--orange .auth-submit {
--auth-tone: #ff5c00;
}
.auth-role-badge {
border: 1px solid color-mix(in srgb, var(--auth-tone) 28%, transparent);
background: color-mix(in srgb, var(--auth-tone) 15%, transparent);
color: var(--auth-tone);
}
.auth-card--blue .auth-submit,
.auth-card--green .auth-submit,
.auth-card--pink .auth-submit,
.auth-card--orange .auth-submit {
background: var(--auth-tone);
}
.auth-security-hint {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding: 10px 16px;
border-radius: 10px;
color: var(--auth-tone, #ff5c00);
background: color-mix(in srgb, var(--auth-tone, #ff5c00) 9%, transparent);
border: 1px solid color-mix(in srgb, var(--auth-tone, #ff5c00) 20%, transparent);
font-size: 13px;
}
.auth-alt-links {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 14px;
margin-top: 16px;
}
.auth-alt-links a,
.auth-inline-link {
color: #ff5c00;
font-size: 13px;
font-weight: 600;
text-decoration: none;
}
.auth-social-row {
display: grid;
gap: 10px;
margin: 12px 0;
color: #8b8b90;
font-size: 12px;
text-align: center;
}
.auth-social-row div {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.auth-social-row button {
min-height: 38px;
border: 1px solid #2a2a2e;
border-radius: 10px;
background: #1a1a1d;
color: #ffffff;
font-weight: 600;
}
@media (max-width: 820px) {
.auth-top-nav > div a:not(.btn-accent) {
display: none;
}
.auth-screen--split {
grid-template-columns: 1fr;
}
.auth-brand-panel {
display: none;
}
.auth-screen--split .auth-card {
margin: 24px;
}
}

View File

@@ -1,5 +1,5 @@
import { TposAuthBoundary } from "@/components/TposAuthBoundary"; import { TposLoginPortal } from "@/components/TposLoginPortal";
export default function LoginAliasPage() { export default function LoginAliasPage() {
return <TposAuthBoundary mode="login" role="admin" />; return <TposLoginPortal />;
} }

View File

@@ -1,170 +1,5 @@
import Link from "next/link"; import { TposPublicLanding } from "@/components/TposPublicLanding";
import { Activity, AlertTriangle, ShoppingBag, Store } from "lucide-react";
import { createShopAction } from "@/app/actions";
import { AppFrame } from "@/components/AppFrame";
import { EmptyState, Metric, PageHeader, StatusPill, money } from "@/components/Primitives";
import { getDashboardStats } from "@/server/db/queries";
import { getShopService } from "@/server/services/shop";
import { listActivity } from "@/server/db/queries";
import { serviceMap, verticalOptions } from "@/server/domain/catalog";
export const dynamic = "force-dynamic"; export default function LandingPage() {
return <TposPublicLanding />;
type PageProps = {
searchParams?: Promise<{ shopId?: string }>;
};
export default async function DashboardPage({ searchParams }: PageProps) {
const params = (await searchParams) ?? {};
const shop = await getShopService(params.shopId);
const stats = await getDashboardStats(shop?.id ?? null);
const activity = await listActivity();
return (
<AppFrame shopId={shop?.id}>
<PageHeader eyebrow="GoodGo Platform" title={shop ? `Vận hành ${shop.name}` : "Tạo cửa hàng đầu tiên"}>
{shop ? (
<Link className="primary-action" href={`/pos?shopId=${shop.id}`}>
<ShoppingBag size={18} />
Mở POS
</Link>
) : null}
</PageHeader>
{!shop ? (
<div className="split">
<form action={createShopAction} className="form-panel stack">
<h2>Thiết lập cửa hàng</h2>
<label className="field">
<span>Tên cửa hàng</span>
<input name="name" placeholder="GoodGo Cafe District 1" required />
</label>
<label className="field">
<span>Ngành hàng</span>
<select name="vertical" defaultValue="cafe">
{verticalOptions.map((vertical) => (
<option key={vertical.id} value={vertical.id}>
{vertical.label}
</option>
))}
</select>
</label>
<button className="primary-action" type="submit">
<Store size={18} />
Tạo cửa hàng
</button>
</form>
<EmptyState title="Chưa có dữ liệu cửa hàng">Dashboard sẽ xuất hiện sau khi tạo cửa hàng đu tiên.</EmptyState>
</div>
) : (
<>
<div className="metric-grid">
<Metric label="Doanh thu hôm nay" value={money(stats.todayRevenue)} tone="good" />
<Metric label="Doanh thu tháng" value={money(stats.monthRevenue)} tone="info" />
<Metric label="Đơn hàng" value={stats.orderCount} />
<Metric label="Sắp hết hàng" value={stats.lowStockCount} tone={stats.lowStockCount > 0 ? "warn" : "neutral"} />
</div>
<div className="dashboard-grid">
<section className="panel">
<div className="panel-title">
<h2>Đơn gần đây</h2>
<Link href={`/orders?shopId=${shop.id}`}>Xem tất cả</Link>
</div>
{stats.recentOrders.length ? (
<table className="table">
<thead>
<tr>
<th> đơn</th>
<th>Món</th>
<th>Trạng thái</th>
<th>Tổng</th>
</tr>
</thead>
<tbody>
{stats.recentOrders.map((order) => (
<tr key={order.id}>
<td>{order.transactionId ?? order.id.slice(0, 8)}</td>
<td>{order.itemCount}</td>
<td>
<StatusPill label={order.status} tone={order.status === "Paid" ? "good" : "neutral"} />
</td>
<td>{money(order.totalAmount)}</td>
</tr>
))}
</tbody>
</table>
) : (
<EmptyState title="Chưa có đơn hàng" />
)}
</section>
<section className="panel stack">
<div className="panel-title">
<h2>Service map MVP</h2>
<StatusPill label="Monolith MVP" tone="good" />
</div>
{serviceMap.map((service) => {
const Icon = service.icon;
return (
<div className="activity-item" key={service.name}>
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Icon size={16} />
{service.name}
</span>
<span>{service.label}</span>
</div>
);
})}
</section>
<section className="panel">
<div className="panel-title">
<h2>Tồn kho cảnh báo</h2>
<AlertTriangle size={18} />
</div>
{stats.lowStock.length ? (
<table className="table">
<thead>
<tr>
<th>Mặt hàng</th>
<th>Tồn</th>
<th>Mức đt lại</th>
</tr>
</thead>
<tbody>
{stats.lowStock.map((item) => (
<tr key={item.id}>
<td>{item.name ?? item.productName}</td>
<td>{item.quantity}</td>
<td>{item.reorderLevel}</td>
</tr>
))}
</tbody>
</table>
) : (
<EmptyState title="Tồn kho đang ổn" />
)}
</section>
<section className="panel">
<div className="panel-title">
<h2>Hoạt đng</h2>
<Activity size={18} />
</div>
<div className="activity-list">
{activity.map((item) => (
<div className="activity-item" key={item.id}>
<span>{item.action}</span>
<span>{new Date(item.createdAt).toLocaleString("vi-VN")}</span>
</div>
))}
{!activity.length ? <EmptyState title="Chưa có hoạt động" /> : null}
</div>
</section>
</div>
</>
)}
</AppFrame>
);
} }

View File

@@ -0,0 +1 @@
export { default } from "../about/page";

View File

@@ -17,7 +17,7 @@ import type { ReactNode } from "react";
import type { Shop } from "@/server/domain/types"; import type { Shop } from "@/server/domain/types";
const nav = [ const nav = [
{ href: "/", label: "Tổng quan", icon: Gauge }, { href: "/admin", label: "Quản trị", icon: Gauge },
{ href: "/pos", label: "Bán hàng", icon: TerminalSquare }, { href: "/pos", label: "Bán hàng", icon: TerminalSquare },
{ href: "/catalog", label: "Sản phẩm", icon: PackagePlus }, { href: "/catalog", label: "Sản phẩm", icon: PackagePlus },
{ href: "/inventory", label: "Kho", icon: Boxes }, { href: "/inventory", label: "Kho", icon: Boxes },
@@ -39,6 +39,7 @@ export function Shell({
const router = useRouter(); const router = useRouter();
const params = useSearchParams(); const params = useSearchParams();
const shopQuery = currentShop ? `?shopId=${currentShop.id}` : ""; const shopQuery = currentShop ? `?shopId=${currentShop.id}` : "";
const adminHref = currentShop ? `/admin/shop/${currentShop.id}/overview` : "/admin";
function switchShop(shopId: string) { function switchShop(shopId: string) {
const next = new URLSearchParams(params.toString()); const next = new URLSearchParams(params.toString());
@@ -49,7 +50,7 @@ export function Shell({
return ( return (
<div className="app-shell"> <div className="app-shell">
<aside className="sidebar"> <aside className="sidebar">
<Link className="brand" href={shopQuery ? `/${shopQuery}` : "/"}> <Link className="brand" href={adminHref}>
<span className="brand-mark">a</span> <span className="brand-mark">a</span>
<span> <span>
<strong>aPOS</strong> <strong>aPOS</strong>
@@ -74,9 +75,9 @@ export function Shell({
<nav className="side-nav"> <nav className="side-nav">
{nav.map((item) => { {nav.map((item) => {
const active = pathname === item.href; const active = item.href === "/admin" ? pathname === "/admin" || pathname.startsWith("/admin/") : pathname === item.href;
const Icon = item.icon; const Icon = item.icon;
const href = item.href === "/" ? `/${shopQuery}` : `${item.href}${shopQuery}`; const href = item.href === "/admin" ? adminHref : `${item.href}${shopQuery}`;
return ( return (
<Link key={item.href} href={href} className={active ? "side-link active" : "side-link"}> <Link key={item.href} href={href} className={active ? "side-link active" : "side-link"}>
<Icon size={18} /> <Icon size={18} />

View File

@@ -3,13 +3,116 @@
import type { FormEvent } from "react"; import type { FormEvent } from "react";
import { useMemo, useState, useTransition } from "react"; import { useMemo, useState, useTransition } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { ArrowRight, Lock, Mail, ShieldCheck, Store, Users } from "lucide-react"; import Link from "next/link";
import { ArrowRight, Heart, Lock, Mail, ShieldAlert, ShieldCheck, Store, UserCheck } from "lucide-react";
const roleCards = [ type RoleCard = {
{ role: "admin", title: "Quản trị", href: "/auth/login/admin", icon: Store, email: "admin@goodgo.vn", password: "Admin@123" }, role: string;
{ role: "staff", title: "Nhân viên", href: "/auth/login/staff", icon: Users, email: "staff@goodgo.vn", password: "Staff@123" }, title: string;
{ role: "customer", title: "Khách hàng", href: "/auth/login/customer", icon: Mail, email: "customer@goodgo.vn", password: "Customer@123" }, heading: string;
{ role: "superadmin", title: "Super Admin", href: "/auth/login/superadmin", icon: ShieldCheck, email: "superadmin@goodgo.vn", password: "SuperAdmin@123" } subtitle: string;
badge: string;
hint: string;
inputLabel: string;
submit: string;
href: string;
icon: typeof Store;
email: string;
password: string;
tone: "blue" | "orange" | "green" | "pink";
split?: boolean;
brandTitle?: string;
brandText?: string;
links: Array<{ label: string; href: string }>;
};
const roleCards: RoleCard[] = [
{
role: "admin",
title: "Quản trị",
heading: "Đăng nhập Admin",
subtitle: "Truy cập hệ thống quản trị aPOS",
badge: "QUẢN TRỊ",
hint: "Khu vực bảo mật cao",
inputLabel: "Email quản trị viên",
submit: "Đăng nhập bảo mật",
href: "/auth/login/admin",
icon: ShieldCheck,
email: "admin@goodgo.vn",
password: "Admin@123",
tone: "blue",
links: [{ label: "Chi nhánh", href: "/auth/login/branch" }, { label: "Nhân viên", href: "/auth/login/staff" }]
},
{
role: "branch",
title: "Chi nhánh",
heading: "Đăng nhập Chi nhánh",
subtitle: "Quản lý chi nhánh, ca làm việc, báo cáo và hiệu suất cửa hàng",
badge: "BRANCH",
hint: "Đồng bộ dữ liệu cửa hàng theo thời gian thực",
inputLabel: "Email hoặc số điện thoại",
submit: "Đăng nhập",
href: "/auth/login/branch",
icon: Store,
email: "admin@goodgo.vn",
password: "Admin@123",
tone: "orange",
split: true,
brandTitle: "aPOS Branch",
brandText: "Theo dõi doanh thu, nhân sự, ca bán và báo cáo cửa hàng trong một giao diện thống nhất.",
links: [{ label: "Quên mật khẩu?", href: "/forgot-password" }, { label: "Liên hệ", href: "/#contact" }]
},
{
role: "staff",
title: "Nhân viên",
heading: "Đăng nhập Nhân viên",
subtitle: "Nhập mã nhân viên hoặc email để bắt đầu ca làm việc",
badge: "NHÂN VIÊN",
hint: "Thu ngân, Barista, Phục vụ, Quản lý",
inputLabel: "Mã nhân viên hoặc email",
submit: "Đăng nhập ca làm việc",
href: "/auth/login/staff",
icon: UserCheck,
email: "staff@goodgo.vn",
password: "Staff@123",
tone: "green",
links: [{ label: "Liên hệ quản lý", href: "/#contact" }, { label: "Admin", href: "/auth/login/admin" }]
},
{
role: "customer",
title: "Khách hàng",
heading: "Chào mừng bạn!",
subtitle: "Đăng nhập để nhận ưu đãi và tích điểm thưởng",
badge: "CUSTOMER",
hint: "Ưu đãi, tích điểm, lịch sử mua hàng",
inputLabel: "Email hoặc số điện thoại",
submit: "Đăng nhập khách hàng",
href: "/auth/login/customer",
icon: Heart,
email: "customer@goodgo.vn",
password: "Customer@123",
tone: "pink",
split: true,
brandTitle: "aPOS Loyalty",
brandText: "Theo dõi điểm thưởng, voucher và lịch sử mua hàng từ QR menu đến POS.",
links: [{ label: "Đăng ký ngay", href: "/auth/register/customer" }, { label: "Điều khoản", href: "/about" }]
},
{
role: "superadmin",
title: "Super Admin",
heading: "Super Admin",
subtitle: "Điều phối platform, merchant, feature flags và health checks",
badge: "PLATFORM",
hint: "Khu vực quản trị hệ thống",
inputLabel: "Email Super Admin",
submit: "Vào platform",
href: "/auth/login/superadmin",
icon: ShieldAlert,
email: "superadmin@goodgo.vn",
password: "SuperAdmin@123",
tone: "orange",
links: [{ label: "Admin", href: "/auth/login/admin" }]
}
]; ];
export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; role?: string }) { export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; role?: string }) {
@@ -38,7 +141,7 @@ export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; ro
} }
const returnUrl = searchParams.get("returnUrl"); const returnUrl = searchParams.get("returnUrl");
if (returnUrl) { if (returnUrl) {
router.push(returnUrl); if (returnUrl.startsWith("/") && !returnUrl.startsWith("//")) router.push(returnUrl);
return; return;
} }
const portal = payload.data?.roles?.[0]?.portal ?? "admin"; const portal = payload.data?.roles?.[0]?.portal ?? "admin";
@@ -49,78 +152,103 @@ export function TposAuth({ mode = "login", role = "admin" }: { mode?: string; ro
if (mode !== "login") { if (mode !== "login") {
return ( return (
<main className="auth-screen"> <>
<section className="auth-copy"> <AuthNav />
<span className="eyebrow">aPOS account</span> <main className="auth-screen">
<h1>{mode === "register" ? "Tạo tài khoản vận hành" : "Khôi phục truy cập"}</h1> <section className="auth-copy">
<p>Luồng này giữ route parity với TPOS gốc. Bản MVP lưu trạng thái tài khoản trong DB dùng httpOnly session.</p> <span className="eyebrow">aPOS account</span>
</section> <h1>{mode === "register" ? "Đăng ký dùng thử" : "Khôi phục truy cập"}</h1>
<section className="auth-card"> <p>{mode === "register" ? "Tạo merchant, chọn ngành hàng và bắt đầu thiết lập cửa hàng TPOS MVP." : "Xác thực tài khoản, OTP, email sent và reset password theo route parity của TPOS gốc."}</p>
<div className="auth-state"> </section>
<ShieldCheck size={38} /> <section className="auth-card">
<h2>{mode === "register" ? "Đăng ký merchant" : "Xác thực bảo mật"}</h2> <div className="auth-state">
<p>Endpoint đã sẵn sàng trong BFF; màn hình chi tiết sẽ nối vào workflow IAM khi bật onboarding đy đ.</p> <ShieldCheck size={38} />
</div> <h2>{mode === "register" ? "Đăng ký merchant" : "Xác thực bảo mật"}</h2>
</section> <p>Luồng public đã giao diện route đy đ; endpoint onboarding/recovery sẽ nối vào BFF khi bật IAM đy đ.</p>
</main> <Link className="auth-inline-link" href="/auth/login">Quay lại portal đăng nhập</Link>
</div>
</section>
</main>
</>
); );
} }
return ( return (
<main className="auth-screen"> <>
<section className="auth-copy"> <AuthNav />
<span className="eyebrow">GoodGo TPOS</span> <main className={selected.split ? `auth-screen auth-screen--split auth-screen--${selected.role}` : "auth-screen"}>
<h1>Đăng nhập theo vai trò</h1> {selected.split ? (
<p>Giao diện Next dùng lại hình BFF session của TPOS: cookie httpOnly, role portal redirect theo quyền.</p> <section className="auth-brand-panel">
<div className="auth-role-grid"> <div className="auth-brand-mark"><SelectedIcon size={34} /></div>
{roleCards.map((card) => { <h1>{selected.brandTitle}</h1>
const Icon = card.icon; <p>{selected.brandText}</p>
return ( <div className="auth-brand-stats">
<button <span>50,000+ giao dịch/ngày</span>
type="button" <span>99.9% uptime</span>
key={card.role} </div>
className={card.role === selected.role ? "auth-role auth-role--active" : "auth-role"} </section>
onClick={() => { ) : null}
setEmail(card.email);
setPassword(card.password); <form className={`auth-card auth-card--${selected.tone}`} onSubmit={submit}>
router.push(card.href); <div className="auth-card__head">
}} <span className={`auth-role-badge auth-role-badge--${selected.tone}`}>
> <SelectedIcon size={14} />
<Icon size={18} /> {selected.badge}
<span>{card.title}</span> </span>
</button> <h2>{selected.heading}</h2>
); <p>{selected.subtitle}</p>
})}
</div>
</section>
<form className="auth-card" onSubmit={submit}>
<div className="auth-card__head">
<SelectedIcon size={28} />
<div>
<span>{selected.title}</span>
<h2>Đăng nhập</h2>
</div> </div>
</div> <div className={`auth-security-hint auth-security-hint--${selected.tone}`}>
<label className="auth-field"> <ShieldCheck size={16} />
<span>Email</span> <span>{selected.hint}</span>
<div>
<Mail size={16} />
<input value={email} onChange={(event) => setEmail(event.target.value)} autoComplete="email" />
</div> </div>
</label> <label className="auth-field">
<label className="auth-field"> <span>{selected.inputLabel}</span>
<span>Mật khẩu</span> <div>
<div> <Mail size={16} />
<Lock size={16} /> <input value={email} onChange={(event) => setEmail(event.target.value)} autoComplete="email" />
<input type="password" value={password} onChange={(event) => setPassword(event.target.value)} autoComplete="current-password" /> </div>
</label>
<label className="auth-field">
<span>Mật khẩu</span>
<div>
<Lock size={16} />
<input type="password" value={password} onChange={(event) => setPassword(event.target.value)} autoComplete="current-password" />
</div>
</label>
{selected.role === "customer" ? (
<div className="auth-social-row">
<span>Hoặc đăng nhập bằng</span>
<div>
<button type="button">Google</button>
<button type="button">Facebook</button>
</div>
</div>
) : null}
{message ? <div className="auth-message">{message}</div> : null}
<button className="auth-submit" type="submit" disabled={isPending}>
<span>{isPending ? "Đang xử lý..." : selected.submit}</span>
<ArrowRight size={18} />
</button>
<div className="auth-alt-links">
{selected.links.map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}
</div> </div>
</label> </form>
{message ? <div className="auth-message">{message}</div> : null} </main>
<button className="auth-submit" type="submit" disabled={isPending}> </>
<span>{isPending ? "Đang xác thực" : "Vào hệ thống"}</span> );
<ArrowRight size={18} /> }
</button>
</form> function AuthNav() {
</main> return (
<nav className="auth-top-nav">
<Link href="/" className="tpos-logo">aPOS</Link>
<div>
<Link href="/#features">Tính năng</Link>
<Link href="/#pricing">Bảng giá</Link>
<Link href="/auth/login">Đăng nhập</Link>
<Link className="btn-accent" href="/register">Dùng thử miễn phí</Link>
</div>
</nav>
); );
} }

View File

@@ -0,0 +1,42 @@
import Link from "next/link";
import type { CSSProperties } from "react";
import { Building2, Heart, ShieldCheck, Store, UserCheck } from "lucide-react";
const roles = [
{ title: "Chủ doanh nghiệp", href: "/auth/login/admin", icon: Building2, color: "#3B82F6", description: "Quản lý toàn bộ hệ thống, cửa hàng, nhân viên" },
{ title: "Quản lý chi nhánh", href: "/auth/login/branch", icon: Store, color: "#8B5CF6", description: "Quản lý chi nhánh, ca làm việc, báo cáo" },
{ title: "Nhân viên", href: "/auth/login/staff", icon: UserCheck, color: "#22C55E", description: "Thu ngân, barista, phục vụ, bếp" },
{ title: "Khách hàng", href: "/auth/login/customer", icon: Heart, color: "#EC4899", description: "Tích điểm, ưu đãi, lịch sử mua hàng" }
];
export function TposLoginPortal() {
return (
<main className="login-portal">
<Link href="/" className="login-portal__brand">aPOS</Link>
<section className="login-portal__head">
<h1>Chọn loại tài khoản</h1>
<p>Đăng nhập với vai trò phù hợp</p>
</section>
<section className="login-portal__grid">
{roles.map(({ title, href, icon: Icon, color, description }) => (
<Link key={href} href={href} className="login-role-card" style={{ "--role-color": color } as CSSProperties}>
<span>
<Icon size={24} />
</span>
<h2>{title}</h2>
<p>{description}</p>
</Link>
))}
</section>
<p className="login-portal__footer">
Chưa tài khoản? <Link href="/register">Đăng ngay</Link>
</p>
<Link href="/auth/login/superadmin" className="login-portal__super">
<ShieldCheck size={14} />
Super Admin
</Link>
</main>
);
}

View File

@@ -146,7 +146,7 @@ export function TposPortal({
<Globe2 size={18} /> <Globe2 size={18} />
</div> </div>
<div className="portal-list portal-list--compact"> <div className="portal-list portal-list--compact">
{(payload.secondary ?? defaultSecondary(kind)).map((item) => ( {(payload.secondary ?? defaultSecondary(kind, payload.shop)).map((item) => (
<LinkOrDiv key={item.id ?? item.title} item={item} /> <LinkOrDiv key={item.id ?? item.title} item={item} />
))} ))}
</div> </div>
@@ -190,12 +190,15 @@ function ShopSectionPanel({ shop }: { shop: NonNullable<PortalPayload["shop"]> }
<Store size={18} /> <Store size={18} />
</div> </div>
<div className="shop-section-grid"> <div className="shop-section-grid">
{sections.map(([label, slug, Icon]) => ( {sections.map(([label, slug, Icon]) => {
<Link key={slug} className="shop-section" href={`/admin/shop/${shop.id}/${slug}`}> const href = slug === "pos" ? `/pos/${shop.id}/${vertical}` : `/admin/shop/${shop.id}/${slug}`;
<Icon size={17} /> return (
<span>{label}</span> <Link key={slug} className="shop-section" href={href}>
</Link> <Icon size={17} />
))} <span>{label}</span>
</Link>
);
})}
</div> </div>
</article> </article>
); );
@@ -237,15 +240,16 @@ function defaultItems(kind: PortalKind): ListItem[] {
]; ];
} }
function defaultSecondary(kind: PortalKind): ListItem[] { function defaultSecondary(kind: PortalKind, shop?: PortalPayload["shop"]): ListItem[] {
if (kind === "superadmin") return [ if (kind === "superadmin") return [
{ title: "System health", href: "/superadmin/system/health" }, { title: "System health", href: "/superadmin/system/health" },
{ title: "Feature flags", href: "/superadmin/system/flags" }, { title: "Feature flags", href: "/superadmin/system/flags" },
{ title: "Audit log", href: "/superadmin/system/audit" } { title: "Audit log", href: "/superadmin/system/audit" }
]; ];
const vertical = shop?.vertical ?? "cafe";
return [ return [
{ title: "Customer menu", href: "/" }, { title: "Customer menu", href: shop ? `/menu/${shop.id}` : "/" },
{ title: "POS Café", href: "/pos" }, { title: "POS terminal", href: shop ? `/pos/${shop.id}/${vertical}` : "/pos" },
{ title: "Settings", href: "/settings" } { title: "Settings", href: "/settings" }
]; ];
} }

View File

@@ -0,0 +1,192 @@
import Link from "next/link";
import {
ArrowRight,
BarChart3,
Bot,
Check,
Coffee,
Heart,
LogIn,
Mic,
ShieldCheck,
ShoppingBag,
Sparkles,
Store,
UtensilsCrossed,
Zap
} from "lucide-react";
const verticals = [
{ label: "Cafe", icon: Coffee },
{ label: "Nhà hàng & F&B", icon: UtensilsCrossed },
{ label: "Karaoke", icon: Mic },
{ label: "TMV/Spa", icon: Sparkles },
{ label: "Bán lẻ", icon: ShoppingBag }
];
const features = [
{ title: "POS đa ngành", desc: "Cafe, nhà hàng, karaoke, spa, beauty và retail cùng một lõi vận hành.", icon: Store },
{ title: "AI vận hành", desc: "Chat trợ lý, gợi ý tồn kho, phân tích doanh thu và workflow marketing.", icon: Bot },
{ title: "Loyalty & CRM", desc: "Tích điểm, ví thành viên, voucher, chiến dịch và chăm sóc khách hàng.", icon: Heart },
{ title: "Báo cáo realtime", desc: "Doanh thu, payment ledger, EOD, top products và sức khỏe nền tảng.", icon: BarChart3 }
];
const plans = [
{ name: "Starter", price: "Miễn phí", desc: "Một cửa hàng, POS cơ bản, menu QR.", featured: false },
{ name: "Growth", price: "299K", desc: "Đa ngành, inventory, staff, loyalty.", featured: true },
{ name: "Scale", price: "Liên hệ", desc: "Nhiều chi nhánh, marketing, AI và API.", featured: false }
];
export function TposPublicLanding({ variant = "home" }: { variant?: "home" | "project" }) {
const isProject = variant === "project";
return (
<main className="public-shell">
<PublicNav />
<section className={isProject ? "landing-hero landing-hero--project" : "landing-hero"}>
<div className="landing-terminal-preview" aria-hidden="true">
<div className="terminal-bar">
<span>aPOS POS</span>
<b>Online</b>
</div>
<div className="terminal-body">
<div className="terminal-sidebar" />
<div className="terminal-grid">
<i />
<i />
<i />
<i />
</div>
<div className="terminal-order">
<span>Đơn hàng</span>
<b>245.000 đ</b>
</div>
</div>
</div>
<div className="home-hero__badge">{isProject ? "GoodGo TPOS MVP" : "AI-Powered POS & Loyalty"}</div>
<h1 className="home-hero__title">
{isProject ? "Giới thiệu dự án TPOS Next MVP" : "Quản lý bán hàng thông minh với AI"}
</h1>
<p className="home-hero__subtitle">
{isProject
? "Bản Next MVP tái hiện web-client-tpos-net bằng kiến trúc BFF, database nội bộ và giao diện public/auth/portal/POS đồng bộ."
: "Giải pháp POS toàn diện cho Nhà hàng, Cafe, Bar, Karaoke và Spa - tự động hóa kế toán, tích điểm khách hàng và báo cáo realtime."}
</p>
<div className="home-hero__actions">
<Link href="/register" className="home-hero__btn home-hero__btn--primary">
<Zap size={18} />
Dùng thử miễn phí
</Link>
<Link href="/auth/login" className="home-hero__btn home-hero__btn--secondary">
<LogIn size={18} />
Đăng nhập
</Link>
</div>
<div className="home-trust">
<span className="home-trust__label">Hơn 2,000 doanh nghiệp đã tin tưởng sử dụng</span>
<div className="home-trust__stats">
<span className="home-trust__stat">50,000+ giao dịch/ngày</span>
<span className="home-trust__divider"></span>
<span className="home-trust__stat">99.9% uptime</span>
</div>
</div>
</section>
<section className="home-verticals" id="features">
<div className="home-verticals__grid">
{verticals.map(({ label, icon: Icon }) => (
<Link href="/register" className="home-vertical-card" key={label}>
<Icon size={28} />
<span>{label}</span>
</Link>
))}
</div>
</section>
<section className="tpos-section">
<div className="tpos-section-header">
<span className="home-hero__badge">TPOS Modules</span>
<h2 className="tpos-section-title">Một nền tảng cho toàn bộ vòng đi bán hàng</h2>
<p className="tpos-section-desc">Từ QR menu, POS terminal, bếp/barista, inventory, staff đến marketing CRM superadmin.</p>
</div>
<div className="tpos-feature-grid">
{features.map(({ title, desc, icon: Icon }) => (
<article className="tpos-feature-card" key={title}>
<span className="tpos-feature-icon"><Icon size={22} /></span>
<h3>{title}</h3>
<p>{desc}</p>
</article>
))}
</div>
</section>
<section className="project-intro" id="project">
<div>
<span className="home-hero__badge">Project Architecture</span>
<h2>Next MVP chạy đc lập, lấy web-client-tpos-net làm chuẩn giao diện</h2>
<p>
MVP dùng Next App Router, BFF route surface, httpOnly session PostgreSQL schema nội bộ.
Các microservice .NET hiện reference contract, không phải dependency runtime.
</p>
</div>
<div className="project-stack">
{["Landing & Auth", "Role Portals", "POS Verticals", "BFF + MVP DB"].map((item) => (
<div key={item}>
<Check size={16} />
<span>{item}</span>
</div>
))}
</div>
</section>
<section className="tpos-section" id="pricing">
<div className="tpos-section-header">
<span className="home-hero__badge">Plans</span>
<h2 className="tpos-section-title">Gói triển khai theo quy vận hành</h2>
</div>
<div className="tpos-pricing-grid">
{plans.map((plan) => (
<article key={plan.name} className={plan.featured ? "tpos-pricing-card featured" : "tpos-pricing-card"}>
{plan.featured ? <span className="tpos-pricing-badge">Khuyến nghị</span> : null}
<h3>{plan.name}</h3>
<strong>{plan.price}</strong>
<p>{plan.desc}</p>
<Link href="/register">Bắt đu <ArrowRight size={15} /></Link>
</article>
))}
</div>
</section>
<section className="landing-cta" id="contact">
<ShieldCheck size={26} />
<h2>Sẵn sàng mở portal vận hành?</h2>
<p>Chọn vai trò đăng nhập hoặc tạo merchant mới đ tiếp tục setup cửa hàng MVP.</p>
<div>
<Link href="/auth/login" className="home-hero__btn home-hero__btn--secondary">Portal đăng nhập</Link>
<Link href="/admin" className="home-hero__btn home-hero__btn--primary">Vào admin demo</Link>
</div>
</section>
</main>
);
}
function PublicNav() {
return (
<nav className="tpos-navbar">
<div className="tpos-navbar-inner">
<Link href="/" className="tpos-logo">aPOS</Link>
<div className="tpos-nav-links">
<Link href="/#features" className="tpos-nav-link">Tính năng</Link>
<Link href="/about" className="tpos-nav-link">Giới thiệu</Link>
<Link href="/#pricing" className="tpos-nav-link">Bảng giá</Link>
<Link href="/#contact" className="tpos-nav-link">Liên hệ</Link>
<Link href="/login" className="tpos-nav-link">Đăng nhập</Link>
<Link href="/register" className="btn-accent">Dùng thử miễn phí</Link>
</div>
</div>
</nav>
);
}