Move nav chrome to top-right and refresh README.
Cluster language, theme, and notifications in nav-utils on the right; document three-ring product IA and stack in README. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
969a0b037a
commit
47bbc7dd0f
243
README.md
243
README.md
@ -1,50 +1,64 @@
|
|||||||
# nomadro · 数字游民旅居平台
|
# nomadro · 数字游民旅居平台
|
||||||
|
|
||||||
全栈数字游民旅居指南,品牌 **nomadro** — 用一行代码环游世界 🌏
|
品牌 **nomadro** — 用一行代码环游世界 🌏
|
||||||
|
|
||||||
**线上:** [https://nomadweb.nomadro.com](https://nomadweb.nomadro.com)
|
**线上:** [https://nomadweb.nomadro.com](https://nomadweb.nomadro.com)
|
||||||
|
|
||||||
核心产品闭环:**发现目的地 → 智能匹配 / 城市对比 → 旅居计划中心 → 登录云端同步**
|
## 产品路径(环环相扣)
|
||||||
|
|
||||||
|
功能很多,但**不一次全部展示**。导航与首页按三条环串联,每页只露出「接下来」:
|
||||||
|
|
||||||
|
| 环 | 做什么 | 主要入口 |
|
||||||
|
|----|--------|----------|
|
||||||
|
| **探索 Explore** | 发现去哪 | 目的地、地图、下一站、签证;(离首页后再出现计划 / 对比) |
|
||||||
|
| **连接 Connect** | 遇见同行 | 活动 → 社区 → 匹配 → 私信 |
|
||||||
|
| **成长 Grow** | 路上做事 | 学院 → 赏金 → 工具箱(按需)→ 电子书 |
|
||||||
|
|
||||||
|
```
|
||||||
|
发现城市 ──► 写入计划 / 同城活动 ──► 社区 / 匹配 ──► 学院 / 赏金
|
||||||
|
```
|
||||||
|
|
||||||
|
- 顶栏:**探索 / 连接 / 成长** 三组下拉,**EN · 主题 · 通知** 固定在右上
|
||||||
|
- `Ctrl+K` 空态只给三环捷径;次要页面要搜索才出现
|
||||||
|
- 工具箱默认精选 +「展开全部」,不再当全站站点地图
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
| 层级 | 技术 | 说明 |
|
| 层级 | 技术 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 前端 | Next.js 16 + React 19 | App Router、ISR、Turbopack、standalone |
|
| 前端 | Next.js 16 + React 19 | **仅 UI**;App Router、standalone |
|
||||||
| 后端 | FastAPI | REST API、业务逻辑、账号计划同步 |
|
| 后端 | FastAPI | **全部业务逻辑**(无 Next.js 业务 API Route) |
|
||||||
| 数据 | PocketBase + Mock 降级 | 内容库;用户收藏/计划可落盘 |
|
| 数据 | PocketBase | 主存储;本地可 Mock 降级 |
|
||||||
| 部署 | Caddy + systemd(原生优先) | 生产非 Docker 跑 Web/API |
|
| 对象存储 | S3 兼容(生产 SeaweedFS) | 媒体上传 |
|
||||||
|
| 部署 | Caddy + systemd | 原生进程;勿与同机 dylive 业务互相干扰 |
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
nomadweb/
|
nomadweb/
|
||||||
├── frontend/ # Next.js
|
├── frontend/ # Next.js(页面与组件)
|
||||||
│ ├── src/app/ # 路由:/ /plan /compare /tools /profile …
|
│ ├── src/app/ # 路由
|
||||||
│ ├── src/components/ # UI 与工具组件
|
│ ├── src/components/ # UI(含 RingNext 下一步条)
|
||||||
│ └── src/lib/ # API、auth、trip/plan 存储与同步
|
│ └── src/lib/ # api / auth / i18n / rings
|
||||||
├── backend/ # FastAPI
|
├── backend/ # FastAPI
|
||||||
│ ├── app/routers/ # API 路由
|
│ ├── app/routers/ # REST
|
||||||
│ ├── app/services/ # PocketBase、Auth(含计划同步)
|
│ ├── app/services/ # PocketBase、S3、城市深度等
|
||||||
│ └── app/data/ # Mock 降级 + user_store(运行时)
|
│ └── app/data/ # Mock 降级(开发)
|
||||||
├── deploy/ # Caddy、systemd 单元
|
├── deploy/ # Caddy、systemd、env 模板
|
||||||
├── scripts/ # 本地开发、生产部署脚本
|
├── scripts/ # 本地开发与生产更新
|
||||||
├── pocketbase/ # PocketBase 数据目录
|
└── pocketbase/ # PB 数据目录(本地可选)
|
||||||
└── docker-compose*.yml # 本地 / 备选生产 Compose
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 1. PocketBase(可选)
|
### 1. PocketBase(可选)
|
||||||
|
|
||||||
不启动则 API 自动使用 `backend/app/data/mock_data.py`。
|
不启动则 API 使用 Mock。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 8090:8090 -v ./pocketbase/pb_data:/pb_data ghcr.io/muchobien/pocketbase:latest
|
docker run -d -p 8090:8090 -v ./pocketbase/pb_data:/pb_data ghcr.io/muchobien/pocketbase:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
管理后台:http://localhost:8090/_/
|
|
||||||
|
|
||||||
### 2. FastAPI
|
### 2. FastAPI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -67,190 +81,109 @@ npm run dev
|
|||||||
|
|
||||||
访问:http://localhost:3000
|
访问:http://localhost:3000
|
||||||
|
|
||||||
### 其他方式
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\scripts\dev.ps1 # Windows 一键
|
.\scripts\dev.ps1 # Windows 一键
|
||||||
docker compose up -d # Compose 全栈
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心产品
|
## 核心功能一览
|
||||||
|
|
||||||
| 能力 | 路径 / 入口 | 说明 |
|
按环归类(深度页会互相引导,避免平铺):
|
||||||
|------|-------------|------|
|
|
||||||
| 🗺️ 地图 & 目的地 | `/#map` `/#destinations` | 筛选、收藏、勾选对比(最多 4 城) |
|
### 探索
|
||||||
| 🎯 智能匹配 | FAB / `M` | 4 步问卷;偏好本地保存;Top 结果可写入计划 / 对比 |
|
| 能力 | 路径 |
|
||||||
| ⚖️ 城市对比台 | `/compare` | 可分享 `?cities=`;指标并排;写入计划 |
|
|------|------|
|
||||||
| 🗓️ 旅居计划中心 | `/plan` | 多城时间轴、出发月、预算、站内备注、签证方案与超期提醒、就绪清单、MD / ICS 导出、分享链接 |
|
| 地图 / 目的地 | `/#map` `/#destinations` `/destinations/[slug]` |
|
||||||
| ☁️ 账号同步 | `/login?next=/plan` | 登录后合并本机与云端计划;收藏同步;服务重启落盘保留 |
|
| 智能匹配 | 首页 CTA / `M` |
|
||||||
| 👤 个人中心 | `/profile` | 计划仪表盘、就绪度、收藏写入计划、成就 |
|
| 下一站决策 | `/next-stop` |
|
||||||
|
| 城市对比 | `/compare` |
|
||||||
|
| 旅居计划 | `/plan`(时间轴、预算、签证提醒、清单、ICS) |
|
||||||
|
| 签证指南 | `/#visa` |
|
||||||
|
|
||||||
|
### 连接
|
||||||
|
| 能力 | 路径 |
|
||||||
|
|------|------|
|
||||||
|
| 活动 RSVP / 直播间 | `/meetups` `/meetups/[id]/live` |
|
||||||
|
| 社区讨论 | `/community` |
|
||||||
|
| 游民匹配 | `/dating` |
|
||||||
|
| 私信 | `/chat` |
|
||||||
|
| 通知 | `/notifications` |
|
||||||
|
|
||||||
|
### 成长
|
||||||
|
| 能力 | 路径 |
|
||||||
|
|------|------|
|
||||||
|
| 游民学院 | `/digital` |
|
||||||
|
| 赏金任务 | `/gigs` |
|
||||||
|
| 工具箱 | `/tools`(实用小工具,非功能总览) |
|
||||||
|
| 电子书 | `/book` |
|
||||||
|
| 会员 | `/join` `/pricing` |
|
||||||
|
|
||||||
演示账号:`demo@nomadro.com` / `demo123`
|
演示账号:`demo@nomadro.com` / `demo123`
|
||||||
|
|
||||||
## 首页与工具箱
|
|
||||||
|
|
||||||
首页仍聚合大量旅居工具(按视口懒加载),完整列表见 `/tools` 与 `/changelog`。常见能力包括:
|
|
||||||
|
|
||||||
- 费用计算器、多币种、启动金 / 跑道、落地首月成本
|
|
||||||
- 时区看板、会议重叠、日照办公窗、时差估算
|
|
||||||
- 签证指南 + 停留倒计时、保险、税居天数
|
|
||||||
- 联合办公、住宿、SIM、电源、紧急联系、防坑与取现
|
|
||||||
- 行李清单、到站清单、专注时钟、博客与 FAQ 等
|
|
||||||
|
|
||||||
导航、Hero、快捷条、底栏与新手引导优先导向 **计划 / 对比**,小工具集中在工具箱。
|
|
||||||
|
|
||||||
## 体验与性能
|
|
||||||
|
|
||||||
- 首屏:地图 + 目的地;中屏及以下 `WhenVisible` + 动态拆包
|
|
||||||
- Outfit 自托管;中文系统字体;缩短 Loader;粒子延后、无连线
|
|
||||||
- 深浅色主题、`Ctrl+K` 全局搜索、Toast、Cookie 提示
|
|
||||||
- 响应式布局;`.section` 使用 `content-visibility`
|
|
||||||
|
|
||||||
## 页面路由
|
|
||||||
|
|
||||||
| 路径 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `/` | 首页 |
|
|
||||||
| `/plan` | 旅居计划中心 |
|
|
||||||
| `/compare` | 城市对比台 |
|
|
||||||
| `/tools` | 工具箱 |
|
|
||||||
| `/destinations/[slug]` | 目的地详情(画像、停留月数、加入计划) |
|
|
||||||
| `/login` | 登录 / 注册(`?next=` 回流,默认 `/plan`) |
|
|
||||||
| `/profile` | 用户中心 |
|
|
||||||
| `/blog/[slug]` | 博客 |
|
|
||||||
| `/about` `/privacy` `/changelog` | 关于 / 隐私 / 更新日志 |
|
|
||||||
| `/sitemap.xml` | 含 `/plan`、`/compare` 等 |
|
|
||||||
|
|
||||||
## 后端 API(节选)
|
|
||||||
|
|
||||||
| 端点 | 方法 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `/api/v1/destinations` | GET | 列表(region / search / sort) |
|
|
||||||
| `/api/v1/destinations/{slug}` | GET | 详情 |
|
|
||||||
| `/api/v1/destinations/compare` | POST | 按 slug 取城 |
|
|
||||||
| `/api/v1/calculator` | POST | 费用估算 |
|
|
||||||
| `/api/v1/search` | GET | 全局搜索 |
|
|
||||||
| `/api/v1/visas` 等 | GET | 签证 / FAQ / 博客 / 图表 |
|
|
||||||
| `/api/v1/auth/register` | POST | 注册 |
|
|
||||||
| `/api/v1/auth/login` | POST | 登录 |
|
|
||||||
| `/api/v1/auth/demo` | GET | 演示登录 |
|
|
||||||
| `/api/v1/auth/favorites` | GET/POST | 收藏 |
|
|
||||||
| `/api/v1/auth/plan` | GET/PUT | **旅居计划云端读写** |
|
|
||||||
| `/api/v1/auth/profile/stats` | GET | 个人统计 |
|
|
||||||
| `/api/v1/subscribe` | POST | 邮件订阅 |
|
|
||||||
| `/api/v1/health` | GET | 健康检查 |
|
|
||||||
|
|
||||||
PocketBase 不可用时内容接口降级 Mock。用户收藏与计划写入 `backend/app/data/user_store.json`(运行时生成,勿提交密钥与生产数据)。
|
|
||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
**Backend `backend/.env`**
|
**Backend**
|
||||||
|
|
||||||
```env
|
```env
|
||||||
POCKETBASE_URL=http://127.0.0.1:8090
|
POCKETBASE_URL=http://127.0.0.1:8090
|
||||||
POCKETBASE_ADMIN_EMAIL=admin@nomadro.com
|
POCKETBASE_ADMIN_EMAIL=...
|
||||||
POCKETBASE_ADMIN_PASSWORD=admin123456
|
POCKETBASE_ADMIN_PASSWORD=...
|
||||||
CORS_ORIGINS=http://localhost:3000
|
CORS_ORIGINS=http://localhost:3000
|
||||||
|
# S3(生产见服务器 deploy/nomadro-api.env)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Frontend `frontend/.env.local`**
|
**Frontend**
|
||||||
|
|
||||||
```env
|
```env
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
|
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
|
||||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
```
|
```
|
||||||
|
|
||||||
生产前端构建时:
|
生产构建:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
NEXT_PUBLIC_API_URL=https://nomadweb.nomadro.com/api/v1
|
NEXT_PUBLIC_API_URL=https://nomadweb.nomadro.com/api/v1
|
||||||
NEXT_PUBLIC_SITE_URL=https://nomadweb.nomadro.com
|
NEXT_PUBLIC_SITE_URL=https://nomadweb.nomadro.com
|
||||||
```
|
```
|
||||||
|
|
||||||
## Git 工作流
|
## Git 与部署
|
||||||
|
|
||||||
仓库:[gitea.dsx2020.com/eric/nomadweb](https://gitea.dsx2020.com/eric/nomadweb) · 主开发分支 **`dev1`**
|
仓库:[gitea.dsx2020.com/eric/nomadweb](https://gitea.dsx2020.com/eric/nomadweb) · 分支 **`dev1`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 日常开发
|
|
||||||
git checkout dev1
|
git checkout dev1
|
||||||
git pull origin dev1
|
git pull origin dev1
|
||||||
# … 改代码 …
|
# … 改代码 …
|
||||||
git add -A
|
|
||||||
git status # 确认未包含 .env、密钥、运行时 json
|
|
||||||
git commit -m "feat: 简述改动"
|
|
||||||
git push origin dev1
|
git push origin dev1
|
||||||
|
python scripts/deploy_update.py # 服务器 pull + 增量构建重启
|
||||||
# 部署到生产(服务器 git pull + 增量构建)
|
|
||||||
python scripts/deploy_update.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| 步骤 | 说明 |
|
| 生产进程 | 地址 |
|
||||||
|------|------|
|
|----------|------|
|
||||||
| `dev1` | 功能开发、联调、上线均在此分支 |
|
|
||||||
| `main` | 稳定快照,按需从 `dev1` 合并 |
|
|
||||||
| 密钥 | `deploy/nomadro-api.env` 仅放服务器,模板见 `deploy/nomadro-api.env.example` |
|
|
||||||
| 运行时数据 | `user_store.json` / `social_store.json` / `community_store.json` 不提交 |
|
|
||||||
|
|
||||||
**不要用 SFTP 全量同步当主流程** — `deploy_direct_sync.py` 仅作 Gitea 不可用时的应急备份(慢且易漏文件)。
|
|
||||||
|
|
||||||
## 部署
|
|
||||||
|
|
||||||
### 生产(已上线)
|
|
||||||
|
|
||||||
| 项目 | 地址 |
|
|
||||||
|------|------|
|
|
||||||
| 网站 | https://nomadweb.nomadro.com |
|
| 网站 | https://nomadweb.nomadro.com |
|
||||||
| API 健康检查 | https://nomadweb.nomadro.com/api/v1/health |
|
| `nomadro-web` | `127.0.0.1:3055` |
|
||||||
| API 文档 | https://nomadweb.nomadro.com/docs |
|
| `nomadro-api` | `127.0.0.1:8055` |
|
||||||
| 仓库 | https://gitea.dsx2020.com/eric/nomadweb |
|
|
||||||
|
|
||||||
**架构(原生 systemd,Caddy 反代):**
|
|
||||||
|
|
||||||
| 进程 | 地址 |
|
|
||||||
|------|------|
|
|
||||||
| Caddy | HTTPS → 见 `deploy/caddy/nomadweb.caddy` |
|
|
||||||
| `nomadro-web` | `127.0.0.1:3055`(Next standalone) |
|
|
||||||
| `nomadro-api` | `127.0.0.1:8055`(uvicorn) |
|
|
||||||
| PocketBase | `127.0.0.1:8090` |
|
| PocketBase | `127.0.0.1:8090` |
|
||||||
|
| S3 | `s3.nomadro.com` → SeaweedFS |
|
||||||
|
|
||||||
单元:`deploy/systemd/nomadro-web.service`、`nomadro-api.service`。
|
**注意:** 同机还跑有 [dylive.nomadro.com](https://dylive.nomadro.com/)(`/opt/douyinWEB`)。更新 nomadweb 时**不要**改动 / 限流 / 杀掉直播相关进程。
|
||||||
|
|
||||||
**推荐更新(Git):**
|
密钥仅放服务器 `deploy/nomadro-api.env`(模板见 `*.env.example`),勿提交。
|
||||||
|
|
||||||
```bash
|
|
||||||
git push origin dev1
|
|
||||||
python scripts/deploy_update.py
|
|
||||||
```
|
|
||||||
|
|
||||||
服务器执行 `git pull` → 按变更只重建前端或后端 → `systemctl restart`;比 SFTP 全量同步快且可回滚。
|
|
||||||
|
|
||||||
**应急(Gitea 不可用 / 未 push 的本地提交):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/deploy_direct_sync.py
|
|
||||||
```
|
|
||||||
|
|
||||||
SFTP 同步变更文件 → 重建前端(并重启 API)→ 健康检查。
|
|
||||||
|
|
||||||
**备选 Docker Compose:** 见 `docker-compose.prod.yml`(生产默认以原生为准)。
|
|
||||||
|
|
||||||
## 快捷键
|
## 快捷键
|
||||||
|
|
||||||
| 按键 | 功能 |
|
| 按键 | 功能 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `Ctrl+K` | 全局搜索 |
|
| `Ctrl+K` | 全局搜索(三环捷径) |
|
||||||
| `M` | 智能匹配 |
|
| `M` | 智能匹配 |
|
||||||
| `P` | 打开旅居计划中心 |
|
| `P` | 旅居计划 |
|
||||||
| `T` | 打开工具箱 |
|
| `T` | 工具箱 |
|
||||||
| `G` | 命运转盘(首页) |
|
| `?` | 帮助 |
|
||||||
| `?` | 快捷键帮助 |
|
|
||||||
| `Esc` | 关闭弹窗 |
|
| `Esc` | 关闭弹窗 |
|
||||||
|
|
||||||
## 相关文档
|
## 相关
|
||||||
|
|
||||||
- 产品迭代:[`/changelog`](https://nomadweb.nomadro.com/changelog)
|
- 迭代说明:[/changelog](https://nomadweb.nomadro.com/changelog)
|
||||||
- 本地变更记录:`frontend/src/app/changelog/page.tsx`
|
- 前端脚手架说明:`frontend/README.md`(以本文件为准)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +1,20 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# frontend · nomadro
|
||||||
|
|
||||||
## Getting Started
|
Next.js App Router UI for [nomadro](https://nomadweb.nomadro.com).
|
||||||
|
|
||||||
First, run the development server:
|
**业务逻辑全部在 FastAPI**(`../backend`)。本目录只负责页面与交互。
|
||||||
|
|
||||||
|
完整架构、三环产品路径、环境变量与部署说明见仓库根目录 **[README.md](../README.md)**。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cp .env.local.example .env.local
|
||||||
|
npm install --legacy-peer-deps
|
||||||
npm run dev
|
npm run dev
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
生产构建需设置:
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
```env
|
||||||
|
NEXT_PUBLIC_API_URL=https://nomadweb.nomadro.com/api/v1
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
NEXT_PUBLIC_SITE_URL=https://nomadweb.nomadro.com
|
||||||
|
```
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
||||||
|
|||||||
@ -141,7 +141,42 @@ img { max-width: 100%; display: block; }
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container > .logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container > .nav-links {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-utils {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-utils .nav-lang-btn,
|
||||||
|
.nav-utils .nav-cta,
|
||||||
|
.nav-utils .nav-notify {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-utils .nav-cta {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-notify-guest {
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@ -9477,17 +9512,20 @@ img { max-width: 100%; display: block; }
|
|||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: var(--border-glass);
|
border-top: var(--border-glass);
|
||||||
}
|
}
|
||||||
.nav-lang-desktop { margin-left: 4px; }
|
|
||||||
.nav-login-desktop { margin-left: 4px; }
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.nav-lang-desktop,
|
.nav-login-desktop,
|
||||||
.nav-login-desktop { display: none; }
|
.nav-user-desktop { display: none; }
|
||||||
.nav-mobile-extras { display: flex; }
|
.nav-mobile-extras { display: flex; }
|
||||||
.nav-mobile-extras .nav-login-btn {
|
.nav-mobile-extras .nav-login-btn {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.nav-utils { gap: 6px; }
|
||||||
|
.nav-utils .nav-lang-btn {
|
||||||
|
min-width: 40px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.plan-page,
|
.plan-page,
|
||||||
.compare-page,
|
.compare-page,
|
||||||
@ -10743,7 +10781,7 @@ img { max-width: 100%; display: block; }
|
|||||||
.live-chat { border-left: none; border-top: var(--border-glass); min-height: 320px; }
|
.live-chat { border-left: none; border-top: var(--border-glass); min-height: 320px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— Feature rings / progressive IA —— */
|
/* -- Feature rings / progressive IA -- */
|
||||||
.nav-group {
|
.nav-group {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -197,9 +197,6 @@ export default function Navbar() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="nav-mobile-extras">
|
<div className="nav-mobile-extras">
|
||||||
<button type="button" className="nav-lang-btn" onClick={toggleLocale} aria-label={t.nav.lang}>
|
|
||||||
{locale === "zh" ? t.lang.en : t.lang.zh}
|
|
||||||
</button>
|
|
||||||
{!user && (
|
{!user && (
|
||||||
<Link
|
<Link
|
||||||
href={`/login?next=${encodeURIComponent(loginNext)}`}
|
href={`/login?next=${encodeURIComponent(loginNext)}`}
|
||||||
@ -209,40 +206,60 @@ export default function Navbar() {
|
|||||||
{t.nav.login}
|
{t.nav.login}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
{user && (
|
||||||
|
<Link href="/profile" className="nav-login-btn" onClick={closeAll}>
|
||||||
|
{t.footer.profile}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<button type="button" className="nav-lang-btn nav-lang-desktop" onClick={toggleLocale} aria-label={t.nav.lang}>
|
{/* Right chrome: language / theme / notifications — always top-right */}
|
||||||
{locale === "zh" ? "EN" : "中文"}
|
<div className="nav-utils" aria-label="preferences">
|
||||||
</button>
|
<button type="button" className="nav-lang-btn" onClick={toggleLocale} aria-label={t.nav.lang}>
|
||||||
<button type="button" className="nav-cta" onClick={toggleTheme} aria-label={t.nav.theme}>
|
{locale === "zh" ? "EN" : "中文"}
|
||||||
<span className="theme-icon">{theme === "dark" ? "🌙" : "☀️"}</span>
|
</button>
|
||||||
</button>
|
<button type="button" className="nav-cta" onClick={toggleTheme} aria-label={t.nav.theme}>
|
||||||
{user ? (
|
<span className="theme-icon">{theme === "dark" ? "🌙" : "☀️"}</span>
|
||||||
<>
|
</button>
|
||||||
|
{user ? (
|
||||||
<Link href="/notifications" className="nav-notify" aria-label={t.nav.notifications}>
|
<Link href="/notifications" className="nav-notify" aria-label={t.nav.notifications}>
|
||||||
🔔{unread > 0 && <em className="nav-notify-badge">{unread}</em>}
|
🔔{unread > 0 && <em className="nav-notify-badge">{unread}</em>}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/profile" className="nav-user">
|
) : (
|
||||||
|
<Link
|
||||||
|
href={`/login?next=${encodeURIComponent(loginNext)}`}
|
||||||
|
className="nav-notify nav-notify-guest"
|
||||||
|
aria-label={t.nav.notifications}
|
||||||
|
title={t.nav.login}
|
||||||
|
>
|
||||||
|
🔔
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{user ? (
|
||||||
|
<Link href="/profile" className="nav-user nav-user-desktop">
|
||||||
<span>{user.avatar}</span>
|
<span>{user.avatar}</span>
|
||||||
<span className="nav-user-name">{user.name}</span>
|
<span className="nav-user-name">{user.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
) : (
|
||||||
) : (
|
<Link
|
||||||
<Link href={`/login?next=${encodeURIComponent(loginNext)}`} className="nav-login-btn nav-login-desktop">
|
href={`/login?next=${encodeURIComponent(loginNext)}`}
|
||||||
{t.nav.login}
|
className="nav-login-btn nav-login-desktop"
|
||||||
</Link>
|
>
|
||||||
)}
|
{t.nav.login}
|
||||||
<button
|
</Link>
|
||||||
type="button"
|
)}
|
||||||
className={`mobile-menu-btn${menuOpen ? " open" : ""}`}
|
<button
|
||||||
onClick={() => setMenuOpen(!menuOpen)}
|
type="button"
|
||||||
aria-label={t.nav.menu}
|
className={`mobile-menu-btn${menuOpen ? " open" : ""}`}
|
||||||
>
|
onClick={() => setMenuOpen(!menuOpen)}
|
||||||
<span />
|
aria-label={t.nav.menu}
|
||||||
<span />
|
>
|
||||||
<span />
|
<span />
|
||||||
</button>
|
<span />
|
||||||
|
<span />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user