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>
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
dist
|
||||
.dev-screenshot-*.png
|
||||
.playwright-mcp
|
||||
.env
|
||||
.env.*
|
||||
.DS_Store
|
||||
*.log
|
||||
9
.gitignore
vendored
Normal 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
@@ -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 Mono(VI 继承)
|
||||
- **布局**:12 列 bento grid,大格 + 小格不对称堆叠,`rounded-3xl` 柔和圆角
|
||||
- **动效**:每格独立淡入(stagger),hover 放大轻微 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
@@ -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
|
||||
- 现代 SaaS:Linear、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
@@ -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
@@ -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
@@ -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
26
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
public/brand/ip/dash/default.png
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
public/brand/ip/dash/expressions.png
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
public/brand/ip/wrench-uncle/default.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
public/brand/ip/wrench-uncle/expressions.png
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/brand/ip/wrench-uncle/poses.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
public/brand/ip/wrench-uncle/with-dash.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
public/brand/ip/wrench/default.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
public/brand/ip/wrench/expressions.png
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
BIN
public/brand/ip/wrench/poses.png
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
52
public/brand/logo/obdx-logo-icon-v2.svg
Normal 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 |
BIN
public/brand/logo/obdx-logo-icon.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
53
public/brand/logo/obdx-logo-primary-dark-v2.svg
Normal 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 |
BIN
public/brand/logo/obdx-logo-primary-dark.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
54
public/brand/logo/obdx-logo-primary-light-v2.svg
Normal 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 |
BIN
public/brand/logo/obdx-logo-primary-light.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
63
public/brand/logo/obdx-logo-tagline-dark-v2.svg
Normal 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 |
BIN
public/brand/logo/obdx-logo-tagline-dark.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
public/brand/scenes/obdx-scene-checkup-16x9.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
public/brand/scenes/obdx-scene-checkup-square.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
public/brand/scenes/obdx-scene-done-16x9.png
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
BIN
public/brand/scenes/obdx-scene-done-square.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
public/brand/scenes/obdx-scene-ride-16x9.png
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
public/brand/scenes/obdx-scene-ride-square.png
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
17
src/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
171
src/components/Comparison.tsx
Normal 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
@@ -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
@@ -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 & 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 & how much.
|
||||
</h3>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
119
src/components/Pricing.tsx
Normal 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
@@ -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 1–2 weeks, but plan a repair. Very common on 2016–2018 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||