diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md index ad9d7f2..e215bc4 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,36 @@ -# cnomadcna +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). +## Getting Started +First, run the development server: -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin http://gitlab.yidooplanet.com/gitlab-instance-0a899031/cnomadcna.git -git branch -M main -git push -uf origin main +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev ``` -## Integrate with your tools +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -- [ ] [Set up project integrations](http://gitlab.yidooplanet.com/gitlab-instance-0a899031/cnomadcna/-/settings/integrations) +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -## Collaborate with your team +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. -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +## Learn More -## Test and Deploy +To learn more about Next.js, take a look at the following resources: -Use the built-in continuous integration in GitLab. +- [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. -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! -*** +## Deploy on Vercel -# Editing this README +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. -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/components/CityCard.tsx b/app/components/CityCard.tsx new file mode 100644 index 0000000..d64b09d --- /dev/null +++ b/app/components/CityCard.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { useState } from "react"; +import { City } from "../data/cities"; + +function ratingToPercent(rating: string): number { + const m: Record = { + 极好: 95, + 很好: 80, + 好: 60, + 一般: 40, + 差: 20, + }; + return m[rating] || 50; +} + +function getBarColor(p: number): string { + if (p >= 60) return "#22c55e"; + if (p >= 35) return "#eab308"; + return "#ef4444"; +} + +function ScoreBar({ + icon, + label, + percent, + animate, +}: { + icon: string; + label: string; + percent: number; + animate: boolean; +}) { + return ( +
+ + {icon} + {label} + +
+
+
+
+ ); +} + +export default function CityCard({ + city, + rank, +}: { + city: City; + rank: number; +}) { + const [hovered, setHovered] = useState(false); + + const scores = [ + { + icon: "⭐", + label: "综合", + percent: (city.overallScore / 5) * 100, + }, + { + icon: "💵", + label: "费用", + percent: Math.max(15, 100 - city.costPerMonth / 200), + }, + { + icon: "📡", + label: "网速", + percent: Math.min(95, (city.internetSpeed / 120) * 100), + }, + { + icon: "👍", + label: "好评", + percent: ratingToPercent(city.liked), + }, + { + icon: "👮", + label: "安全", + percent: ratingToPercent(city.safety), + }, + ]; + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {/* Icon watermark */} + + {city.icon} + + + {/* ===== Default state ===== */} +
+ {/* Top row */} +
+ + {rank} + + + 📡 {city.internetSpeed}Mbps + +
+ + {/* Center: city name + country */} +
+

+ {city.name} +

+

+ {city.countryEmoji} {city.country} +

+
+ + {/* Bottom: temp + cost + nomads */} +
+
+ + 👥 {city.nomadsNow}人在此 + + + 🌡 {city.temperature}°C + +
+
+ + {city.description} + + + ¥{city.costPerMonth.toLocaleString()}/月 + +
+
+
+ + {/* ===== Hover overlay (desktop only) ===== */} + +
+ ); +} diff --git a/app/components/FilterBar.tsx b/app/components/FilterBar.tsx new file mode 100644 index 0000000..7d3202c --- /dev/null +++ b/app/components/FilterBar.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { allTags } from "../data/cities"; + +interface FilterBarProps { + activeFilter: string; + onFilterChange: (filter: string) => void; + sortBy: string; + onSortChange: (sort: string) => void; + resultCount: number; +} + +const sortOptions = [ + { value: "score", label: "综合评分" }, + { value: "cost-asc", label: "费用最低" }, + { value: "cost-desc", label: "费用最高" }, + { value: "internet", label: "网速最快" }, + { value: "safety", label: "最安全" }, + { value: "nomads", label: "游民最多" }, + { value: "temperature", label: "最温暖" }, +]; + +const tagIcons: Record = { + 全部: "🌍", + 热门: "🔥", + 便宜: "💰", + 高速网络: "📡", + 温暖: "☀️", + 安全: "🛡️", + 海滨: "🏖️", + 山城: "🏔️", + 都市: "🌆", + 美食: "🍜", + 文化: "🎨", + 一线城市: "🏙️", + 新一线: "⭐", + 宜居: "🏡", + 历史名城: "🏛️", + 友好社区: "🤝", + 户外运动: "🏄", +}; + +export default function FilterBar({ + activeFilter, + onFilterChange, + sortBy, + onSortChange, + resultCount, +}: FilterBarProps) { + return ( +
+ {/* Top bar: result count + sort */} +
+
+

+ 发现目的地 +

+ + {resultCount} 个城市 + +
+
+ 排序: + +
+
+ + {/* Filter tags */} +
+ {allTags.map((tag) => ( + + ))} +
+
+ ); +} diff --git a/app/components/HeroSection.tsx b/app/components/HeroSection.tsx new file mode 100644 index 0000000..b0cc103 --- /dev/null +++ b/app/components/HeroSection.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; + +const avatarPhotos = [ + "https://i.pravatar.cc/150?img=32", + "https://i.pravatar.cc/150?img=33", + "https://i.pravatar.cc/150?img=25", + "https://i.pravatar.cc/150?img=52", + "https://i.pravatar.cc/150?img=44", + "https://i.pravatar.cc/150?img=55", + "https://i.pravatar.cc/150?img=47", + "https://i.pravatar.cc/150?img=57", + "https://i.pravatar.cc/150?img=48", + "https://i.pravatar.cc/150?img=59", + "https://i.pravatar.cc/150?img=49", +]; + +const features = [ + { icon: "🍹", text: "每年在 30+ 城市参加线下聚会" }, + { icon: "❤️", text: "结识新朋友,约会交友" }, + { icon: "🧪", text: "研究目的地,找到最适合居住和工作的地方" }, + { icon: "🌎", text: "追踪你的旅行,记录你去过的地方" }, + { icon: "💬", text: "加入社群聊天,在旅途中找到你的归属" }, +]; + +export default function HeroSection() { + const [email, setEmail] = useState(""); + + const handleJoin = (e: React.FormEvent) => { + e.preventDefault(); + if (email.trim()) { + alert(`欢迎加入游牧中国!我们将发送确认邮件到 ${email}`); + } + }; + + return ( +
+
+
+
+ +
+
+ {/* Left Column */} +
+ + 🏆 中国数字游民社区 · 始于 2024 + + +

+ 探索中国 +
+ 远程工作,自由生活 +

+ +

+ 加入全球华人数字游民社区,发现中国最适合远程工作和生活的城市, + 从大理到成都,从深圳到杭州,开启你的游牧人生 +

+ +
+ {features.map((f) => ( +
+ + {f.icon} + + + {f.text} + +
+ ))} +
+ +
+
+ {avatarPhotos.slice(0, 8).map((src, i) => ( + + ))} +
+ + 5,000+ 成员已加入 + +
+ +
+

+ 媒体报道 +

+
+ {["36氪", "少数派", "极客公园", "虎嗅", "创业邦", "澎湃新闻"].map( + (name) => ( + + {name} + + ), + )} +
+
+
+ + {/* Right Column: Video + Email */} +
+
+
+
+
+
+
+
+
+ +
+ +
+ setEmail(e.target.value)} + placeholder="输入你的邮箱..." + className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20 focus:border-[#ff4d4f] transition-colors mb-3" + /> + +

+ 已有账号?输入邮箱即可自动登录 +

+
+
+ +
+ + 🍹 线下聚会 + + + ❤️ 交友约会 + + + 💬 加入社群 + +
+
+
+
+
+ ); +} diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx new file mode 100644 index 0000000..1e5ddd9 --- /dev/null +++ b/app/components/Navbar.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const navLinks = [ + { label: "城市", icon: "🏙️", href: "/" }, + { label: "旅行", icon: "✈️", href: "/meetups" }, + { label: "社区", icon: "💬", href: "/dating" }, + { label: "探索", icon: "🎒", href: "/#community" }, +]; + +export default function Navbar() { + const [mobileOpen, setMobileOpen] = useState(false); + const pathname = usePathname(); + + function isActive(href: string) { + if (href.startsWith("/#")) return false; + if (href === "/") return pathname === "/"; + return pathname.startsWith(href); + } + + return ( + + ); +} diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx new file mode 100644 index 0000000..a1ea159 --- /dev/null +++ b/app/components/Sidebar.tsx @@ -0,0 +1,245 @@ +const meetups = [ + { city: "清迈", emoji: "🇹🇭", date: "3月9日 周一", attendees: 12 }, + { city: "曼谷", emoji: "🇹🇭", date: "3月12日 周四", attendees: 8 }, + { city: "巴厘岛", emoji: "🇮🇩", date: "3月14日 周六", attendees: 15 }, + { city: "大理", emoji: "🇨🇳", date: "3月16日 周一", attendees: 6 }, + { city: "里斯本", emoji: "🇵🇹", date: "3月20日 周五", attendees: 23 }, +]; + +const travelingNow = [ + { name: "小明", bg: "#f43f5e", destination: "清迈" }, + { name: "雨晴", bg: "#8b5cf6", destination: "巴厘岛" }, + { name: "阿杰", bg: "#3b82f6", destination: "曼谷" }, + { name: "小燕", bg: "#10b981", destination: "东京" }, + { name: "大伟", bg: "#f59e0b", destination: "里斯本" }, + { name: "佳琪", bg: "#ec4899", destination: "墨西哥城" }, + { name: "志远", bg: "#06b6d4", destination: "柏林" }, + { name: "心怡", bg: "#ef4444", destination: "首尔" }, + { name: "凯文", bg: "#84cc16", destination: "巴塞罗那" }, + { name: "子豪", bg: "#7c3aed", destination: "成都" }, + { name: "浩然", bg: "#0d9488", destination: "大理" }, + { name: "雅琪", bg: "#ea580c", destination: "台北" }, + { name: "天翔", bg: "#6366f1", destination: "布达佩斯" }, + { name: "美玲", bg: "#d946ef", destination: "梅德林" }, + { name: "俊杰", bg: "#14b8a6", destination: "普吉岛" }, + { name: "晓华", bg: "#f97316", destination: "吉隆坡" }, +]; + +const newMembers = [ + { name: "林", bg: "#3b82f6" }, + { name: "张", bg: "#8b5cf6" }, + { name: "陈", bg: "#f43f5e" }, + { name: "周", bg: "#10b981" }, + { name: "吴", bg: "#f59e0b" }, + { name: "杨", bg: "#ec4899" }, + { name: "赵", bg: "#06b6d4" }, + { name: "黄", bg: "#ef4444" }, + { name: "孙", bg: "#84cc16" }, + { name: "马", bg: "#7c3aed" }, + { name: "朱", bg: "#0d9488" }, + { name: "胡", bg: "#ea580c" }, + { name: "郭", bg: "#6366f1" }, + { name: "何", bg: "#d946ef" }, + { name: "高", bg: "#14b8a6" }, + { name: "梁", bg: "#f97316" }, + { name: "郑", bg: "#a855f7" }, + { name: "罗", bg: "#22c55e" }, + { name: "谢", bg: "#e11d48" }, + { name: "韩", bg: "#0ea5e9" }, +]; + +const hotTopics = [ + { title: "清迈最佳共享办公空间推荐 Top 10", views: 2340 }, + { title: "2026年数字游民签证政策汇总", views: 1890 }, + { title: "远程工作者的跨国税务规划指南", views: 1560 }, + { title: "东南亚 vs 拉美:哪个更适合游民?", views: 1230 }, + { title: "新手游民必备工具与装备清单", views: 980 }, +]; + +export default function Sidebar() { + return ( + + ); +} diff --git a/app/data/cities.ts b/app/data/cities.ts new file mode 100644 index 0000000..d67d435 --- /dev/null +++ b/app/data/cities.ts @@ -0,0 +1,384 @@ +export interface City { + id: number; + name: string; + country: string; + countryEmoji: string; + overallScore: number; + costPerMonth: number; + internetSpeed: number; + safety: string; + liked: string; + temperature: number; + humidity: number; + nomadsNow: number; + description: string; + gradientFrom: string; + gradientTo: string; + icon: string; + tags: string[]; +} + +export const cities: City[] = [ + { + id: 1, + name: "大理", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.5, + costPerMonth: 3000, + internetSpeed: 50, + safety: "很好", + liked: "极好", + temperature: 16, + humidity: 60, + nomadsNow: 520, + description: "苍山洱海,文艺气息浓厚的慢生活之城", + gradientFrom: "#2563eb", + gradientTo: "#0891b2", + icon: "🏯", + tags: ["热门", "便宜", "宜居", "山城", "文化", "友好社区"], + }, + { + id: 2, + name: "成都", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.4, + costPerMonth: 4500, + internetSpeed: 80, + safety: "很好", + liked: "极好", + temperature: 17, + humidity: 75, + nomadsNow: 680, + description: "天府之国,美食与慢节奏的完美融合", + gradientFrom: "#059669", + gradientTo: "#65a30d", + icon: "🐼", + tags: ["热门", "美食", "都市", "新一线", "文化", "友好社区"], + }, + { + id: 3, + name: "深圳", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.1, + costPerMonth: 8000, + internetSpeed: 120, + safety: "极好", + liked: "很好", + temperature: 23, + humidity: 70, + nomadsNow: 850, + description: "科技创新之城,创业者的乐园", + gradientFrom: "#1d4ed8", + gradientTo: "#0284c7", + icon: "🌃", + tags: ["热门", "高速网络", "都市", "一线城市", "安全"], + }, + { + id: 4, + name: "上海", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.0, + costPerMonth: 10000, + internetSpeed: 110, + safety: "极好", + liked: "好", + temperature: 17, + humidity: 65, + nomadsNow: 1200, + description: "国际大都市,东西方文化交汇的中心", + gradientFrom: "#b45309", + gradientTo: "#78716c", + icon: "🏙️", + tags: ["热门", "高速网络", "都市", "一线城市", "文化"], + }, + { + id: 5, + name: "杭州", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.3, + costPerMonth: 6000, + internetSpeed: 100, + safety: "极好", + liked: "很好", + temperature: 17, + humidity: 70, + nomadsNow: 560, + description: "西湖之畔,互联网重镇与诗画江南", + gradientFrom: "#047857", + gradientTo: "#0d9488", + icon: "⛲", + tags: ["热门", "高速网络", "都市", "新一线", "宜居", "文化"], + }, + { + id: 6, + name: "厦门", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.1, + costPerMonth: 5500, + internetSpeed: 70, + safety: "很好", + liked: "很好", + temperature: 21, + humidity: 75, + nomadsNow: 320, + description: "海滨花园城市,鼓浪屿的浪漫与闽南风情", + gradientFrom: "#ea580c", + gradientTo: "#0284c7", + icon: "🌊", + tags: ["海滨", "温暖", "美食", "宜居", "文化"], + }, + { + id: 7, + name: "北京", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.8, + costPerMonth: 9000, + internetSpeed: 100, + safety: "极好", + liked: "好", + temperature: 14, + humidity: 50, + nomadsNow: 780, + description: "千年古都,政治文化中心的深厚底蕴", + gradientFrom: "#dc2626", + gradientTo: "#b91c1c", + icon: "🏛️", + tags: ["都市", "一线城市", "文化", "高速网络", "历史名城"], + }, + { + id: 8, + name: "广州", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.9, + costPerMonth: 6500, + internetSpeed: 90, + safety: "很好", + liked: "好", + temperature: 23, + humidity: 80, + nomadsNow: 480, + description: "食在广州,千年商都的烟火气", + gradientFrom: "#f97316", + gradientTo: "#ef4444", + icon: "🌺", + tags: ["热门", "美食", "都市", "一线城市", "温暖"], + }, + { + id: 9, + name: "重庆", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.7, + costPerMonth: 4000, + internetSpeed: 75, + safety: "很好", + liked: "好", + temperature: 18, + humidity: 80, + nomadsNow: 350, + description: "8D魔幻山城,火锅与夜景的不夜城", + gradientFrom: "#ef4444", + gradientTo: "#c2410c", + icon: "🌶️", + tags: ["便宜", "美食", "山城", "都市", "文化"], + }, + { + id: 10, + name: "昆明", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.2, + costPerMonth: 3500, + internetSpeed: 55, + safety: "好", + liked: "很好", + temperature: 16, + humidity: 55, + nomadsNow: 380, + description: "四季如春的春城,通往东南亚的门户", + gradientFrom: "#d946ef", + gradientTo: "#059669", + icon: "🌸", + tags: ["便宜", "温暖", "宜居", "友好社区", "户外运动"], + }, + { + id: 11, + name: "三亚", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.5, + costPerMonth: 5000, + internetSpeed: 60, + safety: "好", + liked: "好", + temperature: 26, + humidity: 85, + nomadsNow: 250, + description: "中国的热带天堂,椰风海韵的度假胜地", + gradientFrom: "#0ea5e9", + gradientTo: "#06b6d4", + icon: "🏖️", + tags: ["海滨", "温暖", "户外运动"], + }, + { + id: 12, + name: "丽江", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.0, + costPerMonth: 3200, + internetSpeed: 45, + safety: "好", + liked: "好", + temperature: 14, + humidity: 50, + nomadsNow: 280, + description: "玉龙雪山下的纳西古城,灵魂栖息地", + gradientFrom: "#6366f1", + gradientTo: "#7c3aed", + icon: "🏔️", + tags: ["便宜", "文化", "山城", "宜居", "历史名城"], + }, + { + id: 13, + name: "西安", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.8, + costPerMonth: 4200, + internetSpeed: 70, + safety: "很好", + liked: "好", + temperature: 15, + humidity: 55, + nomadsNow: 230, + description: "十三朝古都,历史与美食交织的城市", + gradientFrom: "#78716c", + gradientTo: "#a16207", + icon: "🗿", + tags: ["便宜", "美食", "文化", "历史名城"], + }, + { + id: 14, + name: "南京", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.9, + costPerMonth: 5500, + internetSpeed: 85, + safety: "很好", + liked: "好", + temperature: 16, + humidity: 70, + nomadsNow: 400, + description: "六朝古都,紫金山下的文化名城", + gradientFrom: "#7c3aed", + gradientTo: "#059669", + icon: "🍃", + tags: ["文化", "都市", "新一线", "历史名城"], + }, + { + id: 15, + name: "长沙", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.6, + costPerMonth: 4500, + internetSpeed: 75, + safety: "好", + liked: "好", + temperature: 17, + humidity: 75, + nomadsNow: 290, + description: "娱乐之都,夜生活与美食的不眠城", + gradientFrom: "#f59e0b", + gradientTo: "#16a34a", + icon: "🎪", + tags: ["美食", "都市", "新一线", "友好社区"], + }, + { + id: 16, + name: "青岛", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.7, + costPerMonth: 4800, + internetSpeed: 65, + safety: "很好", + liked: "好", + temperature: 13, + humidity: 65, + nomadsNow: 220, + description: "红瓦绿树碧海蓝天,啤酒飘香的海滨城市", + gradientFrom: "#2563eb", + gradientTo: "#ca8a04", + icon: "🍺", + tags: ["海滨", "美食", "宜居", "户外运动"], + }, + { + id: 17, + name: "苏州", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 4.0, + costPerMonth: 5000, + internetSpeed: 80, + safety: "极好", + liked: "很好", + temperature: 17, + humidity: 70, + nomadsNow: 260, + description: "上有天堂下有苏杭,古典园林与现代新城", + gradientFrom: "#0d9488", + gradientTo: "#64748b", + icon: "🏡", + tags: ["文化", "宜居", "安全", "历史名城"], + }, + { + id: 18, + name: "珠海", + country: "中国", + countryEmoji: "🇨🇳", + overallScore: 3.9, + costPerMonth: 5200, + internetSpeed: 70, + safety: "很好", + liked: "好", + temperature: 23, + humidity: 75, + nomadsNow: 180, + description: "浪漫之城,毗邻澳门的海滨花园", + gradientFrom: "#0891b2", + gradientTo: "#6366f1", + icon: "🎡", + tags: ["海滨", "温暖", "宜居", "安全"], + }, +]; + +export const allTags = [ + "全部", + "热门", + "便宜", + "高速网络", + "温暖", + "安全", + "海滨", + "山城", + "都市", + "美食", + "文化", + "一线城市", + "新一线", + "宜居", + "历史名城", + "友好社区", + "户外运动", +]; diff --git a/app/dating/page.tsx b/app/dating/page.tsx new file mode 100644 index 0000000..69b23da --- /dev/null +++ b/app/dating/page.tsx @@ -0,0 +1,304 @@ +"use client"; + +import { useState } from "react"; + +type TabType = "friends" | "dating"; + +interface Profile { + id: string; + name: string; + age: number; + location: string; + tags: string[]; + bio: string; + gradient: string; + initial: string; + avatarColor: string; + photo: string; +} + +interface MatchCard { + id: string; + name: string; + location: string; + timeAgo: string; + initial: string; + color: string; + photo: string; +} + +const MOCK_PROFILES: Profile[] = [ + { + id: "1", + name: "林晓雨", + age: 28, + location: "大理", + tags: ["本科学历", "远程工作者", "数字游民", "咖啡爱好者"], + bio: "数字游民三年,目前在云南大理远程做产品设计。喜欢爬山、摄影,周末常去洱海边发呆。", + gradient: "from-rose-400 via-pink-400 to-fuchsia-400", + initial: "林", + avatarColor: "bg-rose-500", + photo: "https://i.pravatar.cc/150?img=1", + }, + { + id: "2", + name: "陈浩然", + age: 32, + location: "成都", + tags: ["硕士学历", "独立开发者", "自由职业者", "素食主义"], + bio: "在成都住了两年,做独立开发。喜欢冲浪和冥想,寻找志同道合的旅伴一起探索世界。", + gradient: "from-amber-400 via-orange-400 to-red-400", + initial: "陈", + avatarColor: "bg-amber-500", + photo: "https://i.pravatar.cc/150?img=3", + }, + { + id: "3", + name: "王思琪", + age: 26, + location: "深圳", + tags: ["本科学历", "内容创作者", "数字游民", "环保主义者"], + bio: "自由撰稿人,在深圳写写画画。热爱瑜伽和冥想,相信慢生活才是真正的奢侈。", + gradient: "from-emerald-400 via-teal-400 to-cyan-400", + initial: "王", + avatarColor: "bg-emerald-500", + photo: "https://i.pravatar.cc/150?img=5", + }, + { + id: "4", + name: "张明远", + age: 30, + location: "杭州", + tags: ["MBA学历", "创业中", "自由职业者", "徒步爱好者"], + bio: "正在杭州创业,做远程团队协作工具。周末喜欢去西湖徒步,偶尔品鉴龙井茶。", + gradient: "from-violet-400 via-purple-400 to-indigo-400", + initial: "张", + avatarColor: "bg-violet-500", + photo: "https://i.pravatar.cc/150?img=7", + }, + { + id: "5", + name: "李雅婷", + age: 27, + location: "厦门", + tags: ["本科学历", "UI设计师", "数字游民", "摄影爱好者"], + bio: "在厦门远程做UI设计,业余时间探索闽南美食。喜欢用相机记录生活,寻找一起探店的朋友。", + gradient: "from-rose-500 via-red-400 to-orange-500", + initial: "李", + avatarColor: "bg-rose-500", + photo: "https://i.pravatar.cc/150?img=9", + }, + { + id: "6", + name: "刘子轩", + age: 33, + location: "昆明", + tags: ["博士学历", "数据科学家", "远程工作者", "户外爱好者"], + bio: "在昆明远程工作,热爱户外运动。正在探索云南各地,希望认识更多志同道合的朋友。", + gradient: "from-blue-400 via-indigo-400 to-violet-500", + initial: "刘", + avatarColor: "bg-blue-500", + photo: "https://i.pravatar.cc/150?img=11", + }, +]; + +const MOCK_MATCHES: MatchCard[] = [ + { id: "m1", name: "周雨桐", location: "杭州", timeAgo: "2天前", initial: "周", color: "bg-pink-400", photo: "https://i.pravatar.cc/150?img=29" }, + { id: "m2", name: "吴俊杰", location: "成都", timeAgo: "1周前", initial: "吴", color: "bg-amber-400", photo: "https://i.pravatar.cc/150?img=34" }, + { id: "m3", name: "郑诗涵", location: "上海", timeAgo: "3周前", initial: "郑", color: "bg-emerald-400", photo: "https://i.pravatar.cc/150?img=36" }, + { id: "m4", name: "孙宇航", location: "深圳", timeAgo: "1月前", initial: "孙", color: "bg-violet-400", photo: "https://i.pravatar.cc/150?img=38" }, + { id: "m5", name: "赵梦琪", location: "厦门", timeAgo: "1年前", initial: "赵", color: "bg-rose-400", photo: "https://i.pravatar.cc/150?img=41" }, +]; + +export default function DatingPage() { + const [activeTab, setActiveTab] = useState("friends"); + const [currentIndex, setCurrentIndex] = useState(0); + + const currentProfile = MOCK_PROFILES[currentIndex]; + const hasMoreProfiles = currentIndex < MOCK_PROFILES.length - 1; + + const handleLike = () => { + if (hasMoreProfiles) { + setCurrentIndex((i) => i + 1); + } else { + setCurrentIndex(0); + } + }; + + const handleDislike = () => { + if (hasMoreProfiles) { + setCurrentIndex((i) => i + 1); + } else { + setCurrentIndex(0); + } + }; + + const badgeText = + activeTab === "dating" + ? "你们都在寻找 🌹 约会" + : "你们都在寻找 🤝 交友"; + + return ( +
+ {/* Tabs */} +
+
+ + +
+
+ +
+ {/* Left Sidebar - hidden on mobile */} + + + {/* Center - Main Card */} +
+
+
+ {/* Top right: badge + report */} +
+ + {badgeText} + + + 举报资料 + +
+ + {/* Avatar */} +
+ {currentProfile.name} +
+ + {/* Name, age, location */} +
+

+ {currentProfile.name},{currentProfile.age} +

+

+ 📍 {currentProfile.location} +

+
+ + {/* Tags */} +
+

+ 匹配标签: +

+
+ {currentProfile.tags.map((tag) => ( + + {tag} + + ))} +
+
+ + {/* Bio */} +

+ {currentProfile.bio} +

+ + {/* Action buttons */} +
+
+ + + 不感兴趣 + +
+
+ + 喜欢 +
+
+
+ + {/* Progress indicator */} +

+ {currentIndex + 1} / {MOCK_PROFILES.length} +

+
+
+ + {/* Right spacer for desktop balance */} +
+
+
+ ); +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..985eda7 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,227 @@ +@import "tailwindcss"; + +:root { + --background: #fafafa; + --foreground: #111827; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +body { + background: var(--background); + color: var(--foreground); + font-family: var(--font-geist-sans, system-ui), "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", sans-serif; +} + +html { + scroll-behavior: smooth; +} + +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 3px; +} + +/* ==================== Items Grid (mobile-first) ==================== */ +.items-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-auto-rows: 180px; + gap: 8px; + padding: 8px; +} + +@media (min-width: 640px) { + .items-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-auto-rows: 260px; + gap: 20px; + padding: 16px 20px; + } +} + +@media (min-width: 1024px) { + .items-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-auto-rows: 280px; + gap: 30px; + padding: 20px 40px; + } +} + +@media (min-width: 1440px) { + .items-grid { + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 35px; + } +} + +/* Pin right-items to the LAST explicit column */ +.items-grid .right-item:nth-child(1) { + grid-column: -2 / -1; + grid-row: 1; +} +.items-grid .right-item:nth-child(2) { + grid-column: -2 / -1; + grid-row: 2; +} +.items-grid .right-item:nth-child(3) { + grid-column: -2 / -1; + grid-row: 3; +} + +/* ==================== City Card ==================== */ +.city-card { + height: 100%; + border-radius: 15px; + position: relative; + overflow: hidden; + cursor: pointer; + color: white; + box-sizing: border-box; + min-width: 0; +} + +.city-card:hover { + animation: pulse 0.6s ease; +} + +/* ==================== Right Item ==================== */ +.right-item { + height: 100%; + border-radius: 15px; + position: relative; + overflow: hidden; + width: 100%; + min-width: 0; + box-sizing: border-box; + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04); +} + +.right-item:hover { + animation: pulse 0.6s ease; +} + +.right-item-header { + width: 100%; + height: 38px; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.5px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(90deg, #ff4d4f, #ff7a45); + color: #fff; +} + +@media (min-width: 640px) { + .right-item-header { + height: 42px; + font-size: 15px; + } +} + +/* ==================== Hover / Pulse Animation ==================== */ +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.03); + } + 100% { + transform: scale(1); + } +} + +/* ==================== Hero Gradient Text ==================== */ +@keyframes gradient-shift { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } +} + +.hero-gradient-text { + background: linear-gradient( + 135deg, + #f97316, + #ec4899, + #8b5cf6, + #3b82f6, + #f97316 + ); + background-size: 300% 300%; + animation: gradient-shift 6s ease infinite; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ==================== Filter Buttons ==================== */ +.filter-btn { + transition: all 0.15s ease; +} +.filter-btn:hover { + background: #f3f4f6; +} +.filter-btn.active { + background: #111827; + color: white; +} +.filter-btn.active:hover { + background: #1f2937; +} + +/* ==================== Community Cards ==================== */ +.community-card { + background: white; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.03); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} +.community-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.04); +} + +/* ==================== Utilities ==================== */ +.hide-scrollbar::-webkit-scrollbar { + display: none; +} +.hide-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.line-clamp-1 { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} diff --git a/app/join/page.tsx b/app/join/page.tsx new file mode 100644 index 0000000..48aa7f4 --- /dev/null +++ b/app/join/page.tsx @@ -0,0 +1,280 @@ +"use client"; + +import { useState } from "react"; + +type FormState = { + nickname: string; + gender: string; + city: string; + occupation: string; + education: string; + wechat: string; + phone: string; + intro: string; +}; + +const OCCUPATIONS = [ + "自由职业者", + "远程工程师", + "设计师", + "内容创作者", + "创业者", + "其他", +] as const; + +const EDUCATIONS = ["高中", "大专", "本科", "硕士", "博士"] as const; + +const initialForm: FormState = { + nickname: "", + gender: "", + city: "", + occupation: "", + education: "", + wechat: "", + phone: "", + intro: "", +}; + +export default function JoinPage() { + const [form, setForm] = useState(initialForm); + + const updateField = (field: keyof FormState, value: string) => { + setForm((prev) => ({ ...prev, [field]: value })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!form.nickname.trim()) { + alert("请填写昵称"); + return; + } + if (!form.wechat.trim()) { + alert("请填写微信号"); + return; + } + alert("提交成功!我们将通过微信联系你。"); + setForm(initialForm); + }; + + return ( +
+ {/* Top Section: Gradient Hero */} +
+
+

+ 加入游牧中国社区 +

+

+ 填写信息加入全球华人数字游民社群 +

+
+
+ + {/* Form Card */} +
+
+
+ {/* 昵称 */} +
+ + updateField("nickname", e.target.value)} + required + placeholder="请输入你的昵称" + className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 placeholder-gray-400 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20" + /> +
+ + {/* 性别 */} +
+ +
+ {["男", "女", "其他"].map((g) => ( + + ))} +
+
+ + {/* 当前所在城市 */} +
+ + updateField("city", e.target.value)} + placeholder="如:清迈、曼谷、大理" + className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 placeholder-gray-400 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20" + /> +
+ + {/* 职业 */} +
+ + +
+ + {/* 学历 */} +
+ + +
+ + {/* 微信号 */} +
+ + updateField("wechat", e.target.value)} + required + placeholder="用于联系你加入社群" + className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 placeholder-gray-400 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20" + /> +
+ + {/* 手机号 */} +
+ +
+ + +86 + + updateField("phone", e.target.value)} + placeholder="选填" + className="flex-1 rounded-r-xl border-0 px-4 py-3 text-gray-900 placeholder-gray-400 focus:ring-0" + /> +
+
+ + {/* 简单介绍自己 */} +
+ +