PINECOCO / BUILD WITH INTENTION

Một ngôn ngữ thiết kế.
Mọi hành trình kết nối.

Hệ thống thiết kế PineCoco: màu sắc có mục đích, thành phần nhất quán và trải nghiệm được xây từ chính token của sản phẩm.

Lime / Ink / Canvas Token Component Screen Sáng & tối · WCAG AA

01 / FOUNDATIONS

Rõ ràng từ nền tảng.

Lime tạo điểm nhấn. Ink giúp dễ đọc.
Canvas xám dịu dành không gian cho nội dung.

Có định hướng

Mỗi màn hình có một hành động chính màu Lime. Thứ bậc rõ ràng từ tiêu đề đến nội dung phụ.

Nhất quán

Dùng token ngữ nghĩa (bg-primary, text-muted-foreground) thay vì mã màu riêng cho từng component.

Dễ tiếp cận

Chữ đạt tương phản 4.5:1, trạng thái có cả icon và nhãn, vùng chạm tối thiểu 44 px, focus luôn nhìn thấy.

Màu cốt lõi

Sáng / Tối · nguồn: app/globals.css, tailwind.config.ts
Sáng#C9F158
Tối#C9F158

Lime

Hành động chính, trạng thái đang chọn và focus ring. Luôn đặt chữ Ink lên trên.

CSS
--primary
Tailwind
bg-primary
Public
--pc-accent

Ink trên Lime · 12.6:1 · AAA

Sáng#B5D94E
Tối#B5D94E

Lime đậm

Trạng thái hover và pressed của nút Lime.

CSS
Chỉ có trong Tailwind
Tailwind
bg-lime-600
Public
--pc-accent-hover

Ink trên Lime đậm · 10.1:1 · AAA

Sáng#202020
Tối#EDEDED

Ink

Tiêu đề, nội dung chính và nút tương phản cao.

CSS
--foreground
Tailwind
text-foreground
Public
--pc-ink

Ink trên Canvas · 14.7:1 · AAA

Sáng#F2F3F5
Tối#171717

Canvas

Nền trang. Cũng là --secondary và --muted cho vùng nền phụ.

CSS
--background
Tailwind
bg-background
Public
--pc-page

Ink trên Canvas (tối) · 15.3:1 · AAA

Sáng#FFFFFF
Tối#242424

Surface

Thẻ, popover, sheet và ô nhập liệu.

CSS
--card
Tailwind
bg-card
Public
--pc-surface

Ink trên Surface · 16.3:1 · AAA

Sáng#737373
Tối#909090

Muted

Chữ phụ, chú thích, placeholder.

CSS
--muted-foreground
Tailwind
text-muted-foreground
Public
--pc-muted = #626871

Trang public đặt chữ phụ cỡ nhỏ trên Canvas nên dùng sắc đậm hơn để đạt 4.5:1.

Muted trên Surface · 4.7:1 · AA

Sáng#E6E6E6
Tối#363636

Border

Đường viền thẻ, ô nhập liệu và đường phân tách.

CSS
--border
Tailwind
border-border
Public
--pc-border

Viền trang trí, không mang nghĩa. Ô nhập liệu luôn có nhãn hiển thị.

Sáng#EF4444
Tối#DD4B4B

Destructive

Nền của thao tác xóa, rời chuyến đi. Chữ lỗi dùng Danger.

CSS
--destructive
Tailwind
bg-destructive
Public
--pc-destructive

Chữ trắng trên Destructive · 3.8:1 · Chỉ chữ lớn

Màu trạng thái

Chữ Tailwind 700 trên nền 50
Thành côngThao tác đã hoàn tấttext-emerald-700 bg-emerald-50--pc-success · 5.2:1
Cần chú ýKiểm tra trước khi tiếp tụctext-amber-700 bg-amber-50--pc-warning · 4.8:1
Có lỗiNói rõ lỗi và cách xử lýtext-red-700 bg-red-50--pc-danger · 5.9:1
Thông tinNgữ cảnh hỗ trợ quyết địnhtext-blue-700 bg-blue-50--pc-info · 6.2:1

Lime luôn đi với chữ Ink. Không dùng text-primary cho chữ trên nền sáng: Lime trên trắng chỉ đạt 1.3:1. Trạng thái luôn kết hợp icon và nhãn, không chỉ dựa vào màu. Trang public hiện chỉ dùng chế độ sáng; ứng dụng hỗ trợ cả hai qua lớp .dark.

TYPOGRAPHY / MANROPE

Đi cùng nhau.
Nhớ lâu hơn.

Manrope qua next/font, biến --font-manrope. Hỗ trợ tiếng Việt đầy đủ, tiêu đề gọn và ưu tiên khả năng đọc.

Display
36–72 px · 700 · text-4xl font-bold
Heading
20–24 px · 700 · text-xl font-bold
Body
14–16 px · 400–500 · text-sm / text-base
Caption
12 px · 500 · text-xs
Label
10–11 px · 700 · in hoa · text-[10px] tracking-widest

GEOMETRY / NHỊP 4 PX

Khoảng thở có chủ đích.

14
28
312
416
624
832
1248
1664
rounded-lgÔ nhập, nút nhỏ
rounded-xlÔ icon, chip
rounded-2xlThẻ
rounded-3xlSheet, khối lớn
rounded-fullPill, avatar, nút tròn

Spacing theo thang Tailwind: 1 bước = 4 px. Bo góc lấy từ tailwind.config.ts; thẻ dùng rounded-2xl, nút hành động dùng rounded-full.

Icon / Lucide React

16 · 20 · 24 px · stroke 1.75–2 · SVG
MapPin
CalendarDays
Wallet
Users
ScanLine
Plane
Search
Plus
Bell
Settings
ShieldCheck
ArrowUpRight

Import theo tên từ lucide-react. Icon trang trí có aria-hidden; nút chỉ chứa icon phải có aria-label. Không dùng emoji hay ký tự mũi tên làm icon giao diện.

02 / COMPONENTS

Nhỏ gọn. Dùng lại được.

Thử trạng thái trực tiếp.
Các ví dụ này không tạo dữ liệu chuyến đi.

Hành động & trạng thái

Trạng thái ví dụ: sẵn sàng.

Dùng token

// Trong ứng dụng: class Tailwind gắn với token
<button className="bg-primary text-primary-foreground rounded-full">
  Thêm khoản chi
</button>
<div className="bg-card border border-border rounded-2xl p-4">
  <p className="text-muted-foreground text-sm">Chữ phụ</p>
</div>
/* styles/marketing-tokens.css */
--pc-accent: #c9f158;
--pc-accent-hover: #b5d94e;
--pc-ink: #202020;
--pc-page: #f2f3f5;
--pc-surface: #ffffff;
--pc-muted: #626871;
--pc-border: #e6e6e6;
--pc-destructive: #ef4444;
--pc-success: #047857;
--pc-success-soft: #ecfdf5;
--pc-warning: #b45309;
--pc-warning-soft: #fffbeb;
--pc-danger: #b91c1c;
--pc-danger-soft: #fef2f2;
--pc-info: #1d4ed8;
--pc-info-soft: #eff6ff;

Nút sao chép lấy khối --pc-* dành cho trang public.

03 / PRODUCT IN PRACTICE

Từ hệ thống đến màn hình.

Ảnh chụp trực tiếp ứng dụng hiện tại.
Viewport 440 × 956 · Chế độ khách.

Chụp từ bản web responsive trên Chromium ở chế độ khách với một chuyến đi minh họa; đây không phải ảnh chụp từ thiết bị iOS vật lý.

04 / DEVELOPMENT

Hiểu hệ thống trước khi tích hợp.

Xác thực theo phiên

PineCoco dùng phiên NextAuth hoặc phiên khách có chữ ký do máy chủ cấp. Mỗi endpoint kiểm tra quyền truy cập tương ứng.

Tài liệu bên dưới là tham khảo API nội bộ. Đối chiếu route handler trước khi tích hợp; đây chưa phải hợp đồng API công khai cho bên thứ ba.

Mở ứng dụng

Đọc danh sách chuyến đi

const response = await fetch('/api/v3/trips/list', {
  credentials: 'same-origin',
  headers: { Accept: 'application/json' },
});
if (!response.ok) {
  throw new Error('HTTP ' + response.status);
}
const trips = await response.json();

Chạy trong ứng dụng sau khi có phiên đăng nhập hợp lệ.

05 / API REFERENCE

Tìm đúng endpoint.

API nội bộ · JSON · Quyền theo phiên

9 endpoint phù hợp.

POST/api/v1/auth/send-otpGửi mã OTP 6 chữ số đến email

Quyền truy cập: none

Rate limited: 5 req/min per email

Request body

email
string — địa chỉ email hợp lệ

Response

success
boolean
message
string
POST/api/v1/auth/verify-otpXác thực mã OTP, nhận token một lần

Quyền truy cập: none

Request body

email
string
code
string — 6 digits

Response

verified
boolean
tempToken
string — JWT 5 phút, dùng cho /signup
POST/api/v1/auth/signupTạo tài khoản mới sau khi verify OTP

Quyền truy cập: none

Request body

email
string
name
string
passcode
string — 4 digits

Response

user
{ id, email, name, role }
session
NextAuth session object
POST/api/v1/auth/signinĐăng nhập bằng email + 4-digit passcode

Quyền truy cập: none

Request body

email
string
passcode
string — 4 digits

Response

user
{ id, email, name, role }
token
JWT session token
POST/api/v1/uploadUpload ảnh hoặc video lên S3 storage

Quyền truy cập: user

multipart/form-data · max 10 MB

Request body

file
File
type
'image' | 'video' — default 'image'

Response

url
string — CDN URL (WebP/AVIF auto)
publicId
string — object key
POST/api/v1/upload-avatarUpload avatar, tự động crop và resize

Quyền truy cập: user

Request body

file
File — image only

Response

avatarUrl
string — 256×256 CDN URL
publicId
string
POST/api/v1/scan-billĐọc hóa đơn: danh sách món, phí, thuế và tổng tiền (Gemini Flash-Lite, ~2-3 giây)

Quyền truy cập: user

Chỉ tài khoản Pro · multipart/form-data hoặc JSON {text} cho OCR chạy trên thiết bị

Request body

image
File — JPG/PNG/WEBP, max 4 MB (app thu nhỏ còn ≤1600 px, ~250 KB)
text
string? — thay cho image: chữ đã nhận dạng trên máy (Apple Vision / ML Kit), max 12.000 ký tự

Response

receipt
{ merchant, date 'YYYY-MM-DD'|'', currency ISO|'', items[{ name, quantity, amount }], serviceCharge, tax, tip, discount, total }
empty
boolean — ảnh không có hóa đơn
meta
{ model, ms }
POST/api/v1/scan-ticketĐọc vé máy bay (Pro): ảnh boarding pass hoặc chữ đã đọc sẵn trên máy

Quyền truy cập: user

Lỗi: 403 PRO_REQUIRED · 429 AI_QUOTA (hết lượt trong tháng, details.resetsAt) · 503 AI_PAUSED · 503 SCAN_BUSY · 504 SCAN_TIMEOUT

Request body

imageBase64
string? — JPEG/PNG/WebP đã thu nhỏ (cạnh dài ≤ 1600 px), tối đa 6 MB base64
mimeType
string? — mặc định image/jpeg
text
string? — chữ đọc trên máy (Apple Vision), tối đa 8000 ký tự; rẻ hơn ảnh nhiều. Gửi imageBase64 hoặc text

Response

passenger
string
fromCode
string — IATA 3 chữ
toCode
string — IATA 3 chữ
flightNumber
string
departureTime
string — ISO 8601, giờ địa phương
arrivalTime
string — ISO 8601
seat
string?
bookingRef
string?
...
terminal, class, duration, baggage, operatedBy, ticketNo
GET/api/v1/mapbox/searchAutocomplete địa điểm (Mapbox Geocoding, fallback OSM Nominatim)

Quyền truy cập: user

Query parameters

query
string — 2..100 ký tự
lat
number? — đi kèm lng, dùng ưu tiên kết quả gần
lng
number? — đi kèm lat
limit
number — default 10, max 10

Response

[]
LocationSuggestion[] — { id, name, description, placeId, coordinates: [lng, lat] }