Refactor TPOS shop and POS routes for parity
This commit is contained in:
5
microservices/apps/tpos-mvp-next/src/app/about/page.tsx
Normal file
5
microservices/apps/tpos-mvp-next/src/app/about/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { TposPublicLanding } from "@/components/TposPublicLanding";
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return <TposPublicLanding variant="project" />;
|
||||||
|
}
|
||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "../about/page";
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>Mã đơ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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "../about/page";
|
||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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 và 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 đã có 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 mô hình BFF session của TPOS: cookie httpOnly, role portal và 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 có tài khoản? <Link href="/register">Đăng ký ngay</Link>
|
||||||
|
</p>
|
||||||
|
<Link href="/auth/login/superadmin" className="login-portal__super">
|
||||||
|
<ShieldCheck size={14} />
|
||||||
|
Super Admin
|
||||||
|
</Link>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 và 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 và PostgreSQL schema nội bộ.
|
||||||
|
Các microservice .NET hiện có là 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 mô 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user