's'
This commit is contained in:
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@@ -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
|
||||||
100
README.md
100
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
|
```bash
|
||||||
|
npm run dev
|
||||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
# or
|
||||||
|
yarn dev
|
||||||
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)!
|
# or
|
||||||
|
pnpm dev
|
||||||
## Add your files
|
# or
|
||||||
|
bun dev
|
||||||
- [ ] [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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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/)
|
## Learn More
|
||||||
- [ ] [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)
|
|
||||||
|
|
||||||
## 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)
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
- [ ] [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)
|
|
||||||
|
|
||||||
***
|
## 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.
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
189
app/components/CityCard.tsx
Normal file
189
app/components/CityCard.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { City } from "../data/cities";
|
||||||
|
|
||||||
|
function ratingToPercent(rating: string): number {
|
||||||
|
const m: Record<string, number> = {
|
||||||
|
极好: 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 (
|
||||||
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
|
<span className="text-white/80 text-xs sm:text-sm w-14 sm:w-16 shrink-0 flex items-center gap-1">
|
||||||
|
<span>{icon}</span>
|
||||||
|
<span className="hidden sm:inline">{label}</span>
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 h-2 sm:h-2.5 bg-white/10 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-700 ease-out"
|
||||||
|
style={{
|
||||||
|
width: animate ? `${percent}%` : "0%",
|
||||||
|
backgroundColor: getBarColor(percent),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="city-card"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${city.gradientFrom}, ${city.gradientTo})`,
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
>
|
||||||
|
{/* Icon watermark */}
|
||||||
|
<span className="absolute bottom-8 right-2 text-[70px] sm:text-[90px] opacity-[0.12] select-none pointer-events-none leading-none">
|
||||||
|
{city.icon}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* ===== Default state ===== */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 z-10 flex flex-col justify-between p-3 sm:p-4 transition-opacity duration-300 ${hovered ? "opacity-0" : "opacity-100"}`}
|
||||||
|
>
|
||||||
|
{/* Top row */}
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<span className="text-white/70 text-base sm:text-xl font-bold">
|
||||||
|
{rank}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] sm:text-xs bg-white/20 backdrop-blur-sm rounded-full px-1.5 sm:px-2 py-0.5 sm:py-1 text-white flex items-center gap-0.5">
|
||||||
|
📡 {city.internetSpeed}Mbps
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center: city name + country */}
|
||||||
|
<div className="text-center -mt-2">
|
||||||
|
<h3 className="text-xl sm:text-[28px] font-bold text-white drop-shadow-md tracking-wide">
|
||||||
|
{city.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-white/60 mt-0.5">
|
||||||
|
{city.countryEmoji} {city.country}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom: temp + cost + nomads */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1 mb-1.5">
|
||||||
|
<span className="text-[9px] sm:text-[11px] bg-white/15 backdrop-blur-sm rounded-full px-1.5 py-0.5 text-white/80">
|
||||||
|
👥 {city.nomadsNow}人在此
|
||||||
|
</span>
|
||||||
|
<span className="text-[9px] sm:text-[11px] bg-white/15 backdrop-blur-sm rounded-full px-1.5 py-0.5 text-white/80">
|
||||||
|
🌡 {city.temperature}°C
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end justify-between text-white text-xs sm:text-sm">
|
||||||
|
<span className="text-white/80 text-[10px] sm:text-xs line-clamp-1 max-w-[60%]">
|
||||||
|
{city.description}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold shrink-0">
|
||||||
|
¥{city.costPerMonth.toLocaleString()}/月
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ===== Hover overlay (desktop only) ===== */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 z-20 bg-[#2a2a2a]/95 hidden sm:flex flex-col justify-between p-4 sm:p-5 transition-opacity duration-300 ${hovered ? "opacity-100" : "opacity-0 pointer-events-none"}`}
|
||||||
|
>
|
||||||
|
{/* Top icons */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<button className="text-white/50 hover:text-red-400 transition-colors text-base">
|
||||||
|
❤️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-white/30 hover:text-white text-sm transition-colors"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setHovered(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Score bars */}
|
||||||
|
<div className="space-y-2.5 sm:space-y-3">
|
||||||
|
{scores.map((s) => (
|
||||||
|
<ScoreBar
|
||||||
|
key={s.label}
|
||||||
|
icon={s.icon}
|
||||||
|
label={s.label}
|
||||||
|
percent={s.percent}
|
||||||
|
animate={hovered}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom: city info small */}
|
||||||
|
<div className="text-center text-white/40 text-xs">
|
||||||
|
{city.name} · {city.countryEmoji} {city.country}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
app/components/FilterBar.tsx
Normal file
97
app/components/FilterBar.tsx
Normal file
@@ -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<string, string> = {
|
||||||
|
全部: "🌍",
|
||||||
|
热门: "🔥",
|
||||||
|
便宜: "💰",
|
||||||
|
高速网络: "📡",
|
||||||
|
温暖: "☀️",
|
||||||
|
安全: "🛡️",
|
||||||
|
海滨: "🏖️",
|
||||||
|
山城: "🏔️",
|
||||||
|
都市: "🌆",
|
||||||
|
美食: "🍜",
|
||||||
|
文化: "🎨",
|
||||||
|
一线城市: "🏙️",
|
||||||
|
新一线: "⭐",
|
||||||
|
宜居: "🏡",
|
||||||
|
历史名城: "🏛️",
|
||||||
|
友好社区: "🤝",
|
||||||
|
户外运动: "🏄",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FilterBar({
|
||||||
|
activeFilter,
|
||||||
|
onFilterChange,
|
||||||
|
sortBy,
|
||||||
|
onSortChange,
|
||||||
|
resultCount,
|
||||||
|
}: FilterBarProps) {
|
||||||
|
return (
|
||||||
|
<div className="mb-6" id="cities">
|
||||||
|
{/* Top bar: result count + sort */}
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-lg font-bold text-gray-900">
|
||||||
|
发现目的地
|
||||||
|
</h2>
|
||||||
|
<span className="text-sm text-gray-400">
|
||||||
|
{resultCount} 个城市
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-gray-400 hidden sm:block">排序:</span>
|
||||||
|
<select
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(e) => onSortChange(e.target.value)}
|
||||||
|
className="text-sm bg-white border border-gray-200 rounded-lg px-3 py-1.5 text-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
{sortOptions.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter tags */}
|
||||||
|
<div className="flex gap-2 overflow-x-auto hide-scrollbar pb-2">
|
||||||
|
{allTags.map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
onClick={() => onFilterChange(tag)}
|
||||||
|
className={`filter-btn shrink-0 flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium border transition-all ${
|
||||||
|
activeFilter === tag
|
||||||
|
? "active border-gray-900"
|
||||||
|
: "border-gray-200 text-gray-600 hover:border-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-xs">{tagIcons[tag] || "🏷️"}</span>
|
||||||
|
<span>{tag}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
189
app/components/HeroSection.tsx
Normal file
189
app/components/HeroSection.tsx
Normal file
@@ -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 (
|
||||||
|
<section className="relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-orange-50/70 via-white to-[#fafafa]" />
|
||||||
|
<div
|
||||||
|
className="absolute top-0 right-0 w-[500px] h-[500px] -translate-y-1/3 translate-x-1/4 opacity-40 rounded-full"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle, rgba(249,115,22,0.25) 0%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-0 w-[350px] h-[350px] translate-y-1/3 -translate-x-1/4 opacity-30 rounded-full"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle, rgba(139,92,246,0.2) 0%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative max-w-[1400px] mx-auto px-5 sm:px-10 py-10 sm:py-14 md:py-20">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-10 xl:gap-16">
|
||||||
|
{/* Left Column */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-orange-700 bg-orange-100 px-3 py-1.5 rounded-full mb-5">
|
||||||
|
🏆 中国数字游民社区 · 始于 2024
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight mb-5">
|
||||||
|
<span className="hero-gradient-text">探索中国</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-gray-900">远程工作,自由生活</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-base sm:text-lg text-gray-500 max-w-2xl mb-7 leading-relaxed">
|
||||||
|
加入全球华人数字游民社区,发现中国最适合远程工作和生活的城市,
|
||||||
|
从大理到成都,从深圳到杭州,开启你的游牧人生
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2.5 mb-8">
|
||||||
|
{features.map((f) => (
|
||||||
|
<div key={f.text} className="flex items-start gap-2.5">
|
||||||
|
<span className="text-base sm:text-lg shrink-0 mt-0.5">
|
||||||
|
{f.icon}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm sm:text-base text-gray-600 underline decoration-gray-300 underline-offset-2">
|
||||||
|
{f.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 mb-8">
|
||||||
|
<div className="flex -space-x-2">
|
||||||
|
{avatarPhotos.slice(0, 8).map((src, i) => (
|
||||||
|
<img
|
||||||
|
key={i}
|
||||||
|
src={src}
|
||||||
|
alt=""
|
||||||
|
className="w-8 h-8 rounded-full border-2 border-white shadow-sm object-cover"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
<strong className="text-gray-900">5,000+</strong> 成员已加入
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-7 border-t border-gray-100">
|
||||||
|
<p className="text-xs text-gray-400 uppercase tracking-wider mb-3">
|
||||||
|
媒体报道
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-8 gap-y-2">
|
||||||
|
{["36氪", "少数派", "极客公园", "虎嗅", "创业邦", "澎湃新闻"].map(
|
||||||
|
(name) => (
|
||||||
|
<span
|
||||||
|
key={name}
|
||||||
|
className="text-sm font-semibold text-gray-300 hover:text-gray-400 transition-colors cursor-default"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Video + Email */}
|
||||||
|
<div className="lg:w-[360px] xl:w-[400px] shrink-0 mt-10 lg:mt-4">
|
||||||
|
<div className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100">
|
||||||
|
<div className="relative h-48 sm:h-56 bg-gradient-to-br from-emerald-400 via-teal-500 to-cyan-600 flex items-center justify-center cursor-pointer group overflow-hidden">
|
||||||
|
<div className="absolute inset-0 opacity-20">
|
||||||
|
<div className="absolute top-4 left-4 w-20 h-16 rounded-lg bg-white/20" />
|
||||||
|
<div className="absolute bottom-6 right-6 w-24 h-14 rounded-lg bg-white/15" />
|
||||||
|
<div className="absolute top-1/2 left-1/3 w-16 h-16 rounded-full bg-white/10" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-1/3 bg-gradient-to-t from-black/20 to-transparent" />
|
||||||
|
<button className="relative w-16 h-16 rounded-full bg-white/95 flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform">
|
||||||
|
<svg
|
||||||
|
className="w-7 h-7 text-emerald-600 ml-1"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M8 5v14l11-7z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleJoin} className="p-5">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-[#ff4d4f] hover:bg-[#ff3333] text-white py-3 rounded-lg text-sm font-semibold transition-colors active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
加入游牧中国 →
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-gray-400 text-center mt-3">
|
||||||
|
已有账号?输入邮箱即可自动登录
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2 justify-center">
|
||||||
|
<Link
|
||||||
|
href="/meetups"
|
||||||
|
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
🍹 线下聚会
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/dating"
|
||||||
|
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
❤️ 交友约会
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/join"
|
||||||
|
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
💬 加入社群
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
app/components/Navbar.tsx
Normal file
110
app/components/Navbar.tsx
Normal file
@@ -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 (
|
||||||
|
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-lg border-b border-gray-100">
|
||||||
|
<div className="max-w-[1600px] mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
|
||||||
|
{/* Logo */}
|
||||||
|
<Link href="/" className="flex items-center gap-2 shrink-0">
|
||||||
|
<span className="text-2xl">🌏</span>
|
||||||
|
<span className="text-lg font-bold text-gray-900 tracking-tight">
|
||||||
|
游牧中国
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Center Nav */}
|
||||||
|
<div className="hidden md:flex items-center gap-1">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link
|
||||||
|
key={link.label}
|
||||||
|
href={link.href}
|
||||||
|
className={`flex items-center gap-1.5 px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||||
|
isActive(link.href)
|
||||||
|
? "text-gray-900 bg-gray-100"
|
||||||
|
: "text-gray-500 hover:text-gray-900 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{link.icon}</span>
|
||||||
|
<span>{link.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side */}
|
||||||
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
|
<button className="hidden sm:block text-sm text-gray-600 hover:text-gray-900 px-3 py-2 rounded-full hover:bg-gray-50 transition-colors">
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
href="/join"
|
||||||
|
className="text-sm bg-[#ff4d4f] text-white px-4 py-2 rounded-full hover:bg-[#ff7a45] transition-colors font-medium"
|
||||||
|
>
|
||||||
|
加入我们 →
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Mobile hamburger */}
|
||||||
|
<button
|
||||||
|
className="md:hidden ml-1 p-2 rounded-lg hover:bg-gray-100"
|
||||||
|
onClick={() => setMobileOpen(!mobileOpen)}
|
||||||
|
>
|
||||||
|
{mobileOpen ? (
|
||||||
|
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<path d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<path d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<div className="md:hidden border-t border-gray-100 bg-white px-4 py-3 space-y-1">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link
|
||||||
|
key={link.label}
|
||||||
|
href={link.href}
|
||||||
|
className={`flex items-center gap-2 px-4 py-3 rounded-xl text-sm font-medium ${
|
||||||
|
isActive(link.href)
|
||||||
|
? "text-gray-900 bg-gray-50"
|
||||||
|
: "text-gray-500 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
>
|
||||||
|
<span className="text-lg">{link.icon}</span>
|
||||||
|
<span>{link.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<div className="pt-2 border-t border-gray-100">
|
||||||
|
<button className="w-full text-left px-4 py-3 text-sm text-gray-600 hover:bg-gray-50 rounded-xl">
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
245
app/components/Sidebar.tsx
Normal file
245
app/components/Sidebar.tsx
Normal file
@@ -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 (
|
||||||
|
<aside className="w-[320px] shrink-0 hidden lg:block">
|
||||||
|
<div className="sticky top-20 space-y-4 sidebar-scroll max-h-[calc(100vh-100px)] overflow-y-auto pr-1">
|
||||||
|
{/* Insurance / Visa Promo */}
|
||||||
|
<div className="rounded-2xl bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-100 p-5">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<span className="text-2xl">🛡️</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm">全球旅行保险</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
|
为数字游民定制的全球医疗保障方案
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-emerald-700 bg-emerald-100/50 rounded-lg px-3 py-2 mb-3">
|
||||||
|
覆盖 195+ 国家和地区,远程工作专属条款,¥15/天起
|
||||||
|
</p>
|
||||||
|
<button className="w-full text-center text-sm font-semibold text-emerald-700 hover:text-emerald-800 bg-white border border-emerald-200 rounded-full py-2 transition-colors hover:bg-emerald-50">
|
||||||
|
了解更多 →
|
||||||
|
</button>
|
||||||
|
<p className="text-[10px] text-gray-400 text-center mt-2">广告</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upcoming Meetups */}
|
||||||
|
<div className="rounded-2xl bg-white border border-gray-100 shadow-sm p-5">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
||||||
|
🍹 近期聚会
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] text-gray-400">8场/月</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{meetups.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.city + m.date}
|
||||||
|
className="flex items-center justify-between py-2 border-b border-gray-50 last:border-0 last:pb-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span className="text-lg">{m.emoji}</span>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-800">
|
||||||
|
{m.city}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-gray-400">{m.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] text-gray-400 bg-gray-50 rounded-full px-2 py-0.5">
|
||||||
|
{m.attendees} 人报名
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button className="w-full text-center text-xs font-medium text-gray-500 hover:text-gray-700 mt-3 pt-3 border-t border-gray-100 transition-colors">
|
||||||
|
查看全部聚会 →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Traveling Now */}
|
||||||
|
<div className="rounded-2xl bg-white border border-gray-100 shadow-sm p-5">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
||||||
|
🛩️ 正在旅行
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] text-gray-400">
|
||||||
|
{travelingNow.length} 人
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{travelingNow.map((m) => (
|
||||||
|
<div key={m.name} className="group relative">
|
||||||
|
<div
|
||||||
|
className="w-9 h-9 rounded-full flex items-center justify-center text-xs text-white font-medium shadow-sm cursor-pointer hover:scale-110 transition-transform avatar-online"
|
||||||
|
style={{ backgroundColor: m.bg }}
|
||||||
|
title={`${m.name} · ${m.destination}`}
|
||||||
|
>
|
||||||
|
{m.name.charAt(0)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Members */}
|
||||||
|
<div className="rounded-2xl bg-white border border-gray-100 shadow-sm p-5">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
||||||
|
👋 新加入成员
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] text-gray-400">86人/月</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{newMembers.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.name}
|
||||||
|
className="w-9 h-9 rounded-full flex items-center justify-center text-xs text-white font-medium shadow-sm cursor-pointer hover:scale-110 transition-transform"
|
||||||
|
style={{ backgroundColor: m.bg }}
|
||||||
|
>
|
||||||
|
{m.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hot Topics */}
|
||||||
|
<div className="rounded-2xl bg-white border border-gray-100 shadow-sm p-5">
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm flex items-center gap-1.5 mb-4">
|
||||||
|
🔥 热门讨论
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{hotTopics.map((t, i) => (
|
||||||
|
<a
|
||||||
|
key={t.title}
|
||||||
|
href="#"
|
||||||
|
className="flex items-start gap-2.5 group"
|
||||||
|
>
|
||||||
|
<span className="text-xs font-bold text-gray-300 mt-0.5 w-4 shrink-0">
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm text-gray-700 group-hover:text-gray-900 transition-colors line-clamp-1 leading-snug">
|
||||||
|
{t.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-gray-400 mt-0.5">
|
||||||
|
{t.views.toLocaleString()} 次浏览
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button className="w-full text-center text-xs font-medium text-gray-500 hover:text-gray-700 mt-3 pt-3 border-t border-gray-100 transition-colors">
|
||||||
|
查看更多话题 →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat CTA */}
|
||||||
|
<div className="rounded-2xl bg-gradient-to-br from-violet-50 to-purple-50 border border-violet-100 p-5">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<span className="text-2xl">💬</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm">
|
||||||
|
游牧中国社区聊天
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
|
本月已有 3,200+ 条消息
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="w-full text-center text-sm font-semibold text-violet-700 hover:text-violet-800 bg-white border border-violet-200 rounded-full py-2 transition-colors hover:bg-violet-50">
|
||||||
|
加入聊天 →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visa Service */}
|
||||||
|
<div className="rounded-2xl bg-gradient-to-br from-amber-50 to-orange-50 border border-amber-100 p-5">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<span className="text-2xl">🏛️</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm">签证代办服务</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
|
数字游民签证、工作签证、税务规划一站式服务
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||||
|
<span className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5">
|
||||||
|
🇹🇭 泰国
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5">
|
||||||
|
🇵🇹 葡萄牙
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5">
|
||||||
|
🇪🇸 西班牙
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5">
|
||||||
|
🇮🇩 印尼
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button className="w-full text-center text-sm font-semibold text-amber-700 hover:text-amber-800 bg-white border border-amber-200 rounded-full py-2 transition-colors hover:bg-amber-50">
|
||||||
|
了解详情 →
|
||||||
|
</button>
|
||||||
|
<p className="text-[10px] text-gray-400 text-center mt-2">广告</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
384
app/data/cities.ts
Normal file
384
app/data/cities.ts
Normal file
@@ -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 = [
|
||||||
|
"全部",
|
||||||
|
"热门",
|
||||||
|
"便宜",
|
||||||
|
"高速网络",
|
||||||
|
"温暖",
|
||||||
|
"安全",
|
||||||
|
"海滨",
|
||||||
|
"山城",
|
||||||
|
"都市",
|
||||||
|
"美食",
|
||||||
|
"文化",
|
||||||
|
"一线城市",
|
||||||
|
"新一线",
|
||||||
|
"宜居",
|
||||||
|
"历史名城",
|
||||||
|
"友好社区",
|
||||||
|
"户外运动",
|
||||||
|
];
|
||||||
304
app/dating/page.tsx
Normal file
304
app/dating/page.tsx
Normal file
@@ -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<TabType>("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 (
|
||||||
|
<div className="min-h-screen bg-[#fafafa]">
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex justify-center pt-6 pb-4 px-4">
|
||||||
|
<div className="inline-flex rounded-full bg-white p-1 shadow-sm border border-gray-100">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("friends")}
|
||||||
|
className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all duration-200 ${
|
||||||
|
activeTab === "friends"
|
||||||
|
? "bg-[#ff4d4f] text-white shadow-sm"
|
||||||
|
: "text-gray-600 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
交友
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("dating")}
|
||||||
|
className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all duration-200 ${
|
||||||
|
activeTab === "dating"
|
||||||
|
? "bg-[#ff4d4f] text-white shadow-sm"
|
||||||
|
: "text-gray-600 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
约会
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row gap-6 max-w-7xl mx-auto px-4 pb-12">
|
||||||
|
{/* Left Sidebar - hidden on mobile */}
|
||||||
|
<aside className="hidden md:block w-72 flex-shrink-0">
|
||||||
|
<div className="sticky top-24">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 mb-1">
|
||||||
|
约会匹配 💕
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">这些人喜欢了你</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{MOCK_MATCHES.map((match) => (
|
||||||
|
<div
|
||||||
|
key={match.id}
|
||||||
|
className="flex items-center gap-3 p-3 rounded-xl bg-white border border-gray-100 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={match.photo}
|
||||||
|
alt={match.name}
|
||||||
|
className="w-12 h-12 rounded-full object-cover shadow-sm flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-gray-900 truncate">
|
||||||
|
{match.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 truncate">
|
||||||
|
{match.location}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-400 bg-gray-50 px-2 py-1 rounded-full flex-shrink-0">
|
||||||
|
{match.timeAgo}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Center - Main Card */}
|
||||||
|
<main className="flex-1 min-w-0 flex flex-col items-center">
|
||||||
|
<div className="w-full max-w-md mx-auto">
|
||||||
|
<div
|
||||||
|
className={`relative rounded-2xl overflow-hidden bg-gradient-to-br ${currentProfile.gradient} p-8 pb-6 shadow-xl border border-white/20`}
|
||||||
|
>
|
||||||
|
{/* Top right: badge + report */}
|
||||||
|
<div className="absolute top-4 right-4 flex flex-col items-end gap-1">
|
||||||
|
<span className="px-3 py-1.5 rounded-full bg-white/90 text-gray-800 text-xs font-medium backdrop-blur-sm">
|
||||||
|
{badgeText}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-[#ff4d4f] hover:text-red-600 text-sm font-medium"
|
||||||
|
>
|
||||||
|
举报资料
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className="flex justify-center mt-12 mb-6">
|
||||||
|
<img
|
||||||
|
src={currentProfile.photo}
|
||||||
|
alt={currentProfile.name}
|
||||||
|
className="w-28 h-28 rounded-full shadow-lg border-4 border-white/50 object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Name, age, location */}
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<h3 className="text-xl font-bold text-white drop-shadow-sm">
|
||||||
|
{currentProfile.name},{currentProfile.age}
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/90 text-sm mt-1">
|
||||||
|
📍 {currentProfile.location}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-white/90 text-xs font-medium mb-2">
|
||||||
|
匹配标签:
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center">
|
||||||
|
{currentProfile.tags.map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="px-3 py-1 rounded-full bg-white/80 text-gray-800 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bio */}
|
||||||
|
<p className="text-white/95 text-sm leading-relaxed text-center mb-8 px-2">
|
||||||
|
{currentProfile.bio}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex items-center justify-center gap-8">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleDislike}
|
||||||
|
className="w-16 h-16 rounded-full bg-white shadow-lg flex items-center justify-center text-[#ff4d4f] hover:bg-red-50 hover:scale-105 active:scale-95 transition-all"
|
||||||
|
aria-label="不感兴趣"
|
||||||
|
>
|
||||||
|
<span className="text-2xl">✕</span>
|
||||||
|
</button>
|
||||||
|
<span className="text-xs text-white/80 font-medium">
|
||||||
|
不感兴趣
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleLike}
|
||||||
|
className="w-16 h-16 rounded-full bg-[#22c55e] shadow-lg flex items-center justify-center text-white hover:bg-[#16a34a] hover:scale-105 active:scale-95 transition-all"
|
||||||
|
aria-label="喜欢"
|
||||||
|
>
|
||||||
|
<span className="text-2xl">💚</span>
|
||||||
|
</button>
|
||||||
|
<span className="text-xs text-white/80 font-medium">喜欢</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress indicator */}
|
||||||
|
<p className="text-center text-sm text-gray-500 mt-4">
|
||||||
|
{currentIndex + 1} / {MOCK_PROFILES.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Right spacer for desktop balance */}
|
||||||
|
<div className="hidden lg:block w-72 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
227
app/globals.css
Normal file
227
app/globals.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
280
app/join/page.tsx
Normal file
280
app/join/page.tsx
Normal file
@@ -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<FormState>(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 (
|
||||||
|
<div className="min-h-screen bg-[#fafafa]">
|
||||||
|
{/* Top Section: Gradient Hero */}
|
||||||
|
<section className="relative overflow-hidden bg-gradient-to-br from-orange-400 via-orange-500 to-rose-500 px-4 pt-12 pb-16 sm:pt-16 sm:pb-20">
|
||||||
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight text-white drop-shadow-sm sm:text-4xl md:text-5xl">
|
||||||
|
加入游牧中国社区
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-base text-white/95 sm:text-lg md:text-xl">
|
||||||
|
填写信息加入全球华人数字游民社群
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Form Card */}
|
||||||
|
<section className="relative z-10 mx-auto -mt-8 max-w-lg px-4 pb-12 sm:-mt-10 sm:px-6">
|
||||||
|
<div className="rounded-2xl bg-white p-6 shadow-lg sm:p-8">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{/* 昵称 */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="nickname"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
昵称 <span className="text-[#ff4d4f]">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nickname"
|
||||||
|
type="text"
|
||||||
|
value={form.nickname}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 性别 */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-700">
|
||||||
|
性别
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
{["男", "女", "其他"].map((g) => (
|
||||||
|
<label
|
||||||
|
key={g}
|
||||||
|
className="flex cursor-pointer items-center gap-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="gender"
|
||||||
|
value={g}
|
||||||
|
checked={form.gender === g}
|
||||||
|
onChange={(e) => updateField("gender", e.target.value)}
|
||||||
|
className="h-4 w-4 border-gray-300 text-[#ff4d4f] focus:ring-[#ff4d4f]/20"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">{g}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 当前所在城市 */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="city"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
当前所在城市
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="city"
|
||||||
|
type="text"
|
||||||
|
value={form.city}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 职业 */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="occupation"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
职业
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="occupation"
|
||||||
|
value={form.occupation}
|
||||||
|
onChange={(e) => updateField("occupation", e.target.value)}
|
||||||
|
className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20"
|
||||||
|
>
|
||||||
|
<option value="">请选择职业</option>
|
||||||
|
{OCCUPATIONS.map((o) => (
|
||||||
|
<option key={o} value={o}>
|
||||||
|
{o}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 学历 */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="education"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
学历
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="education"
|
||||||
|
value={form.education}
|
||||||
|
onChange={(e) => updateField("education", e.target.value)}
|
||||||
|
className="w-full rounded-xl border border-gray-200 px-4 py-3 text-gray-900 transition-colors focus:border-[#ff4d4f] focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20"
|
||||||
|
>
|
||||||
|
<option value="">请选择学历</option>
|
||||||
|
{EDUCATIONS.map((e) => (
|
||||||
|
<option key={e} value={e}>
|
||||||
|
{e}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 微信号 */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="wechat"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
微信号 <span className="text-[#ff4d4f]">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="wechat"
|
||||||
|
type="text"
|
||||||
|
value={form.wechat}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 手机号 */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="phone"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
手机号
|
||||||
|
</label>
|
||||||
|
<div className="flex rounded-xl border border-gray-200 bg-white transition-colors focus-within:border-[#ff4d4f] focus-within:ring-2 focus-within:ring-[#ff4d4f]/20">
|
||||||
|
<span className="flex items-center border-r border-gray-200 px-4 py-3 text-sm text-gray-500">
|
||||||
|
+86
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
id="phone"
|
||||||
|
type="tel"
|
||||||
|
value={form.phone}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 简单介绍自己 */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="intro"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
简单介绍自己
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="intro"
|
||||||
|
rows={3}
|
||||||
|
value={form.intro}
|
||||||
|
onChange={(e) => updateField("intro", e.target.value)}
|
||||||
|
placeholder="简单介绍一下你的背景、兴趣或加入原因..."
|
||||||
|
className="w-full resize-none 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<div className="pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full rounded-xl bg-[#ff4d4f] px-4 py-3.5 font-semibold text-white transition-colors hover:bg-[#e64547] active:bg-[#cc3d3f]"
|
||||||
|
>
|
||||||
|
提交申请 →
|
||||||
|
</button>
|
||||||
|
<p className="mt-3 text-center text-xs text-gray-500">
|
||||||
|
提交后,我们会在24小时内通过微信联系你
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Bottom: Trust Indicators */}
|
||||||
|
<section className="border-t border-gray-200 bg-white px-4 py-10">
|
||||||
|
<div className="mx-auto flex max-w-lg flex-wrap items-center justify-center gap-8 sm:gap-12">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl font-bold text-gray-900">2,860+</span>
|
||||||
|
<span className="text-sm text-gray-600">成员</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl font-bold text-gray-900">30+</span>
|
||||||
|
<span className="text-sm text-gray-600">城市</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl font-bold text-gray-900">每月 8+</span>
|
||||||
|
<span className="text-sm text-gray-600">聚会</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
app/layout.tsx
Normal file
37
app/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import Navbar from "./components/Navbar";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "游牧中国 - 数字游民的理想目的地",
|
||||||
|
description:
|
||||||
|
"发现全球最适合远程工作和生活的城市,加入中国数字游民社区,探索远程办公新方式",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
441
app/meetups/page.tsx
Normal file
441
app/meetups/page.tsx
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
|
||||||
|
/* ===== Types ===== */
|
||||||
|
interface Attendee {
|
||||||
|
name: string;
|
||||||
|
photo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Meetup {
|
||||||
|
id: string;
|
||||||
|
city: string;
|
||||||
|
emoji: string;
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
venue: string;
|
||||||
|
address: string;
|
||||||
|
description: string;
|
||||||
|
rsvpCount: number;
|
||||||
|
gradientFrom: string;
|
||||||
|
gradientTo: string;
|
||||||
|
attendees: Attendee[];
|
||||||
|
isUpcoming: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Mock Data ===== */
|
||||||
|
const allMeetups: Meetup[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
city: "大理",
|
||||||
|
emoji: "🏯",
|
||||||
|
date: "2026-03-15",
|
||||||
|
time: "14:00",
|
||||||
|
venue: "大理古城人民路咖啡馆",
|
||||||
|
address: "大理市大理古城人民路中段",
|
||||||
|
description: "苍山脚下,洱海边,与大理的慢生活游民们一起喝咖啡、晒太阳、聊人生。",
|
||||||
|
rsvpCount: 12,
|
||||||
|
gradientFrom: "#3b82f6",
|
||||||
|
gradientTo: "#4f46e5",
|
||||||
|
attendees: [
|
||||||
|
{ name: "林晓", photo: "https://i.pravatar.cc/150?img=1" },
|
||||||
|
{ name: "张明", photo: "https://i.pravatar.cc/150?img=2" },
|
||||||
|
{ name: "陈静", photo: "https://i.pravatar.cc/150?img=3" },
|
||||||
|
{ name: "周杰", photo: "https://i.pravatar.cc/150?img=4" },
|
||||||
|
{ name: "吴芳", photo: "https://i.pravatar.cc/150?img=5" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
city: "成都",
|
||||||
|
emoji: "🐼",
|
||||||
|
date: "2026-03-18",
|
||||||
|
time: "19:00",
|
||||||
|
venue: "方所书店",
|
||||||
|
address: "成都市锦江区中纱帽街8号远洋太古里",
|
||||||
|
description: "天府之国游民聚会,在文艺书店里品茶聊天,探讨成都的慢生活与远程工作可能性。",
|
||||||
|
rsvpCount: 9,
|
||||||
|
gradientFrom: "#22c55e",
|
||||||
|
gradientTo: "#10b981",
|
||||||
|
attendees: [
|
||||||
|
{ name: "杨帆", photo: "https://i.pravatar.cc/150?img=6" },
|
||||||
|
{ name: "赵磊", photo: "https://i.pravatar.cc/150?img=7" },
|
||||||
|
{ name: "黄薇", photo: "https://i.pravatar.cc/150?img=8" },
|
||||||
|
{ name: "孙浩", photo: "https://i.pravatar.cc/150?img=9" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
city: "深圳",
|
||||||
|
emoji: "🌃",
|
||||||
|
date: "2026-03-20",
|
||||||
|
time: "19:30",
|
||||||
|
venue: "WeWork 卓越前海壹号",
|
||||||
|
address: "深圳市南山区前海深港合作区卓越前海壹号",
|
||||||
|
description: "深圳数字游民线下见面会,探讨创业、远程工作与湾区发展机遇。",
|
||||||
|
rsvpCount: 15,
|
||||||
|
gradientFrom: "#ff4d4f",
|
||||||
|
gradientTo: "#ff7a45",
|
||||||
|
attendees: [
|
||||||
|
{ name: "李娜", photo: "https://i.pravatar.cc/150?img=10" },
|
||||||
|
{ name: "王强", photo: "https://i.pravatar.cc/150?img=11" },
|
||||||
|
{ name: "刘洋", photo: "https://i.pravatar.cc/150?img=12" },
|
||||||
|
{ name: "陈思", photo: "https://i.pravatar.cc/150?img=13" },
|
||||||
|
{ name: "郑凯", photo: "https://i.pravatar.cc/150?img=14" },
|
||||||
|
{ name: "何琳", photo: "https://i.pravatar.cc/150?img=15" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
city: "上海",
|
||||||
|
emoji: "🏙️",
|
||||||
|
date: "2026-03-22",
|
||||||
|
time: "19:00",
|
||||||
|
venue: "裸心社 新天地",
|
||||||
|
address: "上海市黄浦区马当路388号复兴SOHO广场",
|
||||||
|
description: "魔都数字游民月度聚会,在时尚共享空间交流国际化远程工作与创业心得。",
|
||||||
|
rsvpCount: 18,
|
||||||
|
gradientFrom: "#ec4899",
|
||||||
|
gradientTo: "#ef4444",
|
||||||
|
attendees: [
|
||||||
|
{ name: "许婷", photo: "https://i.pravatar.cc/150?img=16" },
|
||||||
|
{ name: "马超", photo: "https://i.pravatar.cc/150?img=17" },
|
||||||
|
{ name: "罗敏", photo: "https://i.pravatar.cc/150?img=18" },
|
||||||
|
{ name: "林晓", photo: "https://i.pravatar.cc/150?img=19" },
|
||||||
|
{ name: "张明", photo: "https://i.pravatar.cc/150?img=20" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
city: "杭州",
|
||||||
|
emoji: "⛲",
|
||||||
|
date: "2026-03-25",
|
||||||
|
time: "18:00",
|
||||||
|
venue: "梦想小镇·良仓咖啡",
|
||||||
|
address: "杭州市余杭区梦想小镇互联网村",
|
||||||
|
description: "西湖畔数字游民聚会,在创业氛围浓厚的梦想小镇交流产品与运营经验。",
|
||||||
|
rsvpCount: 11,
|
||||||
|
gradientFrom: "#06b6d4",
|
||||||
|
gradientTo: "#0d9488",
|
||||||
|
attendees: [
|
||||||
|
{ name: "陈静", photo: "https://i.pravatar.cc/150?img=21" },
|
||||||
|
{ name: "周杰", photo: "https://i.pravatar.cc/150?img=22" },
|
||||||
|
{ name: "吴芳", photo: "https://i.pravatar.cc/150?img=23" },
|
||||||
|
{ name: "杨帆", photo: "https://i.pravatar.cc/150?img=24" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
city: "厦门",
|
||||||
|
emoji: "🌊",
|
||||||
|
date: "2026-03-28",
|
||||||
|
time: "15:00",
|
||||||
|
venue: "鼓浪屿海角8号咖啡馆",
|
||||||
|
address: "厦门市思明区鼓浪屿龙头路",
|
||||||
|
description: "海岛慢生活聚会,在鼓浪屿的文艺咖啡馆里结识热爱海边远程工作的游民朋友。",
|
||||||
|
rsvpCount: 7,
|
||||||
|
gradientFrom: "#0ea5e9",
|
||||||
|
gradientTo: "#0284c7",
|
||||||
|
attendees: [
|
||||||
|
{ name: "赵磊", photo: "https://i.pravatar.cc/150?img=25" },
|
||||||
|
{ name: "黄薇", photo: "https://i.pravatar.cc/150?img=26" },
|
||||||
|
{ name: "孙浩", photo: "https://i.pravatar.cc/150?img=27" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
city: "北京",
|
||||||
|
emoji: "🏛️",
|
||||||
|
date: "2026-03-30",
|
||||||
|
time: "19:00",
|
||||||
|
venue: "氪空间 望京SOHO",
|
||||||
|
address: "北京市朝阳区望京街10号望京SOHO T3",
|
||||||
|
description: "首都数字游民聚会,在望京科技圈交流北漂远程工作与创业故事。",
|
||||||
|
rsvpCount: 14,
|
||||||
|
gradientFrom: "#6366f1",
|
||||||
|
gradientTo: "#4f46e5",
|
||||||
|
attendees: [
|
||||||
|
{ name: "李娜", photo: "https://i.pravatar.cc/150?img=28" },
|
||||||
|
{ name: "王强", photo: "https://i.pravatar.cc/150?img=29" },
|
||||||
|
{ name: "刘洋", photo: "https://i.pravatar.cc/150?img=30" },
|
||||||
|
{ name: "陈思", photo: "https://i.pravatar.cc/150?img=31" },
|
||||||
|
{ name: "郑凯", photo: "https://i.pravatar.cc/150?img=32" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
city: "昆明",
|
||||||
|
emoji: "🌸",
|
||||||
|
date: "2026-04-02",
|
||||||
|
time: "14:00",
|
||||||
|
venue: "翠湖边上咖啡馆",
|
||||||
|
address: "昆明市五华区翠湖东路",
|
||||||
|
description: "春城数字游民聚会,在四季如春的昆明享受慢节奏,交流旅居与远程工作心得。",
|
||||||
|
rsvpCount: 8,
|
||||||
|
gradientFrom: "#f472b6",
|
||||||
|
gradientTo: "#ec4899",
|
||||||
|
attendees: [
|
||||||
|
{ name: "何琳", photo: "https://i.pravatar.cc/150?img=33" },
|
||||||
|
{ name: "许婷", photo: "https://i.pravatar.cc/150?img=34" },
|
||||||
|
{ name: "马超", photo: "https://i.pravatar.cc/150?img=35" },
|
||||||
|
{ name: "罗敏", photo: "https://i.pravatar.cc/150?img=36" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
city: "广州",
|
||||||
|
emoji: "🌺",
|
||||||
|
date: "2026-04-05",
|
||||||
|
time: "19:00",
|
||||||
|
venue: "TIMETABLE 天德广场",
|
||||||
|
address: "广州市天河区临江大道395号天德广场",
|
||||||
|
description: "羊城数字游民聚会,在珠江新城的高端共享空间探讨粤港澳大湾区远程工作机遇。",
|
||||||
|
rsvpCount: 12,
|
||||||
|
gradientFrom: "#f59e0b",
|
||||||
|
gradientTo: "#ea580c",
|
||||||
|
attendees: [
|
||||||
|
{ name: "林晓", photo: "https://i.pravatar.cc/150?img=37" },
|
||||||
|
{ name: "张明", photo: "https://i.pravatar.cc/150?img=38" },
|
||||||
|
{ name: "陈静", photo: "https://i.pravatar.cc/150?img=39" },
|
||||||
|
{ name: "周杰", photo: "https://i.pravatar.cc/150?img=40" },
|
||||||
|
{ name: "吴芳", photo: "https://i.pravatar.cc/150?img=41" },
|
||||||
|
],
|
||||||
|
isUpcoming: true,
|
||||||
|
},
|
||||||
|
/* ===== 往期聚会 ===== */
|
||||||
|
{
|
||||||
|
id: "10",
|
||||||
|
city: "南京",
|
||||||
|
emoji: "🍃",
|
||||||
|
date: "2026-03-05",
|
||||||
|
time: "19:00",
|
||||||
|
venue: "先锋书店 五台山店",
|
||||||
|
address: "南京市鼓楼区广州路173号五台山体育馆地下",
|
||||||
|
description: "金陵数字游民聚会,在南京最具文艺气息的书店里交流古都文化与远程工作。",
|
||||||
|
rsvpCount: 6,
|
||||||
|
gradientFrom: "#84cc16",
|
||||||
|
gradientTo: "#65a30d",
|
||||||
|
attendees: [
|
||||||
|
{ name: "杨帆", photo: "https://i.pravatar.cc/150?img=42" },
|
||||||
|
{ name: "赵磊", photo: "https://i.pravatar.cc/150?img=43" },
|
||||||
|
],
|
||||||
|
isUpcoming: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "11",
|
||||||
|
city: "重庆",
|
||||||
|
emoji: "🌶️",
|
||||||
|
date: "2026-03-02",
|
||||||
|
time: "18:30",
|
||||||
|
venue: "洪崖洞观景咖啡馆",
|
||||||
|
address: "重庆市渝中区嘉滨路88号洪崖洞",
|
||||||
|
description: "山城数字游民聚会,在魔幻8D城市里结识热爱火锅与远程工作的游民朋友。",
|
||||||
|
rsvpCount: 10,
|
||||||
|
gradientFrom: "#dc2626",
|
||||||
|
gradientTo: "#b91c1c",
|
||||||
|
attendees: [
|
||||||
|
{ name: "黄薇", photo: "https://i.pravatar.cc/150?img=44" },
|
||||||
|
{ name: "孙浩", photo: "https://i.pravatar.cc/150?img=45" },
|
||||||
|
{ name: "李娜", photo: "https://i.pravatar.cc/150?img=46" },
|
||||||
|
{ name: "王强", photo: "https://i.pravatar.cc/150?img=47" },
|
||||||
|
],
|
||||||
|
isUpcoming: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "12",
|
||||||
|
city: "长沙",
|
||||||
|
emoji: "🎪",
|
||||||
|
date: "2026-02-28",
|
||||||
|
time: "19:00",
|
||||||
|
venue: "梅溪湖创新中心 星巴克",
|
||||||
|
address: "长沙市岳麓区梅溪湖国际新城",
|
||||||
|
description: "星城数字游民聚会,在网红城市长沙交流新媒体、直播与远程工作新业态。",
|
||||||
|
rsvpCount: 8,
|
||||||
|
gradientFrom: "#f97316",
|
||||||
|
gradientTo: "#ea580c",
|
||||||
|
attendees: [
|
||||||
|
{ name: "刘洋", photo: "https://i.pravatar.cc/150?img=48" },
|
||||||
|
{ name: "陈思", photo: "https://i.pravatar.cc/150?img=49" },
|
||||||
|
{ name: "郑凯", photo: "https://i.pravatar.cc/150?img=50" },
|
||||||
|
],
|
||||||
|
isUpcoming: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ===== Date formatting ===== */
|
||||||
|
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||||
|
|
||||||
|
function formatMeetupDate(dateStr: string, time: string): string {
|
||||||
|
const d = new Date(dateStr + "T" + time);
|
||||||
|
const weekday = weekdays[d.getDay()];
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = d.getMonth() + 1;
|
||||||
|
const day = d.getDate();
|
||||||
|
const timeFormatted = time.slice(0, 5);
|
||||||
|
return `${weekday}, ${year}年${month}月${day}日, ${timeFormatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Meetup Card Component ===== */
|
||||||
|
function MeetupCard({ meetup }: { meetup: Meetup }) {
|
||||||
|
const formattedDate = formatMeetupDate(meetup.date, meetup.time);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="group bg-white rounded-2xl overflow-hidden shadow-[0_2px_8px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)] hover:shadow-[0_12px_28px_rgba(0,0,0,0.12),0_0_0_1px_rgba(0,0,0,0.05)] hover:-translate-y-2 transition-all duration-300 cursor-pointer flex flex-col h-full">
|
||||||
|
{/* Gradient header */}
|
||||||
|
<div
|
||||||
|
className="h-24 sm:h-28 flex flex-col justify-end p-4 text-white"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${meetup.gradientFrom}, ${meetup.gradientTo})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-xs sm:text-sm text-white/90 font-medium">
|
||||||
|
{formattedDate}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 flex flex-col p-4 sm:p-5">
|
||||||
|
{/* City + RSVP badge */}
|
||||||
|
<div className="flex items-center justify-between gap-3 mb-2">
|
||||||
|
<h3 className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||||
|
{meetup.emoji} {meetup.city}
|
||||||
|
</h3>
|
||||||
|
<span className="shrink-0 bg-[#ff4d4f] text-white text-xs font-semibold px-2.5 py-1 rounded-full">
|
||||||
|
{meetup.rsvpCount} 人报名
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Venue */}
|
||||||
|
<p className="text-sm text-gray-600 font-medium mb-0.5">
|
||||||
|
@ {meetup.venue}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mb-3">{meetup.address}</p>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-2 flex-1 mb-2">
|
||||||
|
{meetup.description}
|
||||||
|
</p>
|
||||||
|
<button className="text-sm font-medium text-[#ff4d4f] hover:text-[#ff7a45] transition-colors text-left w-fit">
|
||||||
|
查看详情 →
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Attendee avatars */}
|
||||||
|
<div className="flex items-center gap-1 mt-4 pt-4 border-t border-gray-100">
|
||||||
|
<div className="flex -space-x-2">
|
||||||
|
{meetup.attendees.map((a) => (
|
||||||
|
<img
|
||||||
|
key={a.name}
|
||||||
|
src={a.photo}
|
||||||
|
alt={a.name}
|
||||||
|
className="w-7 h-7 sm:w-8 sm:h-8 rounded-full border-2 border-white shadow-sm object-cover"
|
||||||
|
title={a.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-400 ml-1">
|
||||||
|
已报名成员
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Page Component ===== */
|
||||||
|
export default function MeetupsPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState<"upcoming" | "previous">("upcoming");
|
||||||
|
|
||||||
|
const upcomingMeetups = useMemo(
|
||||||
|
() => allMeetups.filter((m) => m.isUpcoming),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const previousMeetups = useMemo(
|
||||||
|
() => allMeetups.filter((m) => !m.isUpcoming),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayedMeetups =
|
||||||
|
activeTab === "upcoming" ? upcomingMeetups : previousMeetups;
|
||||||
|
const totalCount =
|
||||||
|
activeTab === "upcoming"
|
||||||
|
? upcomingMeetups.length
|
||||||
|
: previousMeetups.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#fafafa]">
|
||||||
|
<main className="max-w-[1400px] mx-auto px-4 sm:px-6 md:px-10 py-8 sm:py-12">
|
||||||
|
{/* Page header */}
|
||||||
|
<header className="mb-8">
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 flex items-center gap-2">
|
||||||
|
<span className="w-1 h-8 bg-[#ff4d4f] rounded-full" />
|
||||||
|
线下聚会
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mt-1 text-sm sm:text-base">
|
||||||
|
与全球数字游民面对面交流,拓展人脉,分享经验
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-1 p-1 bg-white rounded-xl shadow-sm border border-gray-100 mb-8 inline-flex">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("upcoming")}
|
||||||
|
className={`px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
activeTab === "upcoming"
|
||||||
|
? "bg-[#ff4d4f] text-white shadow-sm"
|
||||||
|
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
即将举办
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("previous")}
|
||||||
|
className={`px-5 py-2.5 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
activeTab === "previous"
|
||||||
|
? "bg-[#ff4d4f] text-white shadow-sm"
|
||||||
|
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
往期聚会
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtitle with count */}
|
||||||
|
<p className="text-gray-500 text-sm mb-6">
|
||||||
|
{activeTab === "upcoming" ? "共" : "共"}
|
||||||
|
<span className="font-semibold text-gray-700 mx-1">
|
||||||
|
{totalCount}
|
||||||
|
</span>
|
||||||
|
{activeTab === "upcoming" ? "场即将举办的聚会" : "场往期聚会"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Meetup cards grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5 sm:gap-6">
|
||||||
|
{displayedMeetups.map((meetup) => (
|
||||||
|
<MeetupCard key={meetup.id} meetup={meetup} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
{displayedMeetups.length === 0 && (
|
||||||
|
<div className="text-center py-16 sm:py-24">
|
||||||
|
<span className="text-5xl sm:text-6xl mb-4 block">🍹</span>
|
||||||
|
<p className="text-gray-500 text-base sm:text-lg">
|
||||||
|
{activeTab === "upcoming"
|
||||||
|
? "暂无即将举办的聚会"
|
||||||
|
: "暂无往期聚会记录"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
527
app/page.tsx
Normal file
527
app/page.tsx
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import HeroSection from "./components/HeroSection";
|
||||||
|
import FilterBar from "./components/FilterBar";
|
||||||
|
import CityCard from "./components/CityCard";
|
||||||
|
import { cities } from "./data/cities";
|
||||||
|
|
||||||
|
/* ===== Avatar photos ===== */
|
||||||
|
const memberPhotos = [
|
||||||
|
{ name: "林晓", photo: "https://i.pravatar.cc/150?img=5" },
|
||||||
|
{ name: "张浩", photo: "https://i.pravatar.cc/150?img=3" },
|
||||||
|
{ name: "陈悦", photo: "https://i.pravatar.cc/150?img=9" },
|
||||||
|
{ name: "周杰", photo: "https://i.pravatar.cc/150?img=7" },
|
||||||
|
{ name: "吴婷", photo: "https://i.pravatar.cc/150?img=1" },
|
||||||
|
{ name: "杨帅", photo: "https://i.pravatar.cc/150?img=12" },
|
||||||
|
{ name: "赵琪", photo: "https://i.pravatar.cc/150?img=16" },
|
||||||
|
{ name: "黄磊", photo: "https://i.pravatar.cc/150?img=11" },
|
||||||
|
{ name: "孙莉", photo: "https://i.pravatar.cc/150?img=20" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const travelingNow = [
|
||||||
|
{ name: "小明", photo: "https://i.pravatar.cc/150?img=52", dest: "大理" },
|
||||||
|
{ name: "雨晴", photo: "https://i.pravatar.cc/150?img=44", dest: "成都" },
|
||||||
|
{ name: "阿杰", photo: "https://i.pravatar.cc/150?img=53", dest: "深圳" },
|
||||||
|
{ name: "小燕", photo: "https://i.pravatar.cc/150?img=47", dest: "杭州" },
|
||||||
|
{ name: "大伟", photo: "https://i.pravatar.cc/150?img=55", dest: "厦门" },
|
||||||
|
{ name: "佳琪", photo: "https://i.pravatar.cc/150?img=48", dest: "昆明" },
|
||||||
|
{ name: "志远", photo: "https://i.pravatar.cc/150?img=57", dest: "丽江" },
|
||||||
|
{ name: "心怡", photo: "https://i.pravatar.cc/150?img=49", dest: "三亚" },
|
||||||
|
{ name: "凯文", photo: "https://i.pravatar.cc/150?img=59", dest: "上海" },
|
||||||
|
{ name: "子豪", photo: "https://i.pravatar.cc/150?img=60", dest: "北京" },
|
||||||
|
{ name: "浩然", photo: "https://i.pravatar.cc/150?img=61", dest: "广州" },
|
||||||
|
{ name: "雅琪", photo: "https://i.pravatar.cc/150?img=21", dest: "南京" },
|
||||||
|
{ name: "天翔", photo: "https://i.pravatar.cc/150?img=67", dest: "长沙" },
|
||||||
|
{ name: "美玲", photo: "https://i.pravatar.cc/150?img=23", dest: "苏州" },
|
||||||
|
{ name: "俊杰", photo: "https://i.pravatar.cc/150?img=68", dest: "青岛" },
|
||||||
|
{ name: "晓华", photo: "https://i.pravatar.cc/150?img=26", dest: "珠海" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const meetups = [
|
||||||
|
{ city: "大理", emoji: "🏯", date: "3月9日", count: 12 },
|
||||||
|
{ city: "成都", emoji: "🐼", date: "3月12日", count: 8 },
|
||||||
|
{ city: "深圳", emoji: "🌃", date: "3月14日", count: 15 },
|
||||||
|
{ city: "厦门", emoji: "🌊", date: "3月16日", count: 6 },
|
||||||
|
{ city: "杭州", emoji: "⛲", date: "3月20日", count: 23 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hotTopics = [
|
||||||
|
{ title: "大理最佳共享办公空间推荐 Top 10", views: 2340 },
|
||||||
|
{ title: "2026年数字游民友好城市排名", views: 1890 },
|
||||||
|
{ title: "远程工作者的税务规划指南", views: 1560 },
|
||||||
|
{ title: "成都 vs 大理:哪个更适合游民?", views: 1230 },
|
||||||
|
{ title: "新手游民必备工具与装备清单", views: 980 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function safetyOrder(s: string): number {
|
||||||
|
const o: Record<string, number> = { 极好: 5, 很好: 4, 好: 3, 一般: 2, 差: 1 };
|
||||||
|
return o[s] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [activeFilter, setActiveFilter] = useState("全部");
|
||||||
|
const [sortBy, setSortBy] = useState("score");
|
||||||
|
|
||||||
|
const filteredCities = useMemo(() => {
|
||||||
|
let result = [...cities];
|
||||||
|
if (activeFilter !== "全部") {
|
||||||
|
result = result.filter((c) => c.tags.includes(activeFilter));
|
||||||
|
}
|
||||||
|
result.sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case "cost-asc":
|
||||||
|
return a.costPerMonth - b.costPerMonth;
|
||||||
|
case "cost-desc":
|
||||||
|
return b.costPerMonth - a.costPerMonth;
|
||||||
|
case "internet":
|
||||||
|
return b.internetSpeed - a.internetSpeed;
|
||||||
|
case "safety":
|
||||||
|
return safetyOrder(b.safety) - safetyOrder(a.safety);
|
||||||
|
case "nomads":
|
||||||
|
return b.nomadsNow - a.nomadsNow;
|
||||||
|
case "temperature":
|
||||||
|
return b.temperature - a.temperature;
|
||||||
|
default:
|
||||||
|
return b.overallScore - a.overallScore;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, [activeFilter, sortBy]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#fafafa]">
|
||||||
|
<HeroSection />
|
||||||
|
|
||||||
|
{/* Stats Strip */}
|
||||||
|
<div className="border-y border-gray-100 bg-white">
|
||||||
|
<div className="max-w-[1400px] mx-auto px-5 sm:px-10 py-4 flex flex-wrap items-center justify-center gap-6 sm:gap-12 text-center">
|
||||||
|
<div>
|
||||||
|
<span className="text-xl sm:text-2xl font-bold text-gray-900">18</span>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">个城市</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-8 bg-gray-100 hidden sm:block" />
|
||||||
|
<div>
|
||||||
|
<span className="text-xl sm:text-2xl font-bold text-gray-900">5,000+</span>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">位游民</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-8 bg-gray-100 hidden sm:block" />
|
||||||
|
<div>
|
||||||
|
<span className="text-xl sm:text-2xl font-bold text-gray-900">8+</span>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">场聚会/月</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-8 bg-gray-100 hidden sm:block" />
|
||||||
|
<div>
|
||||||
|
<span className="text-xl sm:text-2xl font-bold text-gray-900">¥3,000</span>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">起/月生活费</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Bar */}
|
||||||
|
<div className="px-3 sm:px-5 md:px-10 pt-5">
|
||||||
|
<FilterBar
|
||||||
|
activeFilter={activeFilter}
|
||||||
|
onFilterChange={setActiveFilter}
|
||||||
|
sortBy={sortBy}
|
||||||
|
onSortChange={setSortBy}
|
||||||
|
resultCount={filteredCities.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mixed Grid */}
|
||||||
|
<div className="items-grid">
|
||||||
|
<Link href="/join" className="right-item block">
|
||||||
|
<div className="right-item-header">加入社群</div>
|
||||||
|
<div className="flex flex-col items-center justify-center text-center p-2 sm:p-5 h-[calc(100%-38px)] sm:h-[calc(100%-42px)]">
|
||||||
|
<span className="text-2xl sm:text-5xl mb-1 sm:mb-3">💬</span>
|
||||||
|
<p className="text-[10px] sm:text-sm text-gray-600 mb-0.5">
|
||||||
|
加入数字游民微信社群
|
||||||
|
</p>
|
||||||
|
<p className="text-[9px] sm:text-xs text-gray-400 mb-1.5 sm:mb-4">
|
||||||
|
3,200+ 在线成员
|
||||||
|
</p>
|
||||||
|
<span className="bg-[#ff4d4f] text-white px-2.5 sm:px-6 py-1 sm:py-2 rounded-lg text-[10px] sm:text-sm font-semibold whitespace-nowrap">
|
||||||
|
加入社群 →
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/dating" className="right-item block">
|
||||||
|
<div className="right-item-header">新成员</div>
|
||||||
|
<div className="p-2 sm:p-5 flex flex-wrap gap-1 sm:gap-2 content-start">
|
||||||
|
{memberPhotos.map((m) => (
|
||||||
|
<img
|
||||||
|
key={m.name}
|
||||||
|
src={m.photo}
|
||||||
|
alt={m.name}
|
||||||
|
title={m.name}
|
||||||
|
className="w-6 h-6 sm:w-10 sm:h-10 rounded-full border-2 border-white shadow-sm object-cover"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="w-full mt-0.5 text-[9px] sm:text-xs text-gray-400 text-center">
|
||||||
|
本月 86 人加入
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/meetups" className="right-item block">
|
||||||
|
<div className="right-item-header">最新活动</div>
|
||||||
|
<div className="p-1.5 sm:p-4 flex flex-col gap-0.5 sm:gap-2.5 text-[10px] sm:text-sm text-gray-700 h-[calc(100%-38px)] sm:h-[calc(100%-42px)]">
|
||||||
|
<div className="flex items-center gap-1 bg-[#f5f6f7] rounded-lg px-1.5 py-1 sm:px-3 sm:py-2 font-medium text-[#333]">
|
||||||
|
🗓 3月15日
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 bg-[#f5f6f7] rounded-lg px-1.5 py-1 sm:px-3 sm:py-2 font-medium text-[#333]">
|
||||||
|
📍 深圳南山
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 bg-[#f5f6f7] rounded-lg px-1.5 py-1 sm:px-3 sm:py-2 font-medium text-[#333]">
|
||||||
|
👥 12人
|
||||||
|
</div>
|
||||||
|
<div className="flex -space-x-1 mt-auto">
|
||||||
|
{memberPhotos.slice(0, 5).map((m) => (
|
||||||
|
<img
|
||||||
|
key={m.name}
|
||||||
|
src={m.photo}
|
||||||
|
alt=""
|
||||||
|
className="w-4 h-4 sm:w-7 sm:h-7 rounded-full border-2 border-white shadow-sm object-cover"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{filteredCities.map((city, i) => (
|
||||||
|
<CityCard key={city.id} city={city} rank={i + 1} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{filteredCities.length === 0 && (
|
||||||
|
<div className="text-center py-16 sm:py-20">
|
||||||
|
<span className="text-4xl sm:text-5xl mb-4 block">🏜️</span>
|
||||||
|
<p className="text-gray-500 text-base sm:text-lg">
|
||||||
|
没有找到匹配的城市
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveFilter("全部")}
|
||||||
|
className="mt-4 text-sm text-gray-700 bg-gray-200 px-5 py-2 rounded-full hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
显示全部城市
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Popular Routes */}
|
||||||
|
<section className="max-w-[1500px] mx-auto px-3 sm:px-6 md:px-10 pt-8 sm:pt-12 pb-4">
|
||||||
|
<h2 className="text-lg sm:text-xl font-bold text-gray-900 mb-5 sm:mb-6 flex items-center gap-2">
|
||||||
|
<span className="w-1 h-5 bg-[#ff4d4f] rounded-full" />
|
||||||
|
热门路线
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "云南慢生活线",
|
||||||
|
cities: ["昆明", "大理", "丽江"],
|
||||||
|
emojis: ["🌸", "🏯", "🏔️"],
|
||||||
|
duration: "3-6个月",
|
||||||
|
cost: "¥3,200/月起",
|
||||||
|
desc: "从春城昆明出发,沿着滇西北一路感受最纯粹的慢生活",
|
||||||
|
gradient: "from-purple-500 to-pink-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "科技创业线",
|
||||||
|
cities: ["深圳", "杭州", "上海"],
|
||||||
|
emojis: ["🌃", "⛲", "🏙️"],
|
||||||
|
duration: "2-4个月",
|
||||||
|
cost: "¥7,000/月起",
|
||||||
|
desc: "穿越中国三大科技中心,连接最前沿的创业生态",
|
||||||
|
gradient: "from-blue-500 to-cyan-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "美食文化线",
|
||||||
|
cities: ["成都", "重庆", "长沙"],
|
||||||
|
emojis: ["🐼", "🌶️", "🎪"],
|
||||||
|
duration: "2-3个月",
|
||||||
|
cost: "¥4,000/月起",
|
||||||
|
desc: "巴蜀到湘楚,用味蕾丈量中国最火辣的城市",
|
||||||
|
gradient: "from-orange-500 to-red-500",
|
||||||
|
},
|
||||||
|
].map((route) => (
|
||||||
|
<div
|
||||||
|
key={route.title}
|
||||||
|
className="community-card group cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className={`h-2 bg-gradient-to-r ${route.gradient}`} />
|
||||||
|
<div className="p-4 sm:p-5">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm">
|
||||||
|
{route.title}
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] text-gray-400 bg-gray-50 rounded-full px-2 py-0.5">
|
||||||
|
{route.duration}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 mb-3">
|
||||||
|
{route.cities.map((city, i) => (
|
||||||
|
<span key={city} className="flex items-center gap-0.5 text-sm">
|
||||||
|
<span>{route.emojis[i]}</span>
|
||||||
|
<span className="font-medium text-gray-700">{city}</span>
|
||||||
|
{i < route.cities.length - 1 && (
|
||||||
|
<span className="text-gray-300 mx-1">→</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mb-3 leading-relaxed">
|
||||||
|
{route.desc}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
|
{route.cost}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-[#ff4d4f] font-medium group-hover:translate-x-1 transition-transform">
|
||||||
|
查看详情 →
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Community Section */}
|
||||||
|
<section id="community" className="max-w-[1500px] mx-auto px-3 sm:px-6 md:px-10 py-8 sm:py-12">
|
||||||
|
<h2 className="text-lg sm:text-xl font-bold text-gray-900 mb-5 sm:mb-6 flex items-center gap-2">
|
||||||
|
<span className="w-1 h-5 bg-[#ff4d4f] rounded-full" />
|
||||||
|
社区动态
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5">
|
||||||
|
{/* 近期聚会 */}
|
||||||
|
<div className="community-card">
|
||||||
|
<div className="p-4 sm:p-5">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
||||||
|
🍹 近期聚会
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] text-gray-400 bg-gray-50 rounded-full px-2 py-0.5">
|
||||||
|
8场/月
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{meetups.map((m) => (
|
||||||
|
<div key={m.city} className="flex items-center justify-between py-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-base">{m.emoji}</span>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-800">{m.city}</p>
|
||||||
|
<p className="text-[11px] text-gray-400">{m.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] text-gray-400 bg-gray-50 rounded-full px-2 py-0.5">
|
||||||
|
{m.count}人
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/meetups"
|
||||||
|
className="block w-full text-center text-xs font-medium text-[#ff4d4f] hover:text-[#ff7a45] mt-4 pt-3 border-t border-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
查看全部聚会 →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 正在旅行 */}
|
||||||
|
<div className="community-card">
|
||||||
|
<div className="p-4 sm:p-5">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm flex items-center gap-1.5">
|
||||||
|
🛩️ 正在旅行
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] text-gray-400 bg-gray-50 rounded-full px-2 py-0.5">
|
||||||
|
{travelingNow.length}人
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{travelingNow.map((m) => (
|
||||||
|
<img
|
||||||
|
key={m.name}
|
||||||
|
src={m.photo}
|
||||||
|
alt={m.name}
|
||||||
|
title={`${m.name} · ${m.dest}`}
|
||||||
|
className="w-9 h-9 rounded-full shadow-sm cursor-pointer hover:scale-110 transition-transform object-cover"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 热门讨论 */}
|
||||||
|
<div className="community-card">
|
||||||
|
<div className="p-4 sm:p-5">
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm flex items-center gap-1.5 mb-4">
|
||||||
|
🔥 热门讨论
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{hotTopics.map((t, i) => (
|
||||||
|
<a key={t.title} href="#" className="flex items-start gap-2.5 group">
|
||||||
|
<span className={`text-xs font-bold mt-0.5 w-4 shrink-0 ${i < 3 ? "text-[#ff4d4f]" : "text-gray-300"}`}>
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm text-gray-700 group-hover:text-gray-900 transition-colors line-clamp-1 leading-snug">
|
||||||
|
{t.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-gray-400 mt-0.5">
|
||||||
|
{t.views.toLocaleString()} 次浏览
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button className="w-full text-center text-xs font-medium text-[#ff4d4f] hover:text-[#ff7a45] mt-4 pt-3 border-t border-gray-100 transition-colors">
|
||||||
|
查看更多话题 →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 全球旅行保险 */}
|
||||||
|
<div className="community-card bg-gradient-to-br from-emerald-50 to-teal-50">
|
||||||
|
<div className="p-4 sm:p-5">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<span className="text-2xl">🛡️</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm">旅行保险</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">数字游民定制方案</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-emerald-700 bg-emerald-100/50 rounded-lg px-3 py-2 mb-3">
|
||||||
|
覆盖全国 + 境外,远程工作专属条款,¥15/天起
|
||||||
|
</p>
|
||||||
|
<button className="w-full text-center text-sm font-semibold text-emerald-700 hover:text-emerald-800 bg-white/80 hover:bg-white border border-emerald-200 rounded-full py-2 transition-colors">
|
||||||
|
了解更多 →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 社区聊天 */}
|
||||||
|
<div className="community-card bg-gradient-to-br from-violet-50 to-purple-50">
|
||||||
|
<div className="p-4 sm:p-5">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<span className="text-2xl">💬</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm">社区聊天</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">本月 3,200+ 条消息</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<div className="flex -space-x-2">
|
||||||
|
{memberPhotos.slice(0, 6).map((m) => (
|
||||||
|
<img
|
||||||
|
key={m.name}
|
||||||
|
src={m.photo}
|
||||||
|
alt=""
|
||||||
|
className="w-7 h-7 rounded-full border-2 border-white shadow-sm object-cover"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-400">在线中</span>
|
||||||
|
</div>
|
||||||
|
<button className="w-full text-center text-sm font-semibold text-violet-700 hover:text-violet-800 bg-white/80 hover:bg-white border border-violet-200 rounded-full py-2 transition-colors">
|
||||||
|
加入聊天 →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 签证/落户 */}
|
||||||
|
<div className="community-card bg-gradient-to-br from-amber-50 to-orange-50">
|
||||||
|
<div className="p-4 sm:p-5">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<span className="text-2xl">🏛️</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-gray-900 text-sm">落地指南</h3>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
|
租房 · 共享办公 · 本地攻略
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||||
|
{["🏯 大理", "🐼 成都", "🌃 深圳", "⛲ 杭州"].map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="text-[11px] bg-amber-100/60 text-amber-700 rounded-full px-2 py-0.5"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button className="w-full text-center text-sm font-semibold text-amber-700 hover:text-amber-800 bg-white/80 hover:bg-white border border-amber-200 rounded-full py-2 transition-colors">
|
||||||
|
查看攻略 →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="border-t border-gray-200 bg-white">
|
||||||
|
<div className="max-w-[1400px] mx-auto px-5 sm:px-10 py-10 sm:py-12">
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 sm:gap-8 mb-8">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-gray-900 text-sm mb-3">社区</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-500">
|
||||||
|
<li><Link href="/join" className="hover:text-gray-700">加入社群</Link></li>
|
||||||
|
<li><Link href="/meetups" className="hover:text-gray-700">线下聚会</Link></li>
|
||||||
|
<li><Link href="/dating" className="hover:text-gray-700">交友约会</Link></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">成员地图</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-gray-900 text-sm mb-3">工具</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-500">
|
||||||
|
<li><a href="#" className="hover:text-gray-700">气候查询</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">生活成本计算</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">共享办公搜索</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">城市对比</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-gray-900 text-sm mb-3">资源</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-500">
|
||||||
|
<li><a href="#" className="hover:text-gray-700">远程工作指南</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">城市落地攻略</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">游民故事</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">常见问题</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-gray-900 text-sm mb-3">关于</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-gray-500">
|
||||||
|
<li><a href="#" className="hover:text-gray-700">关于我们</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">使用条款</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">隐私政策</a></li>
|
||||||
|
<li><a href="#" className="hover:text-gray-700">联系我们</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-8 border-t border-gray-200 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xl">🌏</span>
|
||||||
|
<span className="text-sm font-bold text-gray-900">游牧中国</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
© 2024-2026 游牧中国 NomadCNA. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-400">
|
||||||
|
<span>CNY ¥</span>
|
||||||
|
<span>°C</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
eslint.config.mjs
Normal file
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
1
homepage_content.html
Normal file
1
homepage_content.html
Normal file
File diff suppressed because one or more lines are too long
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "nextnomadcna",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "16.1.6",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.1.6",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
4037
pnpm-lock.yaml
generated
Normal file
4037
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ignoredBuiltDependencies:
|
||||||
|
- sharp
|
||||||
|
- unrs-resolver
|
||||||
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
public/file.svg
Normal file
1
public/file.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
1
public/globe.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/next.svg
Normal file
1
public/next.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
1
public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
1
public/window.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
34
tsconfig.json
Normal file
34
tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user