diff --git a/app/about/page.tsx b/app/about/page.tsx index fdb565c..561f060 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,25 +1,27 @@ import Link from "next/link"; -import { SiteHeader } from "~/components/layout/site-header"; -import { SiteFooter } from "~/components/layout/site-footer"; export default function AboutPage() { return ( -
- -
-

关于 Nomadro

-

- Nomadro 是中文数字游民操作系统,致力于帮助更多人从固定工位走向自由生活与自由收入。 - 我们提供城市探索、学习路径、会员社区三大核心模块,从地点、技能、工具到社群,构建完整的数字游民支持体系。 +

+
+

关于 Nomadro

+

+ Nomadro 是中文数字游民操作系统,提供游民导航、地理套利、直播工具箱等模块, + 帮助更多人从固定工位走向自由生活与自由收入。

← 返回首页 -
- +
); } diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 9ded534..84cc2e4 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,32 +1,44 @@ import Link from "next/link"; -import { SiteHeader } from "~/components/layout/site-header"; -import { SiteFooter } from "~/components/layout/site-footer"; export default function ContactPage() { return ( -
- -
-

联系我们

-

+

+
+

联系我们

+

加入数字游民社群,获取最新动态与交流机会。

- - 加入社群 - +
+ + 🎬 直播工具箱 + + + 👑 会员社群 + +
← 返回首页 -
- +
); } diff --git a/app/globals.css b/app/globals.css index 16eea3b..5b03fa1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,8 +1,10 @@ @import "tailwindcss"; :root { - --background: #ffffff; + --background: #f8f7fc; --foreground: #171717; + --safe-top: env(safe-area-inset-top, 0px); + --safe-bottom: env(safe-area-inset-bottom, 0px); } @theme inline { @@ -13,72 +15,22 @@ --font-mono: var(--font-geist-mono), ui-monospace, monospace; } +html { + height: 100%; +} + body { + min-height: 100%; background: var(--background); color: var(--foreground); font-family: var(--font-sans); + -webkit-tap-highlight-color: transparent; + -webkit-font-smoothing: antialiased; } -@keyframes float { - 0%, 100% { - transform: translate(0, 0) scale(1); - opacity: 0.6; - } - 25% { - transform: translate(10px, -20px) scale(1.1); - opacity: 0.9; - } - 50% { - transform: translate(-15px, 10px) scale(0.9); - opacity: 0.7; - } - 75% { - transform: translate(5px, 15px) scale(1.05); - opacity: 0.8; - } -} - -@keyframes shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; - } -} - -@keyframes pulse-glow { - 0%, 100% { - box-shadow: 0 0 20px rgba(139, 92, 246, 0.3); - } - 50% { - box-shadow: 0 0 40px rgba(139, 92, 246, 0.5); - } -} - -@keyframes gradient-shift { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.animate-shimmer { - background: linear-gradient( - 90deg, - transparent 0%, - rgba(255, 255, 255, 0.4) 50%, - transparent 100% - ); - background-size: 200% 100%; - animation: shimmer 2s infinite; -} - -.animate-pulse-glow { - animation: pulse-glow 3s ease-in-out infinite; +.text-gradient { + background: linear-gradient(135deg, #7c3aed 0%, #0891b2 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; } diff --git a/app/layout.tsx b/app/layout.tsx index 7140935..33d858c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,28 +1,39 @@ import "./globals.css"; -import { type Metadata } from "next"; +import { type Metadata, type Viewport } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 5, + viewportFit: "cover", + themeColor: "#f8f7fc", +}; + export const metadata: Metadata = { + metadataBase: new URL("https://nomadro.com"), title: "Nomadro | 中文数字游民操作系统", description: - "从城市、技能、工具到社群,一站开始你的数字游民生活。探索城市、学习路径、会员社区,帮你从固定工位走向自由生活与自由收入。", + "从城市探索到远程收入,从 AI 自动化到直播变现。游民导航、地理套利、直播工具箱,一站开启你的自由生活。", keywords: [ "数字游民", "远程工作", "旅居", "自由职业", - "城市探索", + "直播工具箱", + "地理套利", "学习路径", "会员社区", ], openGraph: { title: "Nomadro | 中文数字游民操作系统", description: - "从城市、技能、工具到社群,一站开始你的数字游民生活。", + "从城市探索到远程收入,从 AI 自动化到直播变现,一站开启你的自由生活。", type: "website", + url: "https://nomadro.live", }, - icons: [{ rel: "icon", url: "/nomadro.ico" }], + icons: [{ rel: "icon", url: "/favicon.ico" }], }; const geist = Geist({ diff --git a/app/page.tsx b/app/page.tsx index 2b448cf..4b05990 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,10 +2,19 @@ import { Hero } from "~/components/sections/hero"; export default function HomePage() { return ( -
-
+
+
+
+ © {new Date().getFullYear()} Nomadro +
); } diff --git a/components/layout/site-footer.tsx b/components/layout/site-footer.tsx index 70aeb69..61a73dc 100644 --- a/components/layout/site-footer.tsx +++ b/components/layout/site-footer.tsx @@ -1,76 +1,31 @@ import Link from "next/link"; -import { NAV_LINKS, ENTRY_PATHS } from "~/lib/data"; +import { ENTRY_PATHS } from "~/lib/data"; export function SiteFooter() { return ( -
-
-
-
- - Nomadro +
+
+

+ © {new Date().getFullYear()} Nomadro · 数字游民操作系统 +

+
+ {ENTRY_PATHS.map((path) => ( + + {path.emoji} {path.title.split(" · ")[0]} -

- 中文数字游民操作系统。从城市、技能、工具到社群,一站开始你的自由生活与自由收入。 -

-
-
-

- 核心入口 -

-
    - {ENTRY_PATHS.map((path) => ( -
  • - - {path.title} - -
  • - ))} -
-
-
-

- 关于 -

-
    -
  • - - 关于我们 - -
  • -
  • - - 联系我们 - -
  • -
  • - - 加入社群 - -
  • -
-
-
-
-

- © {new Date().getFullYear()} Nomadro. Digital Nomad OS · 数字游民操作系统 -

+ ))} + + 关于 + + + 联系 +
diff --git a/components/layout/site-header.tsx b/components/layout/site-header.tsx index 966a120..c963c50 100644 --- a/components/layout/site-header.tsx +++ b/components/layout/site-header.tsx @@ -1,116 +1,13 @@ -"use client"; - import Link from "next/link"; -import { useState, useEffect } from "react"; -import { Menu, X } from "lucide-react"; -import { motion, AnimatePresence } from "framer-motion"; -import { NAV_LINKS } from "~/lib/data"; -import { Button } from "~/components/ui/button"; export function SiteHeader() { - const [mobileOpen, setMobileOpen] = useState(false); - const [scrolled, setScrolled] = useState(false); - - useEffect(() => { - const onScroll = () => setScrolled(window.scrollY > 20); - window.addEventListener("scroll", onScroll, { passive: true }); - return () => window.removeEventListener("scroll", onScroll); - }, []); - return ( -
-
- - - Nomadro - - - Digital Nomad OS / 数字游民操作系统 - +
+
+ + Nomadro - - - -
- - - {mobileOpen && ( - -
- Nomadro - -
- -
- )} -
); } diff --git a/components/sections/content-preview.tsx b/components/sections/content-preview.tsx index 1b45416..3da5a15 100644 --- a/components/sections/content-preview.tsx +++ b/components/sections/content-preview.tsx @@ -9,7 +9,7 @@ const TAG_COLORS: Record = { 城市: "bg-blue-50 text-blue-700", 专题: "bg-emerald-50 text-emerald-700", 工具: "bg-amber-50 text-amber-700", - 活动: "bg-rose-50 text-rose-700", + 直播: "bg-rose-500/15 text-rose-300 ring-1 ring-rose-400/20", 学习路径: "bg-violet-50 text-violet-700", 会员: "bg-neutral-100 text-neutral-700", }; diff --git a/components/sections/entry-grid.tsx b/components/sections/entry-grid.tsx index 2fd798d..647f7b4 100644 --- a/components/sections/entry-grid.tsx +++ b/components/sections/entry-grid.tsx @@ -1,82 +1,96 @@ "use client"; import Link from "next/link"; -import { MapPin, BookOpen, Crown, Zap, Users, ArrowRight } from "lucide-react"; +import { MapPin, Zap, Radio, ArrowRight } from "lucide-react"; import { motion } from "framer-motion"; import { ENTRY_PATHS } from "~/lib/data"; const ICON_MAP = { MapPin, - BookOpen, - Crown, Zap, - Users, + Radio, } as const; const ACCENT_STYLES = { - blue: "border-l-4 border-l-blue-500 bg-gradient-to-r from-blue-50/80 to-white hover:from-blue-100/80", - green: "border-l-4 border-l-emerald-500 bg-gradient-to-r from-emerald-50/80 to-white hover:from-emerald-100/80", - amber: "border-l-4 border-l-amber-500 bg-gradient-to-r from-amber-50/80 to-white hover:from-amber-100/80", + blue: { + border: "border-blue-400/20 hover:border-blue-400/40", + glow: "hover:shadow-blue-500/10", + icon: "from-blue-500 to-cyan-500", + text: "text-blue-300", + }, + green: { + border: "border-emerald-400/20 hover:border-emerald-400/40", + glow: "hover:shadow-emerald-500/10", + icon: "from-emerald-500 to-teal-500", + text: "text-emerald-300", + }, + rose: { + border: "border-rose-400/20 hover:border-rose-400/40", + glow: "hover:shadow-rose-500/10", + icon: "from-rose-500 to-pink-500", + text: "text-rose-300", + }, } as const; -const CARD_EMOJI: Record = { - nav: "🧭", - arbitrage: "⚡", - salon: "🎯", -}; - export function EntryGrid() { return ( -
+
- - 🚀 选择你的入口 - - - 三大路径,对应不同的需求和阶段 - + 🚀 +

+ 选择你的入口 +

+

+ 三大路径,对应不同的需求和阶段 +

+ -
+
{ENTRY_PATHS.map((path, i) => { - const Icon = ICON_MAP[path.icon as keyof typeof ICON_MAP] ?? MapPin; + const Icon = ICON_MAP[path.icon]; const accent = ACCENT_STYLES[path.accent]; - const emoji = CARD_EMOJI[path.id] ?? "✨"; return ( - {emoji} - -

+
+ {path.emoji} + {path.id === "live" && ( + + + nomadro.live + + )} +
+
+ +
+

{path.title}

-

+

{path.description}

- + {path.cta} - +
diff --git a/components/sections/final-cta.tsx b/components/sections/final-cta.tsx index d50b6b5..e231284 100644 --- a/components/sections/final-cta.tsx +++ b/components/sections/final-cta.tsx @@ -1,61 +1,72 @@ "use client"; import Link from "next/link"; -import { MapPin, BookOpen, Crown } from "lucide-react"; import { motion } from "framer-motion"; import { FINAL_CTAS } from "~/lib/data"; import { Button } from "~/components/ui/button"; -const ICONS = [MapPin, BookOpen, Crown] as const; - export function FinalCta() { return ( -
-
- + {/* Background */} +
+
+ +
+ - ✨ 从这里开始你的数字游民路径 - - - 先选择最适合你的入口,然后开始行动。 - + +

+ 从这里开始你的数字游民路径 +

+

+ 先选择最适合你的入口,然后开始行动。 +

+
- {FINAL_CTAS.map((cta, i) => { - const Icon = ICONS[i]; - return ( + {FINAL_CTAS.map((cta, i) => ( + - ); - })} + + ))}
diff --git a/components/sections/hero.tsx b/components/sections/hero.tsx index b5ee9f5..f475ce5 100644 --- a/components/sections/hero.tsx +++ b/components/sections/hero.tsx @@ -1,109 +1,97 @@ "use client"; import Link from "next/link"; -import { MapPin, Zap, Users, ArrowRight } from "lucide-react"; -import { motion } from "framer-motion"; +import { ChevronRight } from "lucide-react"; import { ENTRY_CARDS } from "~/lib/data"; -import { Particles } from "~/components/ui/particles"; -const ICON_MAP = { - MapPin, - Zap, - Users, -} as const; - -const ACCENT_STYLES = { - blue: "from-blue-500/20 to-cyan-500/10 border-blue-300/80 hover:border-blue-400 hover:shadow-2xl", - emerald: "from-emerald-500/20 to-teal-500/10 border-emerald-300/80 hover:border-emerald-400 hover:shadow-2xl", - amber: "from-amber-500/20 to-orange-500/10 border-amber-300/80 hover:border-amber-400 hover:shadow-2xl", -} as const; - -const CARD_EMOJI = { - nav: "🧭", - arbitrage: "⚡", - salon: "🎯", -} as const; - -const ICON_COLORS = { - blue: "text-blue-600", - emerald: "text-emerald-600", - amber: "text-amber-600", +const CARD_STYLE = { + blue: { + border: "border-blue-200/80", + bar: "bg-blue-500", + emoji: "bg-blue-50", + }, + emerald: { + border: "border-emerald-200/80", + bar: "bg-emerald-500", + emoji: "bg-emerald-50", + }, + rose: { + border: "border-rose-200/80", + bar: "bg-rose-500", + emoji: "bg-rose-50", + }, } as const; export function Hero() { return ( -
- -
-
-
- -

- - 数字游民 - {" "} - nomadro -

-
+
+
+ {/* 标题 */} +
+

+ 数字游民 +

+

+ nomadro +

+

+ 游民导航 · 地理套利 · 直播工具箱 +
+ 一站开启自由生活 +

+
- - {ENTRY_CARDS.map((card, i) => { - const Icon = ICON_MAP[card.icon]; - const accent = ACCENT_STYLES[card.accent]; - const emoji = CARD_EMOJI[card.id]; - const iconColor = ICON_COLORS[card.accent]; - return ( - + {ENTRY_CARDS.map((card) => { + const style = CARD_STYLE[card.accent]; + return ( + + + + - -
- {emoji} -
-
- -
-
-

- {card.label} -

-

- {card.title} -

-

- {card.description} -

-
- - -
- ); - })} -
+ {card.emoji} + + +
+
+

+ {card.label} +

+ {card.id === "live" && ( + + LIVE + + )} +
+

+ {card.title} +

+

+ {card.description} +

+
+ + + + ); + })}
diff --git a/components/sections/value-grid.tsx b/components/sections/value-grid.tsx index 72019c1..e780c52 100644 --- a/components/sections/value-grid.tsx +++ b/components/sections/value-grid.tsx @@ -18,29 +18,33 @@ const ACCENT_COLORS = [ "from-amber-500 to-orange-500", ] as const; - export function ValueGrid() { return ( -
+
+
- - 🎯 你能在这里获得什么 - - - 不是功能列表,而是实实在在的结果 ✨ - + 🎯 +

+ 你能在这里获得什么 +

+

+ 不是功能列表,而是实实在在的结果 ✨ +

+ -
+
{VALUE_PROPOSITIONS.map((item, i) => { const Icon = ICON_MAP[item.icon]; const gradient = ACCENT_COLORS[i]; @@ -52,15 +56,20 @@ export function ValueGrid() { viewport={{ once: true, margin: "-40px" }} transition={{ delay: i * 0.08 }} whileHover={{ y: -4 }} - className="group rounded-2xl border-2 border-neutral-200/80 bg-white p-6 shadow-sm transition-all hover:border-violet-200 hover:shadow-xl hover:shadow-violet-100/50" + className="group glass-card glass-card-hover rounded-2xl p-6" > -
- +
+
+ +
+ + {item.emoji} +
-

+

{item.title}

-

+

{item.description}

diff --git a/components/ui/aurora-background.tsx b/components/ui/aurora-background.tsx new file mode 100644 index 0000000..33603c7 --- /dev/null +++ b/components/ui/aurora-background.tsx @@ -0,0 +1,53 @@ +"use client"; + +export function AuroraBackground() { + return ( +
+ {/* Base gradient */} +
+ + {/* Aurora layers */} +
+
+
+ + {/* Grid overlay */} +
+ + {/* Scan line */} +
+
+ ); +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 5bdf491..4dd832d 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -2,25 +2,21 @@ import * as React from "react"; export interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: "default" | "outline" | "ghost" | "link"; + variant?: "default" | "outline" | "ghost"; size?: "default" | "sm" | "lg"; - asChild?: boolean; } const Button = React.forwardRef( ({ className = "", variant = "default", size = "default", ...props }, ref) => { const base = - "inline-flex items-center justify-center whitespace-nowrap rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"; + "inline-flex items-center justify-center rounded-xl font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 disabled:pointer-events-none disabled:opacity-50"; const variants = { - default: - "bg-neutral-900 text-white hover:bg-neutral-800 focus-visible:ring-neutral-900", - outline: - "border border-neutral-200 bg-transparent hover:bg-neutral-50 focus-visible:ring-neutral-400", - ghost: "hover:bg-neutral-100 focus-visible:ring-neutral-400", - link: "text-neutral-900 underline-offset-4 hover:underline focus-visible:ring-neutral-400", + default: "bg-violet-600 text-white hover:bg-violet-700 active:bg-violet-800", + outline: "border border-neutral-200 text-neutral-700 hover:bg-neutral-50", + ghost: "text-neutral-600 hover:bg-neutral-100", }; const sizes = { - default: "h-10 px-4 py-2 text-sm", + default: "h-10 px-4 text-sm", sm: "h-8 px-3 text-xs", lg: "h-12 px-6 text-base", }; diff --git a/components/ui/floating-emojis.tsx b/components/ui/floating-emojis.tsx new file mode 100644 index 0000000..e97fc86 --- /dev/null +++ b/components/ui/floating-emojis.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { FLOATING_EMOJIS } from "~/lib/data"; + +const POSITIONS = [ + { left: "8%", top: "18%", size: "text-2xl", delay: 0 }, + { left: "85%", top: "12%", size: "text-3xl", delay: 1 }, + { left: "15%", top: "65%", size: "text-xl", delay: 2 }, + { left: "78%", top: "55%", size: "text-2xl", delay: 0.5 }, + { left: "5%", top: "42%", size: "text-lg", delay: 1.5 }, + { left: "92%", top: "38%", size: "text-xl", delay: 2.5 }, + { left: "25%", top: "8%", size: "text-lg", delay: 3 }, + { left: "65%", top: "78%", size: "text-2xl", delay: 1.2 }, + { left: "45%", top: "88%", size: "text-lg", delay: 0.8 }, + { left: "55%", top: "5%", size: "text-xl", delay: 2.2 }, +] as const; + +export function FloatingEmojis() { + return ( +
+ {POSITIONS.map((pos, i) => ( + + {FLOATING_EMOJIS[i]} + + ))} +
+ ); +} diff --git a/components/ui/globe-animation.tsx b/components/ui/globe-animation.tsx new file mode 100644 index 0000000..a0b70a8 --- /dev/null +++ b/components/ui/globe-animation.tsx @@ -0,0 +1,117 @@ +"use client"; + +export function GlobeAnimation() { + return ( +
+
+ {/* Orbit ring 1 */} +
+ + + + + + + + + + + + + +
+ + {/* Orbit ring 2 */} +
+ + + + + + + + + + + + +
+ + {/* Globe */} +
+ + + + + + + + + + + + + + + {/* Latitude lines */} + {[30, 45, 60, 75, 90].map((lat) => { + const ry = Math.sin((lat * Math.PI) / 180) * 55; + return ( + + ); + })} + {/* Longitude lines */} + {[0, 30, 60, 90, 120, 150].map((lon) => ( + + ))} + {/* Glow dot */} + + + + + +
+
+
+ ); +} diff --git a/components/ui/particles.tsx b/components/ui/particles.tsx index be39e0b..dc53858 100644 --- a/components/ui/particles.tsx +++ b/components/ui/particles.tsx @@ -1,11 +1,20 @@ "use client"; -const PARTICLES = Array.from({ length: 50 }, (_, i) => ({ - size: 4 + (i % 6), +const COLOR_MAP = { + cyan: "from-cyan-400/60 to-cyan-300/30", + violet: "from-violet-400/60 to-violet-300/30", + fuchsia: "from-fuchsia-400/60 to-fuchsia-300/30", +} as const; + +type ParticleColor = keyof typeof COLOR_MAP; + +const PARTICLES = Array.from({ length: 60 }, (_, i) => ({ + size: 2 + (i % 5), left: (i * 17.3) % 100, top: (i * 23.7) % 100, - duration: 10 + (i % 8), - delay: (i * 0.3) % 5, + duration: 12 + (i % 10), + delay: (i * 0.4) % 6, + color: (i % 3 === 0 ? "cyan" : i % 3 === 1 ? "violet" : "fuchsia") as ParticleColor, })); export function Particles() { @@ -14,13 +23,13 @@ export function Particles() { {PARTICLES.map((p, i) => (
diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..8cc3440 --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,4 @@ +nomadro.com, www.nomadro.com { + encode gzip + reverse_proxy 127.0.0.1:4003 +} diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..2e94eaf --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# 日常更新部署 +set -euo pipefail + +APP_DIR="/var/www/nomadro.com" +BRANCH="${BRANCH:-test}" + +cd "$APP_DIR" +git fetch origin +git checkout "$BRANCH" +git pull origin "$BRANCH" +pnpm install --frozen-lockfile +pnpm build +systemctl restart nomadro +echo "Deployed $(git rev-parse --short HEAD)" diff --git a/deploy/nomadro.service b/deploy/nomadro.service new file mode 100644 index 0000000..53ae8f8 --- /dev/null +++ b/deploy/nomadro.service @@ -0,0 +1,15 @@ +[Unit] +Description=Nomadro Next.js App +After=network.target + +[Service] +Type=simple +WorkingDirectory=/var/www/nomadro.com +Environment=PORT=4003 +Environment=NODE_ENV=production +ExecStart=/usr/bin/pnpm start +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/setup-server.sh b/deploy/setup-server.sh new file mode 100644 index 0000000..629eea5 --- /dev/null +++ b/deploy/setup-server.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# 首次部署:安装依赖、克隆代码、配置 Caddy + systemd +# 不影响 nginx:23794 / sing-box 等现有业务 +set -euo pipefail + +APP_DIR="/var/www/nomadro.com" +REPO_URL="${REPO_URL:-https://gitea.dsx2020.com/eric/gitlab-instance-0a899031_nomadro.git}" +BRANCH="${BRANCH:-test}" +GIT_USER="${GIT_USER:-eric}" +GIT_PASS="${GIT_PASS:?Set GIT_PASS}" + +echo "==> Install Node.js 20" +if ! command -v node >/dev/null 2>&1; then + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y nodejs +fi + +echo "==> Install pnpm" +if ! command -v pnpm >/dev/null 2>&1; then + corepack enable + corepack prepare pnpm@10.25.0 --activate +fi + +echo "==> Install Caddy" +if ! command -v caddy >/dev/null 2>&1; then + apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl + curl -1sLf 'https://dl.caddyserver.com/api/download/installer/deb/caddy-stable/caddy-stable.list' \ + | tee /etc/apt/sources.list.d/caddy-stable.list + curl -1sLf 'https://dl.caddyserver.com/api/download/installer/deb/caddy-stable/caddy-stable-archive-keyring.gpg' \ + | tee /usr/share/keyrings/caddy-stable-archive-keyring.gpg >/dev/null + apt-get update + apt-get install -y caddy +fi + +echo "==> Clone or update app" +mkdir -p /var/www +if [ -d "$APP_DIR/.git" ]; then + cd "$APP_DIR" + git fetch origin + git checkout "$BRANCH" + git pull origin "$BRANCH" +else + git clone -b "$BRANCH" "https://${GIT_USER}:${GIT_PASS}@${REPO_URL#https://}" "$APP_DIR" + cd "$APP_DIR" +fi + +echo "==> Build app" +pnpm install --frozen-lockfile +pnpm build + +echo "==> Configure Caddy (only nomadro.com, keep nginx untouched)" +CADDY_MAIN="/etc/caddy/Caddyfile" +SNIPPET="# nomadro.com - managed by nomadro deploy" +if [ -f "$CADDY_MAIN" ] && grep -q "nomadro.com" "$CADDY_MAIN"; then + echo "Caddy nomadro.com block already exists" +else + if [ ! -f "$CADDY_MAIN" ] || [ ! -s "$CADDY_MAIN" ]; then + cp "$APP_DIR/deploy/Caddyfile" "$CADDY_MAIN" + else + echo "" >> "$CADDY_MAIN" + echo "$SNIPPET" >> "$CADDY_MAIN" + cat "$APP_DIR/deploy/Caddyfile" >> "$CADDY_MAIN" + fi +fi +caddy validate --config "$CADDY_MAIN" +systemctl enable caddy +systemctl reload caddy || systemctl restart caddy + +echo "==> Configure systemd service" +cp "$APP_DIR/deploy/nomadro.service" /etc/systemd/system/nomadro.service +systemctl daemon-reload +systemctl enable nomadro +systemctl restart nomadro + +echo "==> Done" +systemctl status nomadro --no-pager | head -10 +systemctl status caddy --no-pager | head -10 +ss -tlnp | grep -E ':80 |:443 |:4003 ' || true diff --git a/lib/data.ts b/lib/data.ts index 201d9d6..5c77479 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -7,21 +7,21 @@ export const NAV_LINKS = [ { href: "/", label: "首页", external: false }, { href: "https://nav.hackrobot.cn", label: "游民导航", external: true }, { href: "https://hackrobot.cn", label: "地理套利", external: true }, - { href: "https://salon.hackrobot.cn", label: "同城沙龙", external: true }, + { href: "https://nomadro.live", label: "直播工具箱", external: true }, { href: "https://digital.hackrobot.cn", label: "学习路径", external: true }, { href: "https://vip.hackrobot.cn", label: "会员社区", external: true }, { href: "/about", label: "关于", external: false }, ] as const; export const HERO_TAGS = [ - "城市探索", - "地理套利", - "AI自动化", - "社群连接", - "同城沙龙", + "🌍 全球旅居", + "💰 地理套利", + "🤖 AI 自动化", + "🎬 直播变现", + "🧭 游民导航", ] as const; -/** 入口页主卡片:游民导航、地理套利、同城沙龙 */ +/** 入口页主卡片:游民导航、地理套利、直播工具箱 */ export const ENTRY_CARDS = [ { id: "nav", @@ -31,24 +31,27 @@ export const ENTRY_CARDS = [ href: "https://nav.hackrobot.cn", icon: "MapPin", accent: "blue", + emoji: "🧭", }, { id: "arbitrage", label: "地理套利", - title: "AI自动化杠杆", + title: "AI 自动化杠杆", description: "低成本工作室、自建云手机、自动化系统", href: "https://hackrobot.cn", icon: "Zap", accent: "emerald", + emoji: "⚡", }, { - id: "salon", - label: "同城沙龙", - title: "线下连接与交流", - description: "城市聚会、主题沙龙、同频人面对面", - href: "https://salon.hackrobot.cn", - icon: "Users", - accent: "amber", + id: "live", + label: "直播工具箱", + title: "随时随地开播", + description: "推流工具、场景模板、直播变现一站搞定", + href: "https://nomadro.live", + icon: "Radio", + accent: "rose", + emoji: "🎬", }, ] as const; @@ -58,24 +61,28 @@ export const VALUE_PROPOSITIONS = [ icon: "MapPin", title: "找到适合长期旅居和远程工作的城市", description: "从大理到深圳,从成都到杭州,帮你筛选真正适合数字游民生活的目的地。", + emoji: "🏝️", }, { id: "income", icon: "TrendingUp", title: "建立可持续的远程技能与收入结构", description: "不是一夜暴富,而是可复制、可积累的技能变现路径。", + emoji: "📈", }, { id: "tools", icon: "Zap", title: "用工具、系统和自动化放大个人能力", description: "减少重复劳动,把时间用在真正重要的事情上。", + emoji: "🛠️", }, { id: "community", icon: "Users", title: "进入同频社区,减少孤独和试错成本", description: "找到同类人,分享经验,互相支持,少走弯路。", + emoji: "🤝", }, ] as const; @@ -88,24 +95,27 @@ export const ENTRY_PATHS = [ href: "https://nav.hackrobot.cn", icon: "MapPin", accent: "blue", + emoji: "🧭", }, { id: "arbitrage", - title: "地理套利 · AI自动化杠杆", + title: "地理套利 · AI 自动化杠杆", description: "适合想搭建低成本工作室、自建云手机、自动化系统的人", cta: "进入工作室", href: "https://hackrobot.cn", icon: "Zap", accent: "green", + emoji: "⚡", }, { - id: "salon", - title: "同城沙龙 · 线下连接", - description: "适合想参加城市聚会、主题沙龙、面对面交流的人", - cta: "探索同城活动", - href: "https://salon.hackrobot.cn", - icon: "Users", - accent: "amber", + id: "live", + title: "直播工具箱 · 随时随地开播", + description: "适合想用直播变现、搭建个人 IP、远程开播的数字游民", + cta: "进入直播工具箱", + href: "https://nomadro.live", + icon: "Radio", + accent: "rose", + emoji: "🎬", }, ] as const; @@ -114,21 +124,25 @@ export const METHODOLOGY_SYSTEMS = [ id: "location", title: "地点自由", description: "选择适合远程生活的城市,平衡成本、网速、社群与生活方式。", + emoji: "✈️", }, { id: "income", title: "收入结构", description: "建立不依赖固定地点的收入来源,技能、内容、自动化缺一不可。", + emoji: "💎", }, { id: "tools", title: "工具杠杆", description: "用系统和工具放大个人产出,把时间用在创造价值上。", + emoji: "⚙️", }, { id: "community", title: "社群连接", description: "同频人的归属感,减少孤独,加速成长,共享信息与机会。", + emoji: "🌐", }, ] as const; @@ -139,6 +153,7 @@ export const SCENARIOS = [ path: "先看学习路径", cta: "查看学习专题", href: "https://digital.hackrobot.cn", + emoji: "🌱", }, { id: "skill", @@ -146,13 +161,15 @@ export const SCENARIOS = [ path: "先看实战资源", cta: "进入学习与专题", href: "https://digital.hackrobot.cn", + emoji: "🚀", }, { - id: "city", - pain: "我想换个城市生活,但不知道先去哪", - path: "先看城市探索", - cta: "探索城市与社区", - href: "https://meetup.hackrobot.cn", + id: "live", + pain: "我想用直播变现,但不知道从哪开始", + path: "先看直播工具箱", + cta: "进入直播工具箱", + href: "https://nomadro.live", + emoji: "🎬", }, { id: "deep", @@ -160,21 +177,25 @@ export const SCENARIOS = [ path: "进入会员社区", cta: "进入会员中心", href: "https://vip.hackrobot.cn", + emoji: "👑", }, ] as const; export const ECOSYSTEM_COLUMNS = [ { - title: "城市 / 活动 / 社群", - items: ["城市库", "路线", "聚会", "微信群", "线下连接"], + title: "城市 / 直播 / 社群", + items: ["城市库", "直播工具", "推流模板", "微信群", "线上连接"], + emoji: "🌍", }, { title: "学习 / 工具 / 专题", items: ["电子书", "教程", "工具", "训练营", "学习路径"], + emoji: "📚", }, { title: "会员 / 私域 / 更新", items: ["会员权益", "视频", "社群", "问答", "持续更新"], + emoji: "💫", }, ] as const; @@ -205,11 +226,11 @@ export const CONTENT_PREVIEW_ITEMS = [ }, { id: "4", - type: "活动", - tag: "本周社群动态", - title: "深圳 · 3 月线下聚会", - summary: "12 人已报名,本周六南山见", - href: "https://meetup.hackrobot.cn", + type: "直播", + tag: "直播工具箱", + title: "游民直播开播指南", + summary: "推流设置、场景搭建、变现路径一站搞定", + href: "https://nomadro.live", }, { id: "5", @@ -233,19 +254,26 @@ export const TRUST_PRINCIPLES = [ { title: "长期主义", description: "不是速成课,不是躺赚项目,而是一套可持续的生活方式系统。", + emoji: "🌿", }, { title: "系统搭建", description: "从选择城市到建立收入,再到工具和社群,每一步都有清晰路径。", + emoji: "🏗️", }, { title: "同频连接", description: "适合愿意行动、愿意搭建自己路径的人,找到同类,一起前行。", + emoji: "🔗", }, ] as const; export const FINAL_CTAS = [ - { label: "看城市与社区", href: "https://meetup.hackrobot.cn" }, - { label: "看专题与学习路径", href: "https://digital.hackrobot.cn" }, - { label: "进会员与私域成长", href: "https://vip.hackrobot.cn" }, + { label: "进入直播工具箱", href: "https://nomadro.live", emoji: "🎬" }, + { label: "看专题与学习路径", href: "https://digital.hackrobot.cn", emoji: "📚" }, + { label: "进会员与私域成长", href: "https://vip.hackrobot.cn", emoji: "👑" }, +] as const; + +export const FLOATING_EMOJIS = [ + "✈️", "🌍", "💻", "☕", "🏖️", "📡", "🎧", "🌅", "🧳", "📱", ] as const; diff --git a/nomadro.sh b/nomadro.sh index 4e69fd6..d853c3e 100644 --- a/nomadro.sh +++ b/nomadro.sh @@ -1,3 +1,8 @@ #!/bin/bash -export PORT=4003 -exec npm run start +set -euo pipefail + +export PORT="${PORT:-4003}" +export NODE_ENV=production + +cd "$(dirname "$0")" +exec pnpm start