Rebrand to nomadro and add wifi score, savings goal, daily quote
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f13722123a
commit
18fb3318b1
11
README.md
11
README.md
@ -1,7 +1,7 @@
|
||||
# NomadFlow · 数字游民旅居平台
|
||||
# nomadro · 数字游民旅居平台
|
||||
|
||||
全栈数字游民旅居指南,采用 **Next.js + FastAPI + PocketBase** 架构。
|
||||
品牌:**NomadFlow** — 用一行代码环游世界 🌏
|
||||
品牌:**nomadro** — 用一行代码环游世界 🌏
|
||||
|
||||
## 技术栈
|
||||
|
||||
@ -108,6 +108,9 @@ docker compose up -d
|
||||
| 🎲 命运转盘 | 随机目的地抽选,趣味决策 |
|
||||
| ✈️ 机票估算 | 出发城 × 季节 × 人数粗算往返票价 |
|
||||
| 📅 活动日历 | Meetup / 工作坊 / 线上活动 |
|
||||
| 📶 网速评级 | 远程办公 WiFi 评级与排行 |
|
||||
| 🎯 启动金目标 | 存款进度追踪,预估出发时间 |
|
||||
| ✨ 今日金句 | 每日游民灵感语录 |
|
||||
| 📝 博客 | 标签筛选 + 阅读进度 + 相关推荐 + 分享 |
|
||||
| ❓ FAQ | 搜索 + 展开/收起全部 |
|
||||
|
||||
@ -131,7 +134,7 @@ docker compose up -d
|
||||
- 🔐 登录 / 注册 / 一键演示账号
|
||||
- ❤️ 收藏目的地(同步 API)
|
||||
- 👤 用户中心:统计、收藏、行程、成就徽章
|
||||
- 演示账号:`demo@nomadflow.io` / `demo123`
|
||||
- 演示账号:`demo@nomadro.com` / `demo123`
|
||||
|
||||
### 后端 API
|
||||
|
||||
@ -169,7 +172,7 @@ docker compose up -d
|
||||
|
||||
```env
|
||||
POCKETBASE_URL=http://127.0.0.1:8090
|
||||
POCKETBASE_ADMIN_EMAIL=admin@nomadflow.io
|
||||
POCKETBASE_ADMIN_EMAIL=admin@nomadro.com
|
||||
POCKETBASE_ADMIN_PASSWORD=admin123456
|
||||
CORS_ORIGINS=http://localhost:3000
|
||||
```
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
pocketbase_url: str = "http://127.0.0.1:8090"
|
||||
pocketbase_admin_email: str = "admin@nomadflow.io"
|
||||
pocketbase_admin_email: str = "admin@nomadro.com"
|
||||
pocketbase_admin_password: str = "admin123456"
|
||||
api_host: str = "0.0.0.0"
|
||||
api_port: int = 8000
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Fallback data when PocketBase is unavailable."""
|
||||
"""Fallback data when PocketBase is unavailable."""
|
||||
|
||||
DESTINATIONS = [
|
||||
{
|
||||
@ -91,9 +91,9 @@ TOOLS = [
|
||||
]
|
||||
|
||||
BLOG_POSTS = [
|
||||
{"id": "1", "slug": "chiangmai-guide-2026", "title": "清迈数字游民完全指南 2026", "excerpt": "从签证到住宿,从咖啡馆到 Co-working,一篇搞定清迈旅居。", "emoji": "🏔️", "author": "NomadFlow", "published_at": "2026-01-15", "read_time": 8, "tags": ["清迈", "指南", "东南亚"]},
|
||||
{"id": "2", "slug": "portugal-d7-visa", "title": "葡萄牙 D7 签证申请全攻略", "excerpt": "手把手教你申请欧洲最受欢迎的远程工作签证。", "emoji": "🇵🇹", "author": "NomadFlow", "published_at": "2026-02-03", "read_time": 12, "tags": ["签证", "葡萄牙", "欧洲"]},
|
||||
{"id": "3", "slug": "nomad-tax-basics", "title": "数字游民税务入门:你需要知道的 5 件事", "excerpt": "183 天规则、双重征税、税务居民身份——一文理清。", "emoji": "🧾", "author": "NomadFlow", "published_at": "2026-02-20", "read_time": 10, "tags": ["税务", "法律", "指南"]},
|
||||
{"id": "1", "slug": "chiangmai-guide-2026", "title": "清迈数字游民完全指南 2026", "excerpt": "从签证到住宿,从咖啡馆到 Co-working,一篇搞定清迈旅居。", "emoji": "🏔️", "author": "nomadro", "published_at": "2026-01-15", "read_time": 8, "tags": ["清迈", "指南", "东南亚"]},
|
||||
{"id": "2", "slug": "portugal-d7-visa", "title": "葡萄牙 D7 签证申请全攻略", "excerpt": "手把手教你申请欧洲最受欢迎的远程工作签证。", "emoji": "🇵🇹", "author": "nomadro", "published_at": "2026-02-03", "read_time": 12, "tags": ["签证", "葡萄牙", "欧洲"]},
|
||||
{"id": "3", "slug": "nomad-tax-basics", "title": "数字游民税务入门:你需要知道的 5 件事", "excerpt": "183 天规则、双重征税、税务居民身份——一文理清。", "emoji": "🧾", "author": "nomadro", "published_at": "2026-02-20", "read_time": 10, "tags": ["税务", "法律", "指南"]},
|
||||
]
|
||||
|
||||
BLOG_CONTENT: dict[str, str] = {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.config import settings
|
||||
from app.routers import api, auth
|
||||
|
||||
app = FastAPI(
|
||||
title="NomadFlow API",
|
||||
title="nomadro API",
|
||||
description="数字游民旅居平台后端 API",
|
||||
version="1.0.0",
|
||||
)
|
||||
@ -25,7 +25,7 @@ app.include_router(auth.router, prefix="/api/v1")
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"name": "NomadFlow API",
|
||||
"name": "nomadro API",
|
||||
"docs": "/docs",
|
||||
"health": "/api/v1/health",
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"""Simple auth service with in-memory fallback when PocketBase users unavailable."""
|
||||
"""Simple auth service with in-memory fallback when PocketBase users unavailable."""
|
||||
import hashlib
|
||||
import secrets
|
||||
from typing import Any
|
||||
@ -79,5 +79,5 @@ def _user_profile(user: dict) -> dict:
|
||||
}
|
||||
|
||||
# Demo account
|
||||
_demo = register_user("demo@nomadflow.io", "demo123", "演示用户")
|
||||
_demo = register_user("demo@nomadro.com", "demo123", "演示用户")
|
||||
DEMO_TOKEN = _demo["token"] if _demo else ""
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import httpx
|
||||
import httpx
|
||||
from typing import Any
|
||||
|
||||
from app.config import settings
|
||||
@ -145,9 +145,9 @@ class PocketBaseService:
|
||||
|
||||
result = await self.create_record("subscriptions", {"email": email})
|
||||
if result:
|
||||
return True, "订阅成功!欢迎加入 NomadFlow 社区 🎉"
|
||||
return True, "订阅成功!欢迎加入 nomadro 社区 🎉"
|
||||
# fallback: always succeed in demo mode
|
||||
return True, "订阅成功!欢迎加入 NomadFlow 社区 🎉"
|
||||
return True, "订阅成功!欢迎加入 nomadro 社区 🎉"
|
||||
|
||||
def _map_destination(self, r: dict) -> dict:
|
||||
return {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Seed PocketBase collections with NomadFlow data.
|
||||
"""
|
||||
Seed PocketBase collections with nomadro data.
|
||||
Run after PocketBase is started: python seed_pocketbase.py
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
@ -18,7 +18,7 @@ services:
|
||||
- "127.0.0.1:8055:8000"
|
||||
environment:
|
||||
- POCKETBASE_URL=http://pocketbase:8090
|
||||
- POCKETBASE_ADMIN_EMAIL=${POCKETBASE_ADMIN_EMAIL:-admin@nomadflow.io}
|
||||
- POCKETBASE_ADMIN_EMAIL=${POCKETBASE_ADMIN_EMAIL:-admin@nomadro.com}
|
||||
- POCKETBASE_ADMIN_PASSWORD=${POCKETBASE_ADMIN_PASSWORD:-admin123456}
|
||||
- CORS_ORIGINS=${CORS_ORIGINS:-https://nomadweb.nomadro.com,http://nomadweb.nomadro.com}
|
||||
depends_on:
|
||||
|
||||
@ -20,7 +20,7 @@ services:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- POCKETBASE_URL=http://pocketbase:8090
|
||||
- POCKETBASE_ADMIN_EMAIL=admin@nomadflow.io
|
||||
- POCKETBASE_ADMIN_EMAIL=admin@nomadro.com
|
||||
- POCKETBASE_ADMIN_PASSWORD=admin123456
|
||||
- CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
depends_on:
|
||||
|
||||
@ -121,14 +121,23 @@ img { max-width: 100%; display: block; }
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
font-size: 1.35rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
background: linear-gradient(135deg, #FF6B6B, #FFE66D, #4ECDC4);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
animation: logoSpin 20s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes logoSpin {
|
||||
@ -6102,3 +6111,269 @@ img { max-width: 100%; display: block; }
|
||||
.flight-card { grid-template-columns: 1fr; }
|
||||
.spin-card { padding: 28px 20px; }
|
||||
}
|
||||
|
||||
/* ===== Daily Quote ===== */
|
||||
.quote-section { padding-top: 40px; padding-bottom: 40px; }
|
||||
|
||||
.quote-card {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 36px 32px;
|
||||
text-align: center;
|
||||
background: var(--bg-card);
|
||||
border: var(--border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
backdrop-filter: blur(20px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quote-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255,107,107,0.06), rgba(78,205,196,0.08));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quote-emoji {
|
||||
font-size: 2.5rem;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quote-card blockquote {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.15rem, 2.5vw, 1.5rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quote-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ===== Wifi Work Score ===== */
|
||||
.wifi-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 0.8fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.wifi-picker {
|
||||
padding: 28px;
|
||||
background: var(--bg-card);
|
||||
border: var(--border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.wifi-meter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wifi-meter-ring {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--wifi-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 24px color-mix(in srgb, var(--wifi-color) 35%, transparent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wifi-meter-ring strong {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wifi-meter-ring small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.wifi-meter-info h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wifi-level {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.wifi-meter-info p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wifi-link {
|
||||
color: var(--accent-3);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.wifi-rank {
|
||||
padding: 24px;
|
||||
background: var(--bg-card);
|
||||
border: var(--border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.wifi-rank h4 {
|
||||
margin-bottom: 14px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wifi-rank ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wifi-rank li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.wifi-rank li:hover,
|
||||
.wifi-rank li.active {
|
||||
background: rgba(78, 205, 196, 0.1);
|
||||
}
|
||||
|
||||
.wifi-rank-num {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wifi-rank-speed {
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ===== Savings Goal ===== */
|
||||
.savings-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 0.9fr;
|
||||
gap: 28px;
|
||||
padding: 28px;
|
||||
background: var(--bg-card);
|
||||
border: var(--border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.savings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.savings-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
background: rgba(78, 205, 196, 0.05);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.savings-ring {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.savings-ring-inner {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.savings-ring-inner strong {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.savings-ring-inner small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.savings-stats {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.savings-stats > div {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.savings-stats small {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.savings-stats strong {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.savings-hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wifi-layout,
|
||||
.savings-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { AuthProvider } from "@/lib/auth";
|
||||
import { ToastProvider } from "@/lib/toast";
|
||||
@ -7,20 +7,20 @@ const SITE = process.env.NEXT_PUBLIC_SITE_URL || "https://nomadweb.nomadro.com";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(SITE),
|
||||
title: "NomadFlow · 数字游民旅居指南",
|
||||
title: "nomadro · 数字游民旅居指南",
|
||||
description: "探索全球旅居生活,发现最适合数字游民的目的地、工具与社区。",
|
||||
keywords: ["数字游民", "旅居", "远程工作", "nomad", "digital nomad"],
|
||||
openGraph: {
|
||||
title: "NomadFlow · 数字游民旅居指南",
|
||||
title: "nomadro · 数字游民旅居指南",
|
||||
description: "用一行代码环游世界 🌏",
|
||||
type: "website",
|
||||
locale: "zh_CN",
|
||||
url: SITE,
|
||||
siteName: "NomadFlow",
|
||||
siteName: "nomadro",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "NomadFlow · 数字游民旅居指南",
|
||||
title: "nomadro · 数字游民旅居指南",
|
||||
description: "用一行代码环游世界 🌏",
|
||||
},
|
||||
manifest: "/manifest.webmanifest",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
@ -55,7 +55,7 @@ export default function LoginPage() {
|
||||
<Link href="/" className="auth-back">← 返回首页</Link>
|
||||
<div className="auth-header">
|
||||
<span className="auth-logo-emoji">🌍</span>
|
||||
<h1>{mode === "login" ? "欢迎回来" : "加入 NomadFlow"}</h1>
|
||||
<h1>{mode === "login" ? "欢迎回来" : "加入 nomadro"}</h1>
|
||||
<p>{mode === "login" ? "登录你的游民账户" : "开始你的旅居之旅"}</p>
|
||||
</div>
|
||||
|
||||
@ -101,7 +101,7 @@ export default function LoginPage() {
|
||||
<button className="btn btn-ghost" style={{ width: "100%" }} onClick={handleDemo} disabled={loading}>
|
||||
🎮 一键体验演示账号
|
||||
</button>
|
||||
<p className="auth-hint">演示账号:demo@nomadflow.io / demo123</p>
|
||||
<p className="auth-hint">演示账号:demo@nomadro.com / demo123</p>
|
||||
</div>
|
||||
</div>
|
||||
</SiteShell>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const SITE = process.env.NEXT_PUBLIC_SITE_URL || "https://nomadweb.nomadro.com";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "NomadFlow · 数字游民旅居指南",
|
||||
short_name: "NomadFlow",
|
||||
name: "nomadro · 数字游民旅居指南",
|
||||
short_name: "nomadro",
|
||||
description: "探索全球旅居生活,发现最适合数字游民的目的地",
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import Link from "next/link";
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import SiteShell from "@/components/SiteShell";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "隐私政策 · NomadFlow",
|
||||
description: "NomadFlow 隐私政策与 Cookie 使用说明",
|
||||
title: "隐私政策 · nomadro",
|
||||
description: "nomadro 隐私政策与 Cookie 使用说明",
|
||||
};
|
||||
|
||||
export default function PrivacyPage() {
|
||||
@ -22,7 +22,7 @@ export default function PrivacyPage() {
|
||||
<section>
|
||||
<h2>我们收集什么</h2>
|
||||
<p>
|
||||
NomadFlow 可能收集你主动提供的信息(如邮箱订阅、注册账号),以及必要的技术信息(浏览器类型、访问页面)用于改进体验。
|
||||
nomadro 可能收集你主动提供的信息(如邮箱订阅、注册账号),以及必要的技术信息(浏览器类型、访问页面)用于改进体验。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@ -44,7 +44,7 @@ export default function PrivacyPage() {
|
||||
<h2>联系我们</h2>
|
||||
<p>
|
||||
如有隐私相关问题,请联系:
|
||||
<a href="mailto:hello@nomadflow.io"> hello@nomadflow.io</a>
|
||||
<a href="mailto:hello@nomadro.com"> hello@nomadro.com</a>
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
@ -9,7 +9,7 @@ import type { Destination, ProfileStats, TripItem } from "@/lib/types";
|
||||
import SiteShell from "@/components/SiteShell";
|
||||
import FavoriteButton from "@/components/FavoriteButton";
|
||||
|
||||
const TRIP_KEY = "nomadflow-trip";
|
||||
const TRIP_KEY = "nomadro-trip";
|
||||
|
||||
interface Badge {
|
||||
emoji: string;
|
||||
@ -21,7 +21,7 @@ interface Badge {
|
||||
function getBadges(stats: ProfileStats | null, trip: TripItem[], favCount: number): Badge[] {
|
||||
const tripMonths = trip.reduce((s, t) => s + t.months, 0);
|
||||
return [
|
||||
{ emoji: "🌱", label: "初出茅庐", desc: "加入 NomadFlow", earned: true },
|
||||
{ emoji: "🌱", label: "初出茅庐", desc: "加入 nomadro", earned: true },
|
||||
{ emoji: "❤️", label: "收藏家", desc: "收藏 3+ 目的地", earned: favCount >= 3 },
|
||||
{ emoji: "🗺️", label: "行程达人", desc: "规划 3+ 城市行程", earned: trip.length >= 3 },
|
||||
{ emoji: "📅", label: "长期旅居", desc: "行程总计 6+ 个月", earned: tripMonths >= 6 },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useToast } from "@/lib/toast";
|
||||
@ -22,7 +22,7 @@ export default function Community({ testimonials }: { testimonials: Testimonial[
|
||||
const email = (form.elements.namedItem("email") as HTMLInputElement).value;
|
||||
try {
|
||||
await api.subscribe(email);
|
||||
toast("订阅成功!欢迎加入 NomadFlow 社区 🎉");
|
||||
toast("订阅成功!欢迎加入 nomadro 社区 🎉");
|
||||
form.reset();
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
44
frontend/src/components/DailyQuote.tsx
Normal file
44
frontend/src/components/DailyQuote.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const QUOTES = [
|
||||
{ text: "世界那么大,你的办公室可以在任何地方。", emoji: "🌍" },
|
||||
{ text: "不是逃离生活,而是用科技重新定义生活。", emoji: "💻" },
|
||||
{ text: "护照是最好的投资,好奇心是最好的指南针。", emoji: "🛂" },
|
||||
{ text: "在巴厘岛写代码,在里斯本开会议,在清迈看日落。", emoji: "☀️" },
|
||||
{ text: "慢下来,你会发现远方其实很近。", emoji: "✈️" },
|
||||
{ text: "远程工作不是躺平,而是选择自己的时区与节奏。", emoji: "🕐" },
|
||||
{ text: "背包里装的是衣服,心里装的是下一座城。", emoji: "🧳" },
|
||||
];
|
||||
|
||||
export default function DailyQuote() {
|
||||
const [idx, setIdx] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const day = Math.floor(Date.now() / 86400000);
|
||||
setIdx(day % QUOTES.length);
|
||||
}, []);
|
||||
|
||||
const q = QUOTES[idx];
|
||||
|
||||
return (
|
||||
<section className="section quote-section" id="quote">
|
||||
<div className="container">
|
||||
<div className="quote-card reveal">
|
||||
<span className="quote-emoji">{q.emoji}</span>
|
||||
<blockquote>“{q.text}”</blockquote>
|
||||
<div className="quote-footer">
|
||||
<span className="section-tag">✨ 今日 nomadro</span>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => setIdx((i) => (i + 1) % QUOTES.length)}
|
||||
>
|
||||
换一句 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useToast } from "@/lib/toast";
|
||||
import type { Destination } from "@/lib/types";
|
||||
|
||||
const STORAGE_KEY = "nomadflow-trip";
|
||||
const STORAGE_KEY = "nomadro-trip";
|
||||
const TZ_LABELS: Record<string, string> = {
|
||||
bali: "UTC+8", lisbon: "UTC+0", chiangmai: "UTC+7",
|
||||
mexico: "UTC-6", barcelona: "UTC+1", tokyo: "UTC+9",
|
||||
@ -52,7 +52,7 @@ export default function DestinationDetailClient({ dest, allDestinations }: Props
|
||||
const share = async () => {
|
||||
const url = window.location.href;
|
||||
if (navigator.share) {
|
||||
await navigator.share({ title: `${dest.name} - NomadFlow`, url });
|
||||
await navigator.share({ title: `${dest.name} - nomadro`, url });
|
||||
} else {
|
||||
await navigator.clipboard.writeText(url);
|
||||
toast("链接已复制到剪贴板 📋");
|
||||
|
||||
@ -1,14 +1,34 @@
|
||||
import Link from "next/link";
|
||||
import Link from "next/link";
|
||||
|
||||
const SITE = process.env.NEXT_PUBLIC_SITE_URL || "https://nomadweb.nomadro.com";
|
||||
|
||||
function BrandLogo() {
|
||||
return (
|
||||
<Link href="/" className="logo">
|
||||
<svg className="logo-icon" viewBox="0 0 40 40" fill="none" aria-hidden="true">
|
||||
<circle cx="20" cy="20" r="18" stroke="url(#footerLogoGrad)" strokeWidth="2.5" />
|
||||
<path d="M12 22c4-8 12-8 16 0" stroke="url(#footerLogoGrad)" strokeWidth="2" strokeLinecap="round" />
|
||||
<circle cx="28" cy="14" r="3" fill="url(#footerLogoGrad)" />
|
||||
<defs>
|
||||
<linearGradient id="footerLogoGrad" x1="0" y1="0" x2="40" y2="40">
|
||||
<stop offset="0%" stopColor="#FF6B6B" />
|
||||
<stop offset="50%" stopColor="#FFE66D" />
|
||||
<stop offset="100%" stopColor="#4ECDC4" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>nomadro</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="container">
|
||||
<div className="footer-grid">
|
||||
<div className="footer-brand">
|
||||
<Link href="/" className="logo"><span>NomadFlow</span></Link>
|
||||
<BrandLogo />
|
||||
<p>让每个人都能自由地工作和生活在这个星球上 🌏</p>
|
||||
<div className="social-links">
|
||||
<a href="https://twitter.com" target="_blank" rel="noopener noreferrer" aria-label="Twitter">𝕏</a>
|
||||
@ -21,19 +41,18 @@ export default function Footer() {
|
||||
<h4>探索</h4>
|
||||
<Link href="/#destinations">目的地</Link>
|
||||
<Link href="/#spin">命运转盘</Link>
|
||||
<Link href="/#cost-compare">省钱对比</Link>
|
||||
<Link href="/#calculator">费用计算器</Link>
|
||||
<Link href="/#wifi">网速评级</Link>
|
||||
<Link href="/#visa">签证指南</Link>
|
||||
<Link href="/#events">活动日历</Link>
|
||||
<Link href="/#blog">博客</Link>
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
<h4>工具</h4>
|
||||
<Link href="/#trip">行程规划</Link>
|
||||
<Link href="/#savings">启动金目标</Link>
|
||||
<Link href="/#flight">机票估算</Link>
|
||||
<Link href="/#calculator">费用计算器</Link>
|
||||
<Link href="/#timezone">时区看板</Link>
|
||||
<Link href="/#events">活动日历</Link>
|
||||
<Link href="/#nomad-score">就绪度测评</Link>
|
||||
<Link href="/#currency">汇率换算</Link>
|
||||
<a href={`${SITE}/docs`} target="_blank" rel="noopener noreferrer">API 文档</a>
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
@ -41,12 +60,12 @@ export default function Footer() {
|
||||
<Link href="/login">登录 / 注册</Link>
|
||||
<Link href="/profile">用户中心</Link>
|
||||
<Link href="/privacy">隐私政策</Link>
|
||||
<a href="mailto:hello@nomadflow.io">hello@nomadflow.io</a>
|
||||
<a href="mailto:hello@nomadro.com">hello@nomadro.com</a>
|
||||
<a href="https://gitea.dsx2020.com/eric/nomadweb" target="_blank" rel="noopener noreferrer">开源仓库 ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-bottom">
|
||||
<p>© 2026 NomadFlow · <a href={SITE}>{SITE.replace("https://", "")}</a></p>
|
||||
<p>© 2026 nomadro · <a href={SITE}>{SITE.replace("https://", "")}</a></p>
|
||||
<p className="footer-emoji-bar">🌴 ☀️ 🏄♂️ 💻 🌊 🗺️ ✈️ 🏝️ 🌅</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -37,6 +37,9 @@ import NomadTips from "./NomadTips";
|
||||
import SpinGlobe from "./SpinGlobe";
|
||||
import FlightCost from "./FlightCost";
|
||||
import NomadEvents from "./NomadEvents";
|
||||
import WifiWorkScore from "./WifiWorkScore";
|
||||
import SavingsGoal from "./SavingsGoal";
|
||||
import DailyQuote from "./DailyQuote";
|
||||
import KeyboardShortcuts from "./KeyboardShortcuts";
|
||||
import BackToTop from "./BackToTop";
|
||||
|
||||
@ -107,12 +110,15 @@ export default function HomeClient(props: Props) {
|
||||
<WorldMap destinations={props.destinations} />
|
||||
<Destinations destinations={props.destinations} onCompare={toggleCompare} compareList={compareList} onOpenMatcher={() => setMatcherOpen(true)} />
|
||||
<SpinGlobe destinations={props.destinations} />
|
||||
<DailyQuote />
|
||||
<TimezoneBoard destinations={props.destinations} />
|
||||
<TripPlanner destinations={props.destinations} />
|
||||
<Calculator destinations={props.destinations} />
|
||||
<CostCompare destinations={props.destinations} />
|
||||
<SavingsGoal destinations={props.destinations} />
|
||||
<FlightCost destinations={props.destinations} />
|
||||
<CurrencyConverter />
|
||||
<WifiWorkScore destinations={props.destinations} />
|
||||
<NomadScore />
|
||||
<Lifestyle />
|
||||
<Charts {...props.charts} />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
@ -24,7 +24,7 @@ export default function Loader() {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<p className="loader-text">NomadFlow</p>
|
||||
<p className="loader-text">nomadro</p>
|
||||
<div className="loader-bar"><div className="loader-progress" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
@ -79,7 +79,7 @@ export default function Navbar() {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>NomadFlow</span>
|
||||
<span>nomadro</span>
|
||||
</Link>
|
||||
<nav className={`nav-links${menuOpen ? " open" : ""}`}>
|
||||
{LINKS.map((l) => (
|
||||
|
||||
115
frontend/src/components/SavingsGoal.tsx
Normal file
115
frontend/src/components/SavingsGoal.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { Destination } from "@/lib/types";
|
||||
|
||||
const STORAGE_KEY = "nomadro-savings";
|
||||
|
||||
interface Props {
|
||||
destinations: Destination[];
|
||||
}
|
||||
|
||||
export default function SavingsGoal({ destinations }: Props) {
|
||||
const [target, setTarget] = useState(30000);
|
||||
const [saved, setSaved] = useState(8000);
|
||||
const [monthly, setMonthly] = useState(3000);
|
||||
const [slug, setSlug] = useState(destinations[0]?.slug || "");
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
const data = JSON.parse(raw);
|
||||
if (data.target) setTarget(data.target);
|
||||
if (data.saved != null) setSaved(data.saved);
|
||||
if (data.monthly) setMonthly(data.monthly);
|
||||
if (data.slug) setSlug(data.slug);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
setReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) return;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ target, saved, monthly, slug }));
|
||||
}, [target, saved, monthly, slug, ready]);
|
||||
|
||||
const dest = destinations.find((d) => d.slug === slug);
|
||||
const pct = Math.min(Math.round((saved / Math.max(target, 1)) * 100), 100);
|
||||
const remain = Math.max(target - saved, 0);
|
||||
const monthsLeft = monthly > 0 ? Math.ceil(remain / monthly) : 0;
|
||||
|
||||
const suggested = useMemo(() => {
|
||||
if (!dest) return target;
|
||||
// 机票粗估 + 3 个月生活费
|
||||
const flight = dest.cost > 8000 ? 6000 : 2800;
|
||||
return dest.cost * 3 + flight;
|
||||
}, [dest, target]);
|
||||
|
||||
return (
|
||||
<section className="section savings-section" id="savings">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<span className="section-tag">🎯 SAVINGS</span>
|
||||
<h2>旅居启动金目标</h2>
|
||||
<p>设定目标、追踪存款,算算还要多久才能出发</p>
|
||||
</div>
|
||||
|
||||
<div className="savings-card reveal">
|
||||
<div className="savings-form">
|
||||
<div className="calc-field">
|
||||
<label>🌍 目标城市</label>
|
||||
<select value={slug} onChange={(e) => setSlug(e.target.value)}>
|
||||
{destinations.map((d) => (
|
||||
<option key={d.slug} value={d.slug}>{d.emoji} {d.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="calc-field">
|
||||
<label>🎯 启动金目标: ¥{target.toLocaleString()}</label>
|
||||
<input type="range" min={10000} max={100000} step={1000} value={target} onChange={(e) => setTarget(+e.target.value)} />
|
||||
{dest && (
|
||||
<button className="btn btn-ghost btn-sm" type="button" onClick={() => setTarget(suggested)}>
|
||||
用 {dest.emoji} 建议值 ¥{suggested.toLocaleString()}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="calc-field">
|
||||
<label>💰 已存: ¥{saved.toLocaleString()}</label>
|
||||
<input type="range" min={0} max={target} step={500} value={Math.min(saved, target)} onChange={(e) => setSaved(+e.target.value)} />
|
||||
</div>
|
||||
<div className="calc-field">
|
||||
<label>📈 每月可存: ¥{monthly.toLocaleString()}</label>
|
||||
<input type="range" min={500} max={20000} step={500} value={monthly} onChange={(e) => setMonthly(+e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="savings-result">
|
||||
<div className="savings-ring" style={{ background: `conic-gradient(var(--accent-3) ${pct}%, rgba(255,255,255,0.08) 0)` }}>
|
||||
<div className="savings-ring-inner">
|
||||
<strong>{pct}%</strong>
|
||||
<small>完成</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="savings-stats">
|
||||
<div>
|
||||
<small>还差</small>
|
||||
<strong className="gradient-text">¥{remain.toLocaleString()}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<small>预计出发</small>
|
||||
<strong>{remain === 0 ? "随时可以!" : `${monthsLeft} 个月后`}</strong>
|
||||
</div>
|
||||
{dest && (
|
||||
<p className="savings-hint">
|
||||
{dest.emoji} {dest.name} 月均约 ¥{dest.cost.toLocaleString()},建议备 3 个月缓冲 + 机票
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -10,7 +10,9 @@ const SECTIONS = [
|
||||
{ id: "trip", emoji: "🗓️", label: "行程" },
|
||||
{ id: "calculator", emoji: "🧮", label: "计算器" },
|
||||
{ id: "cost-compare", emoji: "💰", label: "省钱" },
|
||||
{ id: "savings", emoji: "🎯", label: "存款" },
|
||||
{ id: "flight", emoji: "✈️", label: "机票" },
|
||||
{ id: "wifi", emoji: "📶", label: "网速" },
|
||||
{ id: "lifestyle", emoji: "💻", label: "生活" },
|
||||
{ id: "visa", emoji: "📋", label: "签证" },
|
||||
{ id: "coworking", emoji: "💻", label: "办公" },
|
||||
|
||||
85
frontend/src/components/WifiWorkScore.tsx
Normal file
85
frontend/src/components/WifiWorkScore.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, type CSSProperties } from "react";
|
||||
import Link from "next/link";
|
||||
import type { Destination } from "@/lib/types";
|
||||
|
||||
function workLevel(speed: number) {
|
||||
if (speed >= 150) return { label: "极速办公", emoji: "🚀", color: "#6ee7b7", tip: "视频会议、大文件传输无压力" };
|
||||
if (speed >= 80) return { label: "流畅办公", emoji: "✅", color: "#4ECDC4", tip: "日常远程工作完全够用" };
|
||||
if (speed >= 40) return { label: "基本可用", emoji: "⚠️", color: "#FFE66D", tip: "建议准备热点备份" };
|
||||
return { label: "需谨慎", emoji: "🐢", color: "#FF6B6B", tip: "优先选择联合办公或咖啡厅" };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
destinations: Destination[];
|
||||
}
|
||||
|
||||
export default function WifiWorkScore({ destinations }: Props) {
|
||||
const [slug, setSlug] = useState(destinations[0]?.slug || "");
|
||||
const dest = destinations.find((d) => d.slug === slug) || destinations[0];
|
||||
const level = useMemo(() => workLevel(dest?.speed ?? 0), [dest]);
|
||||
|
||||
const ranked = useMemo(
|
||||
() => [...destinations].sort((a, b) => b.speed - a.speed),
|
||||
[destinations]
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="section wifi-section" id="wifi">
|
||||
<div className="container">
|
||||
<div className="section-header reveal">
|
||||
<span className="section-tag">📶 WIFI SCORE</span>
|
||||
<h2>远程办公网速评级</h2>
|
||||
<p>选城先看网——评估目的地是否适合稳定远程工作</p>
|
||||
</div>
|
||||
|
||||
<div className="wifi-layout reveal">
|
||||
<div className="wifi-picker">
|
||||
<div className="calc-field">
|
||||
<label>🏙️ 选择城市</label>
|
||||
<select value={slug} onChange={(e) => setSlug(e.target.value)}>
|
||||
{destinations.map((d) => (
|
||||
<option key={d.slug} value={d.slug}>{d.emoji} {d.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{dest && (
|
||||
<div className="wifi-meter">
|
||||
<div className="wifi-meter-ring" style={{ "--wifi-color": level.color } as CSSProperties}>
|
||||
<strong>{dest.speed}</strong>
|
||||
<small>Mbps</small>
|
||||
</div>
|
||||
<div className="wifi-meter-info">
|
||||
<h3>{dest.emoji} {dest.name}</h3>
|
||||
<span className="wifi-level" style={{ color: level.color }}>
|
||||
{level.emoji} {level.label}
|
||||
</span>
|
||||
<p>{level.tip}</p>
|
||||
<Link href={`/destinations/${dest.slug}`} className="wifi-link">查看城市详情 →</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="wifi-rank">
|
||||
<h4>🏆 网速排行</h4>
|
||||
<ul>
|
||||
{ranked.map((d, i) => {
|
||||
const lv = workLevel(d.speed);
|
||||
return (
|
||||
<li key={d.slug} className={d.slug === slug ? "active" : ""} onClick={() => setSlug(d.slug)}>
|
||||
<span className="wifi-rank-num">{i + 1}</span>
|
||||
<span>{d.emoji} {d.name}</span>
|
||||
<span className="wifi-rank-speed" style={{ color: lv.color }}>{d.speed} Mbps</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
16
index.html
16
index.html
@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NomadFlow · 数字游民旅居指南</title>
|
||||
<title>nomadro · 数字游民旅居指南</title>
|
||||
<meta name="description" content="探索全球旅居生活,发现最适合数字游民的目的地、工具与社区。" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
@ -24,7 +24,7 @@
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<p class="loader-text">NomadFlow</p>
|
||||
<p class="loader-text">nomadro</p>
|
||||
<div class="loader-bar"><div class="loader-progress"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -48,7 +48,7 @@
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>NomadFlow</span>
|
||||
<span>nomadro</span>
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="#map" data-section="map">🗺️ 地图</a>
|
||||
@ -844,7 +844,7 @@
|
||||
<div class="footer-brand">
|
||||
<a href="#" class="logo">
|
||||
<svg class="logo-icon" viewBox="0 0 40 40" fill="none"><circle cx="20" cy="20" r="18" stroke="url(#logoGrad2)" stroke-width="2.5"/><path d="M12 22c4-8 12-8 16 0" stroke="url(#logoGrad2)" stroke-width="2" stroke-linecap="round"/><circle cx="28" cy="14" r="3" fill="url(#logoGrad2)"/><defs><linearGradient id="logoGrad2" x1="0" y1="0" x2="40" y2="40"><stop offset="0%" stop-color="#FF6B6B"/><stop offset="100%" stop-color="#4ECDC4"/></linearGradient></defs></svg>
|
||||
<span>NomadFlow</span>
|
||||
<span>nomadro</span>
|
||||
</a>
|
||||
<p>让每个人都能自由地工作和生活在这个星球上 🌏</p>
|
||||
<div class="social-links">
|
||||
@ -873,11 +873,11 @@
|
||||
<a href="#">关于我们</a>
|
||||
<a href="#">合作伙伴</a>
|
||||
<a href="#">隐私政策</a>
|
||||
<a href="#">hello@nomadflow.io</a>
|
||||
<a href="#">hello@nomadro.com</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 NomadFlow. Made with ❤️ for Digital Nomads everywhere.</p>
|
||||
<p>© 2026 nomadro. Made with ❤️ for Digital Nomads everywhere.</p>
|
||||
<p class="footer-emoji-bar">🌴 ☀️ 🏄♂️ 💻 🌊 🗺️ ✈️ 🏝️ 🌅</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -886,7 +886,7 @@
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast">
|
||||
<span class="toast-icon">✅</span>
|
||||
<span class="toast-msg">订阅成功!欢迎加入 NomadFlow 社区 🎉</span>
|
||||
<span class="toast-msg">订阅成功!欢迎加入 nomadro 社区 🎉</span>
|
||||
</div>
|
||||
|
||||
<!-- 目的地详情弹窗 -->
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "nomadflow",
|
||||
{
|
||||
"name": "nomadro",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "powershell -ExecutionPolicy Bypass -File scripts/dev.ps1",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user