diff --git a/README.md b/README.md index 7ef79e7..d1e837b 100644 --- a/README.md +++ b/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 ``` diff --git a/backend/app/config.py b/backend/app/config.py index 0b32bad..160894a 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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 diff --git a/backend/app/data/mock_data.py b/backend/app/data/mock_data.py index 318fbb4..1f66f07 100644 --- a/backend/app/data/mock_data.py +++ b/backend/app/data/mock_data.py @@ -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] = { diff --git a/backend/app/main.py b/backend/app/main.py index b0189f4..db20031 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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", } diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py index 9145e81..072ca64 100644 --- a/backend/app/services/auth.py +++ b/backend/app/services/auth.py @@ -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 "" diff --git a/backend/app/services/pocketbase.py b/backend/app/services/pocketbase.py index 2719198..8541667 100644 --- a/backend/app/services/pocketbase.py +++ b/backend/app/services/pocketbase.py @@ -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 { diff --git a/backend/seed_pocketbase.py b/backend/seed_pocketbase.py index f890a64..77a2f46 100644 --- a/backend/seed_pocketbase.py +++ b/backend/seed_pocketbase.py @@ -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 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index a4690bc..153693f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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: diff --git a/docker-compose.yml b/docker-compose.yml index ea2fdfb..bf99f97 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 4bb8e0a..7b3b242 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -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; + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 904ebb8..203fc88 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -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", diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 8896e43..05143b6 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -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() { ← 返回首页
{mode === "login" ? "登录你的游民账户" : "开始你的旅居之旅"}
演示账号:demo@nomadflow.io / demo123
+演示账号:demo@nomadro.com / demo123
diff --git a/frontend/src/app/manifest.ts b/frontend/src/app/manifest.ts index 08663ae..52cdd80 100644 --- a/frontend/src/app/manifest.ts +++ b/frontend/src/app/manifest.ts @@ -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", diff --git a/frontend/src/app/privacy/page.tsx b/frontend/src/app/privacy/page.tsx index 4ed26e7..46ef292 100644 --- a/frontend/src/app/privacy/page.tsx +++ b/frontend/src/app/privacy/page.tsx @@ -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() {- NomadFlow 可能收集你主动提供的信息(如邮箱订阅、注册账号),以及必要的技术信息(浏览器类型、访问页面)用于改进体验。 + nomadro 可能收集你主动提供的信息(如邮箱订阅、注册账号),以及必要的技术信息(浏览器类型、访问页面)用于改进体验。
如有隐私相关问题,请联系: - hello@nomadflow.io + hello@nomadro.com
diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index 5f31856..eb4fc76 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -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 }, diff --git a/frontend/src/components/Community.tsx b/frontend/src/components/Community.tsx index 002493c..92c5fbb 100644 --- a/frontend/src/components/Community.tsx +++ b/frontend/src/components/Community.tsx @@ -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 */ } }; diff --git a/frontend/src/components/DailyQuote.tsx b/frontend/src/components/DailyQuote.tsx new file mode 100644 index 0000000..dd1851f --- /dev/null +++ b/frontend/src/components/DailyQuote.tsx @@ -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 ( +“{q.text}”+