init: OBDX web landing (Bento Garage design)

- Vite + React + TS + Tailwind v4 + framer-motion + lucide
- 5 sections: Hero, Showcase (steps + 3 cases), Pricing, Comparison, Footer
- Brand assets local (logo v2 SVG, 3 mascots, 6 scenes) under public/brand/
- Dockerfile multi-stage (node 20 build → nginx 1.27 alpine)
- nginx /api/* reverse-proxy to obdx-api:8080, SPA fallback
- /healthz endpoint for Coolify

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kang
2026-04-23 16:19:47 +08:00
commit 7193eacfa5
44 changed files with 4012 additions and 0 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
node_modules
dist
.dev-screenshot-*.png
.playwright-mcp
.env
.env.*
.DS_Store
*.log

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
node_modules
dist
.env
.env.local
.env.*.local
.DS_Store
.dev-screenshot-*.png
.playwright-mcp
*.log

67
DESIGN-CANDIDATES.md Normal file
View File

@@ -0,0 +1,67 @@
# OBDX Landing — Design Candidates (Phase A2)
4 套风格候选。每套都守 DESIGN-INPUTS.md 里的强约束logo v2 SVG / 3 吉祥物 / 北美英文 / 5 品牌性格轴),软约束(色板/字体/布局)可自由解构。
旧 VI 默认的 **Dark Garage** 用户已反馈"不过关",候选里有意不包含其变体。
---
## 候选 1 — Under the Hood引擎盖下
**调性**:精密机械美学 × 玻璃质感数据层。想象打开引擎盖看金属、活塞、管路,但所有数据漂浮在透明玻璃卡片里。
- **色板**`#0A0A0A` Jet Black / `#2563EB` Cobalt / `#F59E0B` Amber / `#C0C0C0` Titanium金属感
- **字体**Space Grotesk标题几何工程感+ DM Sans + JetBrains Mono
- **布局**:左右不对称,左边暗色金属肌理 + IP 角色场景图右边透明玻璃卡backdrop-blur叠在金属上
- **动效**:玻璃卡 hover 微微倾斜3D tilt数据从 IP 手里"流"到玻璃卡
- **吉祥物**Wrench Uncle 站 Hero 中心Dash 作为小数据粒子飘浮
- **风险**:深色本质没脱开 Dark Garage
## 候选 2 — Bento Garage便当车库✅ **推荐 / 已实现 Hero 原型**
**调性**:浅色 Bento grid × 日系 Kawaii + 工具感。从"车库暗房"反转到"明亮工作台",温暖木质 + 每个信息点独立圆角格子。
- **色板**`#F5F1EA` Warm Beige / `#FAFAF7` Cream / `#2563EB` Cobalt / `#E07856` Terracotta / `#1A1A1A` Ink
- **字体**Outfit + DM Sans + JetBrains MonoVI 继承)
- **布局**12 列 bento grid大格 + 小格不对称堆叠,`rounded-3xl` 柔和圆角
- **动效**每格独立淡入staggerhover 放大轻微 tilt
- **吉祥物**Wrench Uncle 溢出大 Hero 格子右下角Dash 钻进数据格子
- **为什么推荐**
- 彻底反转 Dark Garage浅色 vs 深色)
- Bento 是 2026 主流模式,读起来有节奏
- 浅色系 + 吉祥物最大化 Pixar 温暖 + Apple 质感
- 保留 Electric Cobalt 品牌色做点缀
- 对比旧 HTML 的临时蓝色调差异化极强
## 候选 3 — Diagnostic Runway诊断跑道
**调性**Apple 产品页 × F1 遥测。极简、巨字号、中心对齐,数据如赛车仪表盘实时跳动。
- **色板**`#000000` Racing Black / `#EF4444` Signal Red / `#FFFFFF` / `#4338CA` Deep Indigo
- **字体**Outfit ExtraBold 150px+(超大 Hero 标题)+ Inter + IBM Plex Mono
- **布局**:全宽纵向 runway每屏一个大主题中心对齐大量留白
- **动效**scroll-driven reveal数字 counter 动画,横向滑动展示竞品对比
- **吉祥物****几乎不用**(只 Hero 一尊 Wrench Uncle其他屏纯 typography + 产品 UI 截图)
- **风险**:弱化了 IP 依赖,品牌识别度下降
## 候选 4 — Paper Workshop纸上车间
**调性**:手绘 sketch + 纸肌理 + 工程图纸笔记。像打开老修车师傅的手写笔记本。
- **色板**`#F2EAD3` Aged Paper / `#1A1A1A` Dark Ink / `#2B6CB0` Blueprint Cyan / `#E07856` Caution Amber
- **字体**Instrument Serif手写风标题+ DM Mono正文+ Caveat 手写体点缀
- **布局**:拼贴、不规则、手绘连线连接信息块
- **动效**:笔迹描绘SVG stroke dashoffset、纸张翻页
- **吉祥物**:改造成"纸质风"笔触描边插画
- **风险**:需要把吉祥物 PNG 做二次艺术处理,工作量大
---
## 推荐:**候选 2 — Bento Garage**,已先出 Hero 原型让你看实物再定
Hero 代码在 `web/src/components/Hero.tsx`,用候选 2 色板/字体/布局。
如果 Hero 不对路,换到候选 1/3/4 的主要变更点:
- → 候选 1`index.css` 色板改深色金属Hero 用 `backdrop-blur` 玻璃卡;背景换 IP 场景图
- → 候选 3`index.css` 色板改黑白红Hero 去掉 bento grid 改全宽中心对齐;字号 6xl → 9xl吉祥物用量减到 1 个
- → 候选 4`index.css` 色板改 paper字体换 Instrument Serif加 SVG 手绘描边 texture

157
DESIGN-INPUTS.md Normal file
View File

@@ -0,0 +1,157 @@
# OBDX Landing — Design Inputs
Phase A 设计探索阶段的输入文档。给 `/ui-ux-pro-max --design-system` 用。
---
## 1. 产品一句话
OBDX 是 AI 汽车诊断服务 —— OBD 蓝牙设备插车 → 扫码 → H5 网页看 AI 原创诊断报告(健康分、故障清单、修车预算)。
- **Tagline**Your car, decoded.
- **替代**Scan. Know. Go. / Every car tells a story. / The mechanic in your pocket.
- **市场**北美US/CA英文优先
- **域名**obdx.ai → 当前部署在 obd.kang-kang.com
## 2. 品牌性格5 轴)
| 轴 | 是 | 不是 |
|----|-----|------|
| Approachable | 像朋友解释问题 | 冷冰冰工具 |
| Trustworthy | 专业数据 + 人话 | 花哨噱头 |
| Premium-Playful | Pixar 温暖 + Apple 质感 | 幼稚或套路 |
| Smart | 懂车又懂人 | 技术炫耀 |
| Modern | 干净极简有设计感 | 传统汽修油腻感 |
**对标**
- 工具品牌Milwaukee、Dyson
- 现代 SaaSLinear、Vercel、Stripe
- 叙事Pixar
**设计哲学**(官方 VI 已定):**Dark Garage × Digital Precision**。可以继承也可以在 Phase A2 挑战 —— 用户已经对这版感觉"不过关",欢迎出截然不同方向的候选。
## 3. 色板VI 已定 — 可作为参考或被覆盖)
### 品牌主色
| 角色 | 名字 | Hex |
|------|------|-----|
| 主 | Electric Cobalt | `#2563EB` |
| 副 | Warm Amber | `#F59E0B` |
| 深底 | Charcoal Black | `#0F172A` |
| 浅底 | Cool White | `#F8FAFC` |
### 语义
| 角色 | Hex |
|------|-----|
| Healthy | Emerald `#10B981` |
| Caution | Warm Amber `#F59E0B` |
| Danger | Signal Red `#EF4444` |
| Info | Slate Blue `#64748B` |
| Premium | Deep Indigo `#4338CA` |
### 扩展
`#0F172A` Deep Night / `#1E293B` Dark Slate / `#334155` Medium Slate / `#94A3B8` Muted Silver / `#E2E8F0` Light Border / `#F1F5F9` Light Gray
## 4. 字体VI 已定)
| 角色 | 字体 | 用途 |
|------|------|------|
| 标题 | **Outfit** | Hero / Section headings |
| 正文 | **DM Sans** | 段落、列表 |
| 数据 | **JetBrains Mono** | 数字、代码、技术标注 |
替代候选(如果 A2 另选Space Grotesk / Inter / IBM Plex Sans。
## 5. 物料清单(所有路径相对项目根)
### Logo
| 文件 | 路径 |
|------|------|
| Icon PNG老版 | `brand-assets/obdx-brand/logo/obdx-logo-icon.png` |
| Primary Dark PNG | `brand-assets/obdx-brand/logo/obdx-logo-primary-dark.png` |
| Primary Light PNG | `brand-assets/obdx-brand/logo/obdx-logo-primary-light.png` |
| Tagline Dark PNG | `brand-assets/obdx-brand/logo/obdx-logo-tagline-dark.png` |
| **Icon SVG v2新版** | `brand-assets/obdx-brand/logo-redesign/obdx-logo-icon-v2.svg` |
| **Primary Dark SVG v2** | `brand-assets/obdx-brand/logo-redesign/obdx-logo-primary-dark-v2.svg` |
| **Primary Light SVG v2** | `brand-assets/obdx-brand/logo-redesign/obdx-logo-primary-light-v2.svg` |
| **Tagline SVG v2** | `brand-assets/obdx-brand/logo-redesign/obdx-logo-tagline-dark-v2.svg` |
推荐用 **SVG v2**
### IP 角色3 只)
| 角色 | 定位 | 资源 |
|------|------|------|
| **Wrench Uncle**(扳手大叔) | 老练修车师傅 IP品牌门面 | `brand-assets/obdx-brand/ip-characters/wrench-uncle/{default, expressions, poses, with-dash}.png` |
| **Wrench** | 年轻助手 / 工具人 | `brand-assets/obdx-brand/ip-characters/wrench/{default, expressions, poses}.png` |
| **Dash**(仪表盘) | 车机化身,传递数据感 | `brand-assets/obdx-brand/ip-characters/dash/{default, expressions}.png` |
### 场景图6 张 — 16:9 + 方形各 3 幕)
| 幕 | 含义 | 16:9 | 方形 |
|----|------|------|------|
| Ride | 开车中,设备亮起 | `obdx-scene-ride-16x9.png` | `obdx-scene-ride-square.png` |
| Checkup | 扫描诊断中 | `obdx-scene-checkup-16x9.png` | `obdx-scene-checkup-square.png` |
| Done | 报告完成,健康分 | `obdx-scene-done-16x9.png` | `obdx-scene-done-square.png` |
路径前缀:`brand-assets/obdx-brand/scenes/`
## 6. 内容素材(已有可复用)
`brand-assets/constants.ts` 搬:
- **PRICING_PLANS**Free $0 / Plus $4.99/月 / Shop $19.99/月3 档)
- **STATS**706GB / 82 brands / 24,935 models / 10 sec
- **COMPETITORS**FIXD / BlueDriver / CarMD / Innova / Snap-on 对比表
- **NAV_LINKS**Features / How It Works / Pricing / Compare
## 7. Landing 必备 Section建议
参考 `brand-assets/Home.tsx` 结构(旧方案逻辑合理,视觉可全新):
1. **Navbar** — logo + 导航 + CTA
2. **Hero** — slogan + 副文案 + CTA + 吉祥物/场景图
3. **Stats** — 4 个数字卡706GB / 82 / 24,935 / 10s
4. **Problem** — 用户 5 大痛点(故障灯、被宰、二手车、工具贵、竞品麻烦)
5. **HowItWorks** — 3 步流程Plug / Scan / Read
6. **Features** — AI 诊断 / 车型专属 / 修车预算 / 分享报告
7. **Comparison** — OBDX vs FIXD/BlueDriver/CarMD/Innova/Snap-on
8. **Pricing** — 3 档卡片
9. **Testimonials** — 推荐语(如有)
10. **DownloadCTA** — 下载引导
11. **Footer**
> 范围选 **A**(只重做 landing功能页 scan/demo/report 不动。
## 8. 历史探索(`brand-assets/ideas.md` 3 方案)
1. **暗夜车库 Dark Garage Aesthetic** — VI 已选,用户反馈"不过关"
2. **蓝图解构 Blueprint Deconstruction** — 工程蓝图美学,网格 + 标注线
3. **皮克斯叙事 Pixar Storytelling** — 叙事驱动,角色中心
Phase A2 候选**需要至少有 1 套是"非暗夜车库变体"**,否则等于没探索。
## 9. 约束(强 vs 软)
### 强约束(不可违反)
- 保留 logo推荐 SVG v2
- 保留 3 只吉祥物 IP 形象Wrench Uncle / Wrench / Dash— 可以只选其一重点用
- 北美市场英文 copy
- 保留上述 5 个品牌性格轴Pixar 温暖 + Apple 质感这句是品牌基因)
### 软约束(可调整)
- 色板VI 那套可替换,但要给得出新色板的叙事理由)
- 字体VI 那套可替换)
- 布局 / 网格 / 动效 / 叙事方式
- 深浅模式默认值VI 默认 dark可改
### 零约束
- 现有 `brand-assets/*.tsx` 12 个组件(**作废**,不作为实现参考)
## 10. 部署约束
- 目标域名 `obd.kang-kang.com`(先用 `obd-new.kang-kang.com` 验证再切)
- Coolify 反代 `*.kang-kang.com``76.13.31.179`
- 前端容器 nginx反代 `/api/*` 到 FastAPI 后端容器(**1c 同域架构**,前端不触发 CORS
- 技术栈Vite + React + TS + Tailwind v4 + framer-motion + lucide-react**不含** shadcn/ui / wouter / sonner
- 资产本地化:所有图片从 `web/public/brand/` 提供,**不走 CloudFront**

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine AS runtime
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/brand/logo/obdx-logo-icon-v2.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="OBDX — Plug in a $10 OBD scanner, scan a QR code, get an AI-written repair report in plain English. 706GB knowledge base, 82 brands, 24,935 vehicle models." />
<title>OBDX — Your car, decoded.</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

52
nginx.conf Normal file
View File

@@ -0,0 +1,52 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
# Hash-named assets (Vite emits hashed filenames in /assets/) — long cache
location ~* ^/assets/.*\.(js|css|woff2?|ttf|otf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Brand images — versioned via filename, long cache
location ~* ^/brand/.*\.(png|jpg|jpeg|svg|webp|gif|ico)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
try_files $uri =404;
}
# Reverse proxy /api/* to FastAPI backend container.
# In Coolify the backend service hostname is the application name on the
# internal network. Override OBDX_API_UPSTREAM at deploy time if it differs.
location /api/ {
proxy_pass http://obdx-api:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_connect_timeout 5s;
}
# SPA fallback — every non-asset path returns index.html so client routes work
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
# Healthcheck for Coolify / load balancer
location = /healthz {
access_log off;
return 200 'ok';
add_header Content-Type text/plain;
}
}

2395
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "obdx-web",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^11.11.17",
"lucide-react": "^0.460.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"tailwindcss": "^4.0.0",
"typescript": "^5.6.3",
"vite": "^5.4.11"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

View File

@@ -0,0 +1,52 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="signalPortFill" x1="84" y1="96" x2="420" y2="420" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3B82F6"/>
<stop offset="0.58" stop-color="#2563EB"/>
<stop offset="1" stop-color="#4338CA"/>
</linearGradient>
<linearGradient id="signalPortGlow" x1="164" y1="160" x2="356" y2="300" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.34"/>
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
</linearGradient>
</defs>
<path
d="M158 92H354C404 92 444 132 444 182V250C444 292 416 328 376 340L344 350V410C344 444 316 472 282 472H230C196 472 168 444 168 410V350L136 340C96 328 68 292 68 250V182C68 132 108 92 158 92ZM150 170C118 170 92 196 92 228C92 260 118 286 150 286H362C394 286 420 260 420 228C420 196 394 170 362 170H150Z"
fill="url(#signalPortFill)"
fill-rule="evenodd"
clip-rule="evenodd"
/>
<path
d="M174 122H338C379 122 412 155 412 196V216C412 175 379 142 338 142H174C133 142 100 175 100 216V196C100 155 133 122 174 122Z"
fill="url(#signalPortGlow)"
/>
<circle cx="256" cy="228" r="46" fill="#F59E0B"/>
<circle cx="256" cy="228" r="18" fill="#0F172A"/>
<circle cx="242" cy="212" r="8" fill="#F8FAFC" fill-opacity="0.9"/>
<path
d="M178 392C178 379.85 187.85 370 200 370H216C228.15 370 238 379.85 238 392V420C238 432.15 228.15 442 216 442H200C187.85 442 178 432.15 178 420V392Z"
fill="#0F172A"
fill-opacity="0.92"
/>
<path
d="M234 384C234 371.85 243.85 362 256 362C268.15 362 278 371.85 278 384V428C278 440.15 268.15 450 256 450C243.85 450 234 440.15 234 428V384Z"
fill="#F59E0B"
/>
<path
d="M274 392C274 379.85 283.85 370 296 370H312C324.15 370 334 379.85 334 392V420C334 432.15 324.15 442 312 442H296C283.85 442 274 432.15 274 420V392Z"
fill="#0F172A"
fill-opacity="0.92"
/>
<path
d="M150 228H362"
stroke="#F8FAFC"
stroke-opacity="0.28"
stroke-width="8"
stroke-linecap="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

@@ -0,0 +1,53 @@
<svg width="1600" height="480" viewBox="0 0 1600 480" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="signalPortFill" x1="84" y1="96" x2="420" y2="420" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3B82F6"/>
<stop offset="0.58" stop-color="#2563EB"/>
<stop offset="1" stop-color="#4338CA"/>
</linearGradient>
<linearGradient id="signalPortGlow" x1="164" y1="160" x2="356" y2="300" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.34"/>
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="markX" x1="0" y1="0" x2="276" y2="276" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#F8FAFC"/>
<stop offset="0.28" stop-color="#F59E0B"/>
<stop offset="1" stop-color="#2563EB"/>
</linearGradient>
</defs>
<rect width="1600" height="480" fill="#0F172A" fill-opacity="0"/>
<g transform="translate(28 0)">
<path
d="M158 92H354C404 92 444 132 444 182V250C444 292 416 328 376 340L344 350V410C344 444 316 472 282 472H230C196 472 168 444 168 410V350L136 340C96 328 68 292 68 250V182C68 132 108 92 158 92ZM150 170C118 170 92 196 92 228C92 260 118 286 150 286H362C394 286 420 260 420 228C420 196 394 170 362 170H150Z"
fill="url(#signalPortFill)"
fill-rule="evenodd"
clip-rule="evenodd"
/>
<path
d="M174 122H338C379 122 412 155 412 196V216C412 175 379 142 338 142H174C133 142 100 175 100 216V196C100 155 133 122 174 122Z"
fill="url(#signalPortGlow)"
/>
<circle cx="256" cy="228" r="46" fill="#F59E0B"/>
<circle cx="256" cy="228" r="18" fill="#0F172A"/>
<circle cx="242" cy="212" r="8" fill="#F8FAFC" fill-opacity="0.9"/>
<path d="M178 392C178 379.85 187.85 370 200 370H216C228.15 370 238 379.85 238 392V420C238 432.15 228.15 442 216 442H200C187.85 442 178 432.15 178 420V392Z" fill="#0F172A" fill-opacity="0.92"/>
<path d="M234 384C234 371.85 243.85 362 256 362C268.15 362 278 371.85 278 384V428C278 440.15 268.15 450 256 450C243.85 450 234 440.15 234 428V384Z" fill="#F59E0B"/>
<path d="M274 392C274 379.85 283.85 370 296 370H312C324.15 370 334 379.85 334 392V420C334 432.15 324.15 442 312 442H296C283.85 442 274 432.15 274 420V392Z" fill="#0F172A" fill-opacity="0.92"/>
</g>
<text
x="560"
y="274"
fill="#F8FAFC"
font-family="Outfit, Avenir Next, Segoe UI, sans-serif"
font-size="206"
font-weight="800"
letter-spacing="-11"
>OBD</text>
<g transform="translate(1110 110)">
<path d="M0 0H58L136 96L214 0H272L166 130L278 276H220L136 172L52 276H0L108 130L0 0Z" fill="url(#markX)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

@@ -0,0 +1,54 @@
<svg width="1600" height="480" viewBox="0 0 1600 480" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="signalPortFill" x1="84" y1="96" x2="420" y2="420" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3B82F6"/>
<stop offset="0.58" stop-color="#2563EB"/>
<stop offset="1" stop-color="#4338CA"/>
</linearGradient>
<linearGradient id="signalPortGlow" x1="164" y1="160" x2="356" y2="300" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.34"/>
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="markX" x1="0" y1="0" x2="276" y2="276" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#0F172A"/>
<stop offset="0.22" stop-color="#2563EB"/>
<stop offset="0.55" stop-color="#F59E0B"/>
<stop offset="1" stop-color="#4338CA"/>
</linearGradient>
</defs>
<rect width="1600" height="480" fill="#F8FAFC" fill-opacity="0"/>
<g transform="translate(28 0)">
<path
d="M158 92H354C404 92 444 132 444 182V250C444 292 416 328 376 340L344 350V410C344 444 316 472 282 472H230C196 472 168 444 168 410V350L136 340C96 328 68 292 68 250V182C68 132 108 92 158 92ZM150 170C118 170 92 196 92 228C92 260 118 286 150 286H362C394 286 420 260 420 228C420 196 394 170 362 170H150Z"
fill="url(#signalPortFill)"
fill-rule="evenodd"
clip-rule="evenodd"
/>
<path
d="M174 122H338C379 122 412 155 412 196V216C412 175 379 142 338 142H174C133 142 100 175 100 216V196C100 155 133 122 174 122Z"
fill="url(#signalPortGlow)"
/>
<circle cx="256" cy="228" r="46" fill="#F59E0B"/>
<circle cx="256" cy="228" r="18" fill="#0F172A"/>
<circle cx="242" cy="212" r="8" fill="#F8FAFC" fill-opacity="0.9"/>
<path d="M178 392C178 379.85 187.85 370 200 370H216C228.15 370 238 379.85 238 392V420C238 432.15 228.15 442 216 442H200C187.85 442 178 432.15 178 420V392Z" fill="#0F172A" fill-opacity="0.92"/>
<path d="M234 384C234 371.85 243.85 362 256 362C268.15 362 278 371.85 278 384V428C278 440.15 268.15 450 256 450C243.85 450 234 440.15 234 428V384Z" fill="#F59E0B"/>
<path d="M274 392C274 379.85 283.85 370 296 370H312C324.15 370 334 379.85 334 392V420C334 432.15 324.15 442 312 442H296C283.85 442 274 432.15 274 420V392Z" fill="#0F172A" fill-opacity="0.92"/>
</g>
<text
x="560"
y="274"
fill="#0F172A"
font-family="Outfit, Avenir Next, Segoe UI, sans-serif"
font-size="206"
font-weight="800"
letter-spacing="-11"
>OBD</text>
<g transform="translate(1110 110)">
<path d="M0 0H58L136 96L214 0H272L166 130L278 276H220L136 172L52 276H0L108 130L0 0Z" fill="url(#markX)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View File

@@ -0,0 +1,63 @@
<svg width="1600" height="640" viewBox="0 0 1600 640" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="signalPortFill" x1="84" y1="96" x2="420" y2="420" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3B82F6"/>
<stop offset="0.58" stop-color="#2563EB"/>
<stop offset="1" stop-color="#4338CA"/>
</linearGradient>
<linearGradient id="signalPortGlow" x1="164" y1="160" x2="356" y2="300" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.34"/>
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
</linearGradient>
<linearGradient id="markX" x1="0" y1="0" x2="276" y2="276" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#F8FAFC"/>
<stop offset="0.28" stop-color="#F59E0B"/>
<stop offset="1" stop-color="#2563EB"/>
</linearGradient>
</defs>
<rect width="1600" height="640" fill="#0F172A" fill-opacity="0"/>
<g transform="translate(28 16)">
<path
d="M158 92H354C404 92 444 132 444 182V250C444 292 416 328 376 340L344 350V410C344 444 316 472 282 472H230C196 472 168 444 168 410V350L136 340C96 328 68 292 68 250V182C68 132 108 92 158 92ZM150 170C118 170 92 196 92 228C92 260 118 286 150 286H362C394 286 420 260 420 228C420 196 394 170 362 170H150Z"
fill="url(#signalPortFill)"
fill-rule="evenodd"
clip-rule="evenodd"
/>
<path
d="M174 122H338C379 122 412 155 412 196V216C412 175 379 142 338 142H174C133 142 100 175 100 216V196C100 155 133 122 174 122Z"
fill="url(#signalPortGlow)"
/>
<circle cx="256" cy="228" r="46" fill="#F59E0B"/>
<circle cx="256" cy="228" r="18" fill="#0F172A"/>
<circle cx="242" cy="212" r="8" fill="#F8FAFC" fill-opacity="0.9"/>
<path d="M178 392C178 379.85 187.85 370 200 370H216C228.15 370 238 379.85 238 392V420C238 432.15 228.15 442 216 442H200C187.85 442 178 432.15 178 420V392Z" fill="#0F172A" fill-opacity="0.92"/>
<path d="M234 384C234 371.85 243.85 362 256 362C268.15 362 278 371.85 278 384V428C278 440.15 268.15 450 256 450C243.85 450 234 440.15 234 428V384Z" fill="#F59E0B"/>
<path d="M274 392C274 379.85 283.85 370 296 370H312C324.15 370 334 379.85 334 392V420C334 432.15 324.15 442 312 442H296C283.85 442 274 432.15 274 420V392Z" fill="#0F172A" fill-opacity="0.92"/>
</g>
<text
x="560"
y="290"
fill="#F8FAFC"
font-family="Outfit, Avenir Next, Segoe UI, sans-serif"
font-size="206"
font-weight="800"
letter-spacing="-11"
>OBD</text>
<g transform="translate(1110 126)">
<path d="M0 0H58L136 96L214 0H272L166 130L278 276H220L136 172L52 276H0L108 130L0 0Z" fill="url(#markX)"/>
</g>
<text
x="562"
y="404"
fill="#94A3B8"
font-family="JetBrains Mono, SFMono-Regular, Consolas, monospace"
font-size="46"
font-weight="600"
letter-spacing="7"
>YOUR CAR, DECODED.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

17
src/App.tsx Normal file
View File

@@ -0,0 +1,17 @@
import Hero from "@/components/Hero";
import Showcase from "@/components/Showcase";
import Pricing from "@/components/Pricing";
import Comparison from "@/components/Comparison";
import Footer from "@/components/Footer";
export default function App() {
return (
<div className="min-h-screen bg-[#F5F1EA] text-[#1A1A1A] antialiased selection:bg-[#2563EB]/20">
<Hero />
<Showcase />
<Pricing />
<Comparison />
<Footer />
</div>
);
}

View File

@@ -0,0 +1,171 @@
import { motion } from "framer-motion";
import { Check, X, Minus } from "lucide-react";
import { COMPETITORS } from "@/lib/constants";
const COLS = [
{ key: "name", label: "Product" },
{ key: "price", label: "Price" },
{ key: "ai", label: "AI" },
{ key: "plain", label: "Plain English" },
{ key: "advantage", label: "OBDX advantage" },
] as const;
function Cell({ value }: { value: string | boolean }) {
if (value === true) {
return <Check size={18} className="text-[#10B981]" strokeWidth={2.5} />;
}
if (value === false) {
return <X size={18} className="text-[#1A1A1A]/30" strokeWidth={2.5} />;
}
if (value === "Partial" || value === "Basic") {
return (
<span className="inline-flex items-center gap-1 text-xs font-medium text-[#F59E0B]">
<Minus size={14} />
{value}
</span>
);
}
return <span className="text-sm">{value}</span>;
}
export default function Comparison() {
return (
<section
id="compare"
className="px-4 md:px-6 lg:px-8 pb-8 md:pb-12 space-y-4 md:space-y-5"
>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5 }}
className="rounded-[28px] bg-[#1A1A1A] text-white p-8 md:p-12 text-center relative overflow-hidden"
>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#F59E0B]/15 text-[#F59E0B] text-xs font-semibold tracking-wider mb-4">
THE COMPETITION
</span>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-extrabold tracking-tight leading-[1.05]">
$60 scanners.
<br />
<span className="text-white/40">$3,000 scanners.</span>
<br />
<span className="text-[#F59E0B]">Or free.</span>
</h2>
<p className="mt-4 text-base md:text-lg text-white/60 max-w-xl mx-auto">
OBDX uses your existing $10 scanner and beats every paid competitor on
AI depth and clarity.
</p>
</motion.div>
{/* Desktop table */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5 }}
className="hidden md:block rounded-[28px] bg-white border border-black/5 overflow-hidden"
>
<table className="w-full">
<thead>
<tr className="bg-[#FAFAF7] border-b border-black/5">
{COLS.map((col) => (
<th
key={col.key}
className="text-left px-6 py-4 text-xs font-mono text-[#1A1A1A]/50 tracking-wider font-semibold uppercase"
>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{/* OBDX row (highlighted) */}
<tr className="bg-[#2563EB]/5 border-b border-[#2563EB]/15">
<td className="px-6 py-5">
<span className="inline-flex items-center gap-2 font-bold text-[#2563EB] text-base">
OBDX
<span className="px-2 py-0.5 rounded-full bg-[#2563EB] text-white text-[10px] font-semibold">
YOU
</span>
</span>
</td>
<td className="px-6 py-5 font-bold text-[#1A1A1A]">
Free
<span className="text-xs text-[#1A1A1A]/50 font-normal ml-1">
/ Plus $4.99
</span>
</td>
<td className="px-6 py-5">
<Check size={18} className="text-[#10B981]" strokeWidth={2.5} />
</td>
<td className="px-6 py-5">
<Check size={18} className="text-[#10B981]" strokeWidth={2.5} />
</td>
<td className="px-6 py-5 text-sm text-[#1A1A1A]/70">
Vehicle-specific + 706 GB knowledge base
</td>
</tr>
{COMPETITORS.map((c) => (
<tr key={c.name} className="border-b border-black/5 last:border-0">
<td className="px-6 py-4 font-semibold text-[#1A1A1A]">
{c.name}
</td>
<td className="px-6 py-4 text-[#1A1A1A]/75">{c.price}</td>
<td className="px-6 py-4">
<Cell value={c.ai} />
</td>
<td className="px-6 py-4">
<Cell value={c.plain} />
</td>
<td className="px-6 py-4 text-sm text-[#1A1A1A]/55 italic">
{c.advantage}
</td>
</tr>
))}
</tbody>
</table>
</motion.div>
{/* Mobile stacked cards */}
<div className="md:hidden space-y-4">
<div className="rounded-[28px] bg-[#2563EB] text-white p-6">
<div className="flex items-center justify-between mb-2">
<span className="text-xl font-bold">OBDX</span>
<span className="px-2 py-0.5 rounded-full bg-white text-[#2563EB] text-[10px] font-bold">
YOU
</span>
</div>
<div className="text-3xl font-extrabold mb-3">
Free <span className="text-base text-white/70 font-normal">or $4.99/mo</span>
</div>
<div className="text-sm text-white/85">
AI · Plain English · Vehicle-specific + 706 GB knowledge
</div>
</div>
{COMPETITORS.map((c) => (
<div
key={c.name}
className="rounded-[28px] bg-white border border-black/5 p-6"
>
<div className="flex items-center justify-between mb-2">
<span className="text-lg font-bold text-[#1A1A1A]">{c.name}</span>
<span className="text-sm text-[#1A1A1A]/60">{c.price}</span>
</div>
<div className="flex items-center gap-4 text-sm">
<span className="flex items-center gap-1 text-[#1A1A1A]/65">
AI <Cell value={c.ai} />
</span>
<span className="flex items-center gap-1 text-[#1A1A1A]/65">
Plain <Cell value={c.plain} />
</span>
</div>
<div className="text-xs text-[#1A1A1A]/55 italic mt-2">
{c.advantage}
</div>
</div>
))}
</div>
</section>
);
}

75
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,75 @@
import { ASSETS } from "@/lib/constants";
const FOOTER_LINKS = {
Product: [
{ label: "Features", href: "#features" },
{ label: "How it works", href: "#how" },
{ label: "Pricing", href: "#pricing" },
{ label: "Compare", href: "#compare" },
],
Company: [
{ label: "About", href: "#" },
{ label: "Blog", href: "#" },
{ label: "Contact", href: "#" },
],
Legal: [
{ label: "Privacy", href: "#" },
{ label: "Terms", href: "#" },
{ label: "Cookies", href: "#" },
],
} as const;
export default function Footer() {
return (
<footer className="px-4 md:px-6 lg:px-8 pb-6 md:pb-8">
<div className="rounded-[28px] bg-[#1A1A1A] text-white p-8 md:p-12">
<div className="grid grid-cols-2 md:grid-cols-5 gap-8 md:gap-10 mb-10 md:mb-12">
<div className="col-span-2">
<img
src={ASSETS.logoDark}
alt="OBDX"
className="h-7 md:h-8 mb-4 brightness-0 invert"
/>
<p className="text-sm text-white/55 max-w-xs leading-relaxed">
AI car diagnostics for the rest of us. Plug in any $10 OBD
scanner, scan a QR code, get an AI-written repair report in plain
English.
</p>
<p className="text-xs font-mono text-white/30 mt-6">
obdx.ai · Made for North America
</p>
</div>
{Object.entries(FOOTER_LINKS).map(([heading, links]) => (
<div key={heading}>
<h4 className="text-xs font-mono text-white/40 tracking-wider uppercase mb-4">
{heading}
</h4>
<ul className="space-y-3">
{links.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="text-sm text-white/75 hover:text-[#F59E0B] transition"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="pt-6 border-t border-white/10 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<span className="text-xs text-white/40 font-mono">
© 2026 OBDX. Your car, decoded.
</span>
<span className="text-xs text-white/40">
Built with AI on 706 GB of vehicle repair knowledge.
</span>
</div>
</div>
</footer>
);
}

217
src/components/Hero.tsx Normal file
View File

@@ -0,0 +1,217 @@
import { motion } from "framer-motion";
import { ArrowRight, Sparkles, Gauge, Database, Car, Clock } from "lucide-react";
import { ASSETS, NAV_LINKS, STATS } from "@/lib/constants";
export default function Hero() {
return (
<section className="min-h-screen p-4 md:p-6 lg:p-8">
<div className="grid gap-4 md:gap-5 grid-cols-1 md:grid-cols-12 auto-rows-min">
{/* Navbar bento */}
<motion.header
initial={{ opacity: 0, y: -12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="md:col-span-12 flex items-center justify-between px-5 md:px-6 py-3.5 rounded-2xl bg-white/70 backdrop-blur-md border border-black/5 shadow-sm"
>
<img src={ASSETS.logoLight} alt="OBDX" className="h-7 md:h-8" />
<nav className="hidden md:flex gap-8 text-sm font-medium text-[#1A1A1A]/70">
{NAV_LINKS.map((l) => (
<a key={l.href} href={l.href} className="hover:text-[#2563EB] transition">
{l.label}
</a>
))}
</nav>
<a
href="#showcase"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-full bg-[#1A1A1A] text-white text-sm font-semibold hover:bg-[#2563EB] transition"
>
See it in action
<ArrowRight size={14} />
</a>
</motion.header>
{/* Big hero bento (8 cols, 2 rows tall) */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="md:col-span-8 md:row-span-2 relative overflow-hidden rounded-[28px] bg-gradient-to-br from-[#FAFAF7] via-[#F5F1EA] to-[#EBE4D5] border border-black/5 p-8 md:p-12 lg:p-14 min-h-[520px] flex flex-col justify-between shadow-[0_4px_24px_-8px_rgba(0,0,0,0.06)]"
>
{/* Subtle grid texture */}
<div
className="absolute inset-0 opacity-[0.04] pointer-events-none"
style={{
backgroundImage:
"linear-gradient(#1A1A1A 1px, transparent 1px), linear-gradient(90deg, #1A1A1A 1px, transparent 1px)",
backgroundSize: "48px 48px",
}}
/>
<div className="relative z-10 max-w-xl">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#2563EB]/10 text-[#2563EB] text-xs font-semibold tracking-wide mb-7">
<Sparkles size={12} />
AI-POWERED DIAGNOSTICS
</span>
<h1 className="text-[clamp(2.75rem,7vw,5.5rem)] font-extrabold tracking-tight text-[#1A1A1A] leading-[1.02]">
Your car,
<br />
<span className="text-[#2563EB]">decoded</span>
<span className="text-[#E07856]">.</span>
</h1>
<p className="mt-6 text-base md:text-lg text-[#1A1A1A]/65 leading-relaxed max-w-md">
Plug in a $10 OBD scanner. Scan the QR code. Get a full AI repair report in plain English in 10 seconds.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<a
href="#showcase"
className="group inline-flex items-center gap-2 px-6 py-3 rounded-full bg-[#1A1A1A] text-white font-semibold hover:bg-[#2563EB] transition"
>
See it in action
<ArrowRight size={16} className="group-hover:translate-x-0.5 transition-transform" />
</a>
<a
href="#how"
className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-white border border-black/10 text-[#1A1A1A] font-semibold hover:bg-black/5 transition"
>
How it works
</a>
</div>
</div>
{/* Wrench Uncle mascot — bottom right, vignette gray bg masked to bg */}
<motion.img
initial={{ opacity: 0, y: 40, rotate: -6 }}
animate={{ opacity: 1, y: 0, rotate: 0 }}
transition={{ duration: 0.8, delay: 0.3, ease: "easeOut" }}
src={ASSETS.wrenchUncle}
alt="Wrench Uncle, the OBDX mechanic mascot"
className="absolute -bottom-10 -right-12 md:-bottom-14 md:-right-14 w-60 md:w-[20rem] lg:w-[26rem] object-contain pointer-events-none select-none mix-blend-multiply"
style={{
maskImage:
"radial-gradient(ellipse 62% 75% at 52% 38%, black 50%, transparent 95%)",
WebkitMaskImage:
"radial-gradient(ellipse 62% 75% at 52% 38%, black 50%, transparent 95%)",
}}
/>
</motion.div>
{/* Bento: diagnosis time (dark) */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.15 }}
className="md:col-span-4 rounded-[28px] bg-[#1A1A1A] text-white p-6 md:p-7 flex flex-col justify-between min-h-[240px] relative overflow-hidden"
>
<div className="flex items-center gap-2 text-xs font-mono text-white/40 tracking-wider">
<Clock size={13} />
DIAGNOSIS TIME
</div>
<div>
<div className="text-6xl md:text-7xl font-extrabold tabular-nums leading-none">
10<span className="text-[#F59E0B] text-3xl ml-1">sec</span>
</div>
<div className="text-sm text-white/60 mt-3">From plug-in to full AI report</div>
</div>
{/* Pulse dot */}
<div className="absolute top-6 right-6 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-[#10B981] animate-pulse" />
<span className="text-[10px] font-mono text-white/40">LIVE</span>
</div>
</motion.div>
{/* Bento: knowledge base (terracotta) */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="md:col-span-4 rounded-[28px] bg-[#E07856] text-white p-6 md:p-7 flex flex-col justify-between min-h-[240px] relative overflow-hidden"
>
<div className="flex items-center gap-2 text-xs font-mono text-white/70 tracking-wider">
<Database size={13} />
KNOWLEDGE BASE
</div>
<div>
<div className="text-6xl md:text-7xl font-extrabold tabular-nums leading-none">
706<span className="text-white/80 text-3xl ml-1">GB</span>
</div>
<div className="text-sm text-white/85 mt-3">
82 brands · 24,935 vehicle models
</div>
</div>
{/* Decorative car icon bg */}
<Car
size={120}
className="absolute -bottom-4 -right-4 text-white/10"
strokeWidth={1.5}
/>
</motion.div>
{/* Stats row (full width) */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.25 }}
className="md:col-span-12 rounded-[28px] bg-white border border-black/5 p-6 md:p-8 grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-4 shadow-[0_4px_24px_-8px_rgba(0,0,0,0.04)]"
>
{STATS.map((s, i) => (
<div
key={i}
className={`${i > 0 ? "md:border-l md:border-black/5 md:pl-6" : ""}`}
>
<div className="text-3xl md:text-4xl font-extrabold text-[#1A1A1A] tabular-nums leading-none">
{s.value}
{s.unit && (
<span className="text-[#2563EB] text-xl md:text-2xl ml-1 font-bold">
{s.unit}
</span>
)}
</div>
<div className="text-sm text-[#1A1A1A]/55 mt-2">{s.label}</div>
</div>
))}
</motion.div>
{/* Feature preview bento row */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="md:col-span-6 rounded-[28px] bg-[#FAFAF7] border border-black/5 p-6 md:p-7 min-h-[200px] flex flex-col justify-between"
>
<div className="flex items-center gap-2 text-xs font-mono text-[#1A1A1A]/40 tracking-wider">
<Gauge size={13} />
VEHICLE-SPECIFIC
</div>
<div>
<h3 className="text-2xl md:text-3xl font-bold text-[#1A1A1A]">
Not generic code lookup.
</h3>
<p className="text-sm text-[#1A1A1A]/60 mt-2 max-w-sm">
OBDX knows your exact model &amp; engine variant. P0301 on a Civic
P0301 on an F-150.
</p>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.35 }}
className="md:col-span-6 rounded-[28px] bg-[#2563EB] text-white p-6 md:p-7 min-h-[200px] flex flex-col justify-between relative overflow-hidden"
>
<div className="flex items-center gap-2 text-xs font-mono text-white/60 tracking-wider">
<Sparkles size={13} />
PLAIN ENGLISH
</div>
<div className="relative z-10">
<h3 className="text-2xl md:text-3xl font-bold">
No jargon.
<br />
Just what's wrong &amp; how much.
</h3>
</div>
</motion.div>
</div>
</section>
);
}

119
src/components/Pricing.tsx Normal file
View File

@@ -0,0 +1,119 @@
import { motion } from "framer-motion";
import { Check, Sparkles, ArrowRight } from "lucide-react";
import { PRICING_PLANS } from "@/lib/constants";
export default function Pricing() {
return (
<section
id="pricing"
className="px-4 md:px-6 lg:px-8 pb-8 md:pb-12 space-y-4 md:space-y-5"
>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5 }}
className="rounded-[28px] bg-white border border-black/5 p-8 md:p-12 text-center"
>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#2563EB]/10 text-[#2563EB] text-xs font-semibold tracking-wider mb-4">
PRICING
</span>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-extrabold tracking-tight text-[#1A1A1A] leading-[1.05]">
Pay nothing.
<br />
<span className="text-[#1A1A1A]/40">Or $4.99 a month.</span>
</h2>
<p className="mt-4 text-base md:text-lg text-[#1A1A1A]/60 max-w-xl mx-auto">
Free covers the basics. Plus unlocks unlimited vehicle-specific AI
analysis. Shop is for independent repair businesses.
</p>
</motion.div>
{/* Pricing grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-5">
{PRICING_PLANS.map((plan, i) => {
const highlighted = plan.highlighted;
return (
<motion.div
key={plan.name}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, delay: i * 0.1 }}
className={`relative rounded-[28px] border p-7 md:p-8 flex flex-col ${
highlighted
? "bg-[#1A1A1A] text-white border-white/10 md:scale-[1.02] shadow-[0_12px_40px_-12px_rgba(0,0,0,0.3)]"
: "bg-white text-[#1A1A1A] border-black/5"
}`}
>
{highlighted && (
<span className="absolute top-4 right-4 inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-[#F59E0B] text-[#1A1A1A] text-[10px] font-bold uppercase tracking-wider">
<Sparkles size={10} />
Most popular
</span>
)}
<div className="flex items-baseline gap-2 mb-1">
<h3 className="text-2xl font-bold">{plan.name}</h3>
</div>
<p
className={`text-sm ${
highlighted ? "text-white/60" : "text-[#1A1A1A]/55"
}`}
>
{plan.description}
</p>
<div className="mt-6 mb-7 flex items-baseline gap-1">
<span className="text-5xl font-extrabold tracking-tight">
{plan.price}
</span>
<span
className={`text-sm ${
highlighted ? "text-white/55" : "text-[#1A1A1A]/45"
}`}
>
{plan.period}
</span>
</div>
<ul className="space-y-3 mb-8 flex-1">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start gap-2 text-sm">
<Check
size={16}
className={`shrink-0 mt-0.5 ${
highlighted ? "text-[#F59E0B]" : "text-[#2563EB]"
}`}
strokeWidth={2.5}
/>
<span
className={
highlighted ? "text-white/85" : "text-[#1A1A1A]/75"
}
>
{feature}
</span>
</li>
))}
</ul>
<a
href="#sample"
className={`inline-flex items-center justify-center gap-2 w-full px-6 py-3 rounded-full font-semibold transition ${
highlighted
? "bg-[#F59E0B] text-[#1A1A1A] hover:bg-white"
: "bg-[#1A1A1A] text-white hover:bg-[#2563EB]"
}`}
>
{plan.cta}
<ArrowRight size={16} />
</a>
</motion.div>
);
})}
</div>
</section>
);
}

259
src/components/Showcase.tsx Normal file
View File

@@ -0,0 +1,259 @@
import { motion } from "framer-motion";
import {
Wrench,
QrCode,
FileText,
ArrowRight,
Car,
} from "lucide-react";
const STEPS = [
{
num: "01",
icon: Wrench,
title: "Plug in",
desc: "Insert any $10 OBD-II scanner under your steering wheel. Bluetooth or Wi-Fi — both work.",
color: "#1A1A1A",
},
{
num: "02",
icon: QrCode,
title: "Scan QR",
desc: "Device shows a QR code. Open your phone camera. Tap the link. No app install needed.",
color: "#2563EB",
},
{
num: "03",
icon: FileText,
title: "Read report",
desc: "AI report opens in your browser in 10 seconds. Plain English. Cost estimates included.",
color: "#E07856",
},
];
const CASES = [
{
persona: "DIY Owner",
vehicle: "2018 Honda Civic 1.5L Turbo",
symptom: "Check engine light came on yesterday.",
dtc: "P0420",
dtcMeaning: "Catalyst System Efficiency Below Threshold",
diagnosis:
"Your catalytic converter is degrading. You can drive safely for 12 weeks, but plan a repair. Very common on 20162018 Civics past 80k miles. Independent shop is fine — no need for the dealer.",
severityLabel: "Plan within 2 weeks",
cost: "$400 $650",
cardClass: "bg-[#FAFAF7] text-[#1A1A1A] border-black/5",
severityColor: "#F59E0B",
},
{
persona: "Used Car Buyer",
vehicle: "2015 Ford F-150 5.0L V8 · 108k mi",
symptom: "Considering this truck — what am I getting into?",
dtc: "P0316 + P0171 + P0496",
dtcMeaning: "Misfire (cyl 1) · Lean fuel · EVAP leak",
diagnosis:
"Misfire = carbon buildup, typical of 5.0L past 100k. Lean and EVAP codes are cheap fixes. Show this report to the seller and negotiate ~$1,200 off the asking price.",
severityLabel: "Negotiate before buying",
cost: "$800 $1,400 total",
cardClass: "bg-[#1A1A1A] text-white border-white/10",
severityColor: "#E07856",
},
{
persona: "Repair Shop",
vehicle: "2020 Toyota Camry 2.5L (customer)",
symptom: "“Random shaking when I idle.”",
dtc: "P0301",
dtcMeaning: "Cylinder 1 Misfire Detected",
diagnosis:
"Most likely: spark plug or coil pack on cyl 1. Start with a $30 spark plug + 30 min labor. If misfire persists, swap the coil pack (~$60). Send the customer this branded report.",
severityLabel: "Easy fix",
cost: "$80 $200",
cardClass: "bg-[#2563EB] text-white border-white/10",
severityColor: "#10B981",
},
];
export default function Showcase() {
return (
<section
id="showcase"
className="px-4 md:px-6 lg:px-8 pb-8 md:pb-12 space-y-4 md:space-y-5"
>
{/* Section header */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5 }}
className="rounded-[28px] bg-white border border-black/5 p-8 md:p-12 text-center"
>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#E07856]/10 text-[#E07856] text-xs font-semibold tracking-wider mb-4">
HOW IT WORKS
</span>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-extrabold tracking-tight text-[#1A1A1A] leading-[1.05]">
See it in action.
</h2>
<p className="mt-4 text-base md:text-lg text-[#1A1A1A]/60 max-w-xl mx-auto">
Three steps. Ten seconds. A real AI repair report you can show your
mechanic or your buyer.
</p>
</motion.div>
{/* Steps */}
<div
id="how"
className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-5"
>
{STEPS.map((step, i) => {
const Icon = step.icon;
return (
<motion.div
key={step.num}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, delay: i * 0.1 }}
className="rounded-[28px] bg-[#FAFAF7] border border-black/5 p-7 md:p-8 min-h-[220px] flex flex-col justify-between"
>
<div className="flex items-center justify-between">
<span className="text-xs font-mono text-[#1A1A1A]/40 tracking-wider">
STEP {step.num}
</span>
<Icon size={22} style={{ color: step.color }} />
</div>
<div>
<h3 className="text-2xl md:text-3xl font-bold text-[#1A1A1A]">
{step.title}
</h3>
<p className="text-sm md:text-base text-[#1A1A1A]/60 mt-2 leading-relaxed">
{step.desc}
</p>
</div>
</motion.div>
);
})}
</div>
{/* Cases header */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5 }}
className="flex items-end justify-between px-2 pt-4"
>
<div>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#2563EB]/10 text-[#2563EB] text-xs font-semibold tracking-wider mb-3">
REAL EXAMPLES
</span>
<h3 className="text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight text-[#1A1A1A] leading-tight">
Three drivers.
<br className="md:hidden" />{" "}
<span className="text-[#1A1A1A]/40">Three diagnoses.</span>
</h3>
</div>
</motion.div>
{/* Cases grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-5">
{CASES.map((c, i) => (
<motion.article
key={i}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, delay: i * 0.1 }}
className={`rounded-[28px] border ${c.cardClass} p-6 md:p-7 flex flex-col gap-5 min-h-[460px]`}
>
<div className="flex items-center justify-between">
<span className="text-[10px] font-mono tracking-widest uppercase opacity-60">
{c.persona}
</span>
<span
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-semibold uppercase tracking-wide"
style={{
background: `${c.severityColor}22`,
color: c.severityColor,
}}
>
<span
className="w-1.5 h-1.5 rounded-full"
style={{ background: c.severityColor }}
/>
{c.severityLabel}
</span>
</div>
<div>
<div className="flex items-center gap-2 opacity-70">
<Car size={14} />
<span className="text-xs font-medium">{c.vehicle}</span>
</div>
<p className="mt-2 text-base italic opacity-90">
{c.symptom}
</p>
</div>
<div
className={`rounded-2xl p-4 ${
c.cardClass.includes("bg-[#FAFAF7]")
? "bg-white border border-black/5"
: "bg-white/10"
}`}
>
<div className="flex items-baseline gap-2 flex-wrap">
<span
className="font-mono text-lg font-bold"
style={{ color: c.severityColor }}
>
{c.dtc}
</span>
<span className="text-xs opacity-60">{c.dtcMeaning}</span>
</div>
</div>
<p className="text-sm leading-relaxed opacity-85 flex-1">
{c.diagnosis}
</p>
<div
className={`flex items-center justify-between pt-4 border-t ${
c.cardClass.includes("bg-[#FAFAF7]")
? "border-black/5"
: "border-white/10"
}`}
>
<span className="text-xs font-mono opacity-50">EST. COST</span>
<span className="text-lg font-bold">{c.cost}</span>
</div>
</motion.article>
))}
</div>
{/* Bottom CTA strip */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5 }}
className="rounded-[28px] bg-gradient-to-br from-[#E07856] to-[#C45F3F] text-white p-8 md:p-10 flex flex-col md:flex-row items-start md:items-center justify-between gap-6"
>
<div className="max-w-2xl">
<h3 className="text-2xl md:text-3xl font-bold leading-tight">
Got a check engine light? Run your own diagnosis.
</h3>
<p className="mt-2 text-white/85 text-sm md:text-base">
Grab any $10 OBD-II scanner. Plug in. Scan the QR. Done.
</p>
</div>
<a
href="#sample"
className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-white text-[#1A1A1A] font-semibold hover:bg-[#1A1A1A] hover:text-white transition whitespace-nowrap"
>
Run a demo report
<ArrowRight size={16} />
</a>
</motion.div>
</section>
);
}

37
src/index.css Normal file
View File

@@ -0,0 +1,37 @@
@import "tailwindcss";
@theme {
/* Bento Garage palette */
--color-bg-warm: #f5f1ea;
--color-bg-cream: #fafaf7;
--color-cobalt: #2563eb;
--color-terracotta: #e07856;
--color-ink: #1a1a1a;
--color-amber: #f59e0b;
--color-emerald: #10b981;
--color-slate: #64748b;
--font-family-heading: "Outfit", sans-serif;
--font-family-body: "DM Sans", sans-serif;
--font-family-mono: "JetBrains Mono", monospace;
}
body {
font-family: var(--font-family-body);
background: var(--color-bg-warm);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-family-heading);
letter-spacing: -0.02em;
}
.font-mono {
font-family: var(--font-family-mono);
}
html {
scroll-behavior: smooth;
}

89
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,89 @@
export const ASSETS = {
logoIcon: "/brand/logo/obdx-logo-icon-v2.svg",
logoDark: "/brand/logo/obdx-logo-primary-dark-v2.svg",
logoLight: "/brand/logo/obdx-logo-primary-light-v2.svg",
logoTagline: "/brand/logo/obdx-logo-tagline-dark-v2.svg",
sceneRide: "/brand/scenes/obdx-scene-ride-16x9.png",
sceneCheckup: "/brand/scenes/obdx-scene-checkup-16x9.png",
sceneDone: "/brand/scenes/obdx-scene-done-16x9.png",
sceneRideSquare: "/brand/scenes/obdx-scene-ride-square.png",
sceneCheckupSquare: "/brand/scenes/obdx-scene-checkup-square.png",
sceneDoneSquare: "/brand/scenes/obdx-scene-done-square.png",
wrench: "/brand/ip/wrench/default.png",
wrenchUncle: "/brand/ip/wrench-uncle/default.png",
wrenchUncleWithDash: "/brand/ip/wrench-uncle/with-dash.png",
dash: "/brand/ip/dash/default.png",
} as const;
export const NAV_LINKS = [
{ label: "Features", href: "#features" },
{ label: "How it works", href: "#how" },
{ label: "Pricing", href: "#pricing" },
{ label: "Compare", href: "#compare" },
] as const;
export const STATS = [
{ value: "706", unit: "GB", label: "Repair Knowledge" },
{ value: "82", unit: "", label: "Car Brands" },
{ value: "24,935", unit: "", label: "Vehicle Models" },
{ value: "10", unit: "sec", label: "Diagnosis" },
] as const;
export const PRICING_PLANS = [
{
name: "Free",
price: "$0",
period: "/forever",
description: "Get started with basic diagnostics",
features: [
"3 scans per month",
"Basic fault code reading",
"Health score overview",
"1 vehicle profile",
],
cta: "Download Free",
highlighted: false,
},
{
name: "Plus",
price: "$4.99",
period: "/month",
description: "Full AI-powered diagnostics",
features: [
"Unlimited scans",
"Complete AI analysis",
"Vehicle-specific insights",
"Repair cost estimates",
"History & trends",
"Priority support",
],
cta: "Start Free Trial",
highlighted: true,
},
{
name: "Shop",
price: "$19.99",
period: "/month",
description: "For independent repair shops",
features: [
"Everything in Plus",
"Branded reports",
"Customer management",
"Multi-vehicle scanning",
"Business analytics",
"API access",
],
cta: "Contact Sales",
highlighted: false,
},
] as const;
export const COMPETITORS = [
{ name: "FIXD", price: "$59.99", app: true, ai: "Basic", plain: true, advantage: "Deeper AI + Free" },
{ name: "BlueDriver", price: "$99.95", app: true, ai: false, plain: false, advantage: "AI + 50% cheaper" },
{ name: "CarMD", price: "$99.99", app: true, ai: false, plain: "Partial", advantage: "Full AI + Modern UX" },
{ name: "Innova", price: "$200+", app: false, ai: false, plain: false, advantage: "4x cheaper + AI" },
{ name: "Snap-on", price: "$3,000+", app: false, ai: false, plain: false, advantage: "Different league" },
] as const;

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}

1
tsconfig.tsbuildinfo Normal file
View File

@@ -0,0 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/components/comparison.tsx","./src/components/footer.tsx","./src/components/hero.tsx","./src/components/pricing.tsx","./src/components/showcase.tsx","./src/lib/constants.ts"],"version":"5.9.3"}

22
vite.config.ts Normal file
View File

@@ -0,0 +1,22 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "node:path";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
port: 5173,
proxy: {
"/api": {
target: "https://obd.kang-kang.com",
changeOrigin: true,
},
},
},
});