This commit is contained in:
eric
2026-03-10 04:41:20 -05:00
parent 82e79110fc
commit 0093294472
31 changed files with 5541 additions and 73 deletions

41
.gitignore vendored Normal file
View 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

106
README.md
View File

@@ -1,92 +1,52 @@
# nomadvip
# NomadVIP | 异度星球
VIP 会员解锁页,集成 PocketBase 支付校验、ZPAY 支付。
## 服务配置(参考 desktop digital
## Getting started
ZPAY、PocketBase、MinIO 配置与 `C:\project\digital` 保持一致,复制 `.env.example``.env.local` 后按需修改:
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `NEXT_PUBLIC_POCKETBASE_URL` | PocketBase 地址 | `https://pocketbase.hackrobot.cn` |
| `NEXT_PUBLIC_API_URL` / `PAYMENT_API_URL` | payjsapi 地址 | `https://api.hackrobot.cn` |
| `PAYMENT_PROVIDER` | 支付提供商 | `zpay` |
| `MINIO_ENDPOINT` | MinIO 端点 | `minioweb.hackrobot.cn` |
| `MINIO_BUCKET` | 存储桶 | `hackrobot` |
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)!
详见 `config/services.config.ts`
## Add your files
## Getting Started
- [ ] [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:
First, run the development server:
```
cd existing_repo
git remote add origin http://gitlab.yidooplanet.com/gitlab-instance-0a899031/nomadvip.git
git branch -M main
git push -uf origin main
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
## Integrate with your tools
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
- [ ] [Set up project integrations](http://gitlab.yidooplanet.com/gitlab-instance-0a899031/nomadvip/-/settings/integrations)
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## Collaborate with your team
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Learn More
## Test and Deploy
To learn more about Next.js, take a look at the following resources:
Use the built-in continuous integration in GitLab.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
***
## Deploy on Vercel
# Editing this README
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

227
app/api/pay/route.ts Normal file
View File

@@ -0,0 +1,227 @@
/**
* nomadvip 支付 API - 参考 digital
* PC: 二维码 | H5: 302 跳转 | 微信内: 收银台表单
*/
import { NextRequest, NextResponse } from "next/server";
import { getPaymentConfig, getApiUrlFromRequest } from "@/config/services.config";
const ZPAY_REQUIRED = ["pid", "type", "out_trade_no", "notify_url", "return_url", "name", "money", "sign", "sign_type"];
function escapeAttr(s: string): string {
return String(s)
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function buildPayHtml(payUrl: string, params: Record<string, unknown>): string {
const flat: Record<string, string> = {};
for (const [k, v] of Object.entries(params)) {
if (v == null) continue;
const s = String(v).trim();
if (s === "") continue;
flat[k] = s;
}
const inputs = Object.entries(flat)
.map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}" />`)
.join("\n");
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>跳转支付...</title>
<style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f0f4f8;}p{color:#64748b;}</style>
</head>
<body>
<p>正在跳转到支付页面...</p>
<form id="zpayForm" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">
${inputs}
</form>
<script>document.getElementById("zpayForm").submit();</script>
</body>
</html>`;
}
function setPayOrderCookie(res: NextResponse, orderId: string) {
res.cookies.set("nomadvip_pay_order", `${orderId}|${Date.now()}`, { path: "/", maxAge: 900 });
}
export async function POST(request: NextRequest) {
try {
let body: Record<string, string | number>;
const contentType = request.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
body = (await request.json()) as Record<string, string | number>;
} else {
const form = await request.formData();
body = Object.fromEntries(form.entries()) as Record<string, string>;
}
const payConfig = getPaymentConfig();
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
const envApiUrl = process.env.PAYMENT_API_URL || process.env.NEXT_PUBLIC_API_URL;
const apiUrl = (envApiUrl ? payConfig.apiUrl : getApiUrlFromRequest(hostHeader)).replace(/\/$/, "");
const user_id = String(body?.user_id || "").trim();
const return_url = String(body?.return_url || "").trim();
const total_fee = Number(body?.total_fee) || 18800;
const type = String(body?.type || "vip");
const channel = String(body?.channel || "alipay");
const device = String(body?.device || "pc");
if (!user_id) {
return NextResponse.json({ ok: false, error: "缺少 user_id" }, { status: 400 });
}
const origin =
request.headers.get("x-forwarded-host")
? `${request.headers.get("x-forwarded-proto") || "https"}://${request.headers.get("x-forwarded-host")}`
: request.headers.get("origin") || "http://localhost:3000";
const finalReturnUrl = return_url || `${origin}/paid`;
const wantHtml = String(body?.html) === "1";
if (!wantHtml) {
return NextResponse.json({ ok: false, error: "请传 html=1" }, { status: 400 });
}
const clientIp =
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
request.headers.get("x-real-ip") ||
"";
const redirectPayload = {
user_id,
total_fee,
type,
channel,
return_url: finalReturnUrl,
device,
...(clientIp && { client_ip: clientIp }),
};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
const redirectRes = await fetch(`${apiUrl}/nomadvip/payh5/redirect`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(redirectPayload),
redirect: "manual",
signal: controller.signal,
}).finally(() => clearTimeout(timeout));
if (redirectRes.status >= 301 && redirectRes.status <= 308) {
const location = redirectRes.headers.get("location");
if (location) {
const res = NextResponse.redirect(location);
const orderId = redirectRes.headers.get("x-pay-order-id")?.trim();
if (orderId) setPayOrderCookie(res, orderId);
return res;
}
}
const resData = await redirectRes.json().catch(() => ({}));
if (redirectRes.status === 200 && resData?.use_form) {
// 微信内:直接用 payh5 获取 params构建收银台表单不调用 redirect 避免重复创建订单)
const createRes = await fetch(`${apiUrl}/nomadvip/payh5`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id, total_fee, type, channel }),
});
const createData = await createRes.json().catch(() => ({}));
const params = createData?.params || {};
const orderId = String(params?.out_trade_no || "").trim();
if (!params || Object.keys(params).length === 0) {
return NextResponse.json({ ok: false, error: "支付参数为空" }, { status: 500 });
}
const html = buildPayHtml(payConfig.zpaySubmitUrl, params as Record<string, unknown>);
const res = new NextResponse(html, {
status: 200,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
if (orderId) setPayOrderCookie(res, orderId);
return res;
}
if (redirectRes.status === 200 && resData?.qrcode_mode && resData?.img) {
const returnUrl = String(resData.return_url || finalReturnUrl).replace(
/[&<>"']/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] || c
);
const imgSrc = String(resData.img).replace(
/[&<>"']/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] || c
);
const orderId = String(resData.order_id || "").replace(
/[&<>"']/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] || c
);
const ch = String(resData.channel || "alipay").toLowerCase();
const isWx = ch === "wxpay" || ch === "wx" || ch === "wechat";
const title = isWx ? "微信扫码支付" : "支付宝扫码支付";
const hint = isWx ? "请使用微信扫描下方二维码完成支付" : "请使用支付宝扫描下方二维码完成支付";
const esc = (s: string) =>
s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] || c);
const html = `<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)}</title>
<style>body{font-family:system-ui;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0f4f8;}h2{color:#333;margin-bottom:8px;}p{color:#64748b;font-size:14px;}img{width:220px;height:220px;margin:20px 0;border:1px solid #e2e8f0;border-radius:8px;background:#fff;}.tip{font-size:12px;color:#94a3b8;margin-top:8px;}</style></head>
<body>
<h2>${esc(title)}</h2>
<p>${esc(hint)}</p>
<img src="${imgSrc}" alt="支付二维码" />
<p class="tip" id="tip">支付成功后将自动跳转...</p>
<script>
(function(){
var orderId="${orderId}";
var returnUrl="${returnUrl}";
if(!orderId){return;}
var tip=document.getElementById("tip");
var t=0;
var maxT=300;
function check(){
t++;
if(t>maxT){tip.textContent="轮询超时";return;}
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId))
.then(function(r){return r.json();})
.then(function(d){
if(d.ok&&d.paid){window.location.href=returnUrl;return;}
setTimeout(check,2000);
})
.catch(function(){setTimeout(check,2000);});
}
setTimeout(check,2000);
})();
</script>
</body></html>`;
const res = new NextResponse(html, {
status: 200,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
if (orderId) setPayOrderCookie(res, orderId);
return res;
}
if (redirectRes.status === 400 || redirectRes.status === 500) {
return NextResponse.json(
{ ok: false, error: resData?.detail || resData?.msg || "支付跳转失败" },
{ status: redirectRes.status }
);
}
return NextResponse.json({ ok: false, error: "支付创建失败" }, { status: 500 });
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e));
const msg = err.message || "";
const hint =
msg.includes("fetch") || msg.includes("ECONNREFUSED")
? "请确认 payjsapi 已启动"
: err.name === "AbortError"
? "请求超时"
: msg;
console.error("Pay API error:", err);
return NextResponse.json({ ok: false, error: `支付请求失败: ${hint}` }, { status: 500 });
}
}

View File

@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getPaymentConfig, getApiUrlFromRequest } from "@/config/services.config";
export async function GET(request: NextRequest) {
const orderId = request.nextUrl.searchParams.get("order_id");
if (!orderId) {
return NextResponse.json({ ok: false, error: "缺少 order_id" }, { status: 400 });
}
const payConfig = getPaymentConfig();
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
const envApiUrl = process.env.PAYMENT_API_URL || process.env.NEXT_PUBLIC_API_URL;
const apiUrl = (envApiUrl ? payConfig.apiUrl : getApiUrlFromRequest(hostHeader)).replace(/\/$/, "");
try {
const res = await fetch(
`${apiUrl}/nomadvip/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`,
{ cache: "no-store" }
);
const data = await res.json().catch(() => ({}));
return NextResponse.json({ ok: true, paid: !!data?.paid });
} catch (e) {
return NextResponse.json({ ok: false, paid: false, error: String(e) }, { status: 500 });
}
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

28
app/globals.css Normal file
View File

@@ -0,0 +1,28 @@
@import "tailwindcss";
@import "github-markdown-css/github-markdown.css";
@import "animate.css";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

50
app/index-content.ts Normal file
View File

@@ -0,0 +1,50 @@
export const indexMd = `
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/image_20251231_015321_20586.jpg" alt="image_20251231_015321_20586.jpg" width="271">
🌟你将收获:
🚀 业务方向无人直播、RPA 自动化、web开发
⚙️ 技术杠杆云手机、PVE 虚拟化、服务器集群
🌐 海外平台YouTube、TikTok、SaaS 运营实战
<br/>
📖 解锁=>电子书+视频教程
1⃣[《数字游民》:地理套利 + 自动化杠杆](https://hackrobot.cn)
2⃣ [《云手机》:低成本工作室搭建指南](https://android.hackrobot.cn)
<img src="https://minioweb.hackrobot.cn/hackrobot/20260101205143_20260101_205202_1552.png" title="" alt="20260101205143_20260101_205202_1552.png" width="180">
📱设备自助下单(开箱即用)
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/wqdq_20260101_212749_8560.jpg" alt="wqdq_20260101_212749_8560.jpg" width="282">
📚 即时问答 (技术咨询)
<img src="https://minioweb.hackrobot.cn/hackrobot/20260101203904_20260101_204741_699.png" title="" alt="20260101203904_20260101_204741_699.png" width="284">
㊙️私密社群
<img src="https://minioweb.hackrobot.cn/hackrobot/20260101204456_20260101_204802_768.png" title="" alt="20260101204456_20260101_204802_768.png" width="186">
<br/>
🚫 内容不适合想躺赚、现金流紧张的人
💰 价格随参与人数阶梯式调整
(维护与答疑成本随规模增长)
📌 本内容为虚拟数字交付
不支持退款
👉 **限时特惠,3月12日恢复**
`;

231
app/index.module.css Normal file
View File

@@ -0,0 +1,231 @@
/* markdown-body from github-markdown-css */
.markVip {
padding-left: 50px;
padding-right: 50px;
}
.markVip img {
display: block;
margin: 10px auto;
max-width: 100%;
height: auto;
border-radius: 6px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.markVip p,
.markVip li,
.markVip h1,
.markVip h2,
.markVip h3 {
font-size: 15px;
}
.index {
height: 100vh;
}
.indexBook {
min-height: 150px;
display: flex;
flex-direction: column-reverse;
align-items: center;
justify-content: space-between;
}
.indexBookLeft {
min-width: 300px;
min-height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
}
.leftTitle {
font-size: 25px;
font-weight: 700;
color: #69b1ff;
margin-top: 5px;
}
.card {
margin-top: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.cardItem {
padding: 10px;
width: 250px;
margin-bottom: 10px;
display: flex;
flex-direction: row;
background-color: #f0f5ff;
border-radius: 10px;
}
.cardItemLeft {
display: flex;
flex-direction: column;
margin-right: 20px;
}
.cardItemLeftImg {
width: 40px;
height: 40px;
}
.cardItemRight {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.cardItemRightUp {
width: 100%;
height: 40px;
display: flex;
flex-direction: row;
font-weight: 700;
font-size: 17px;
}
.cardItemRightDown {
width: 100%;
flex: 1;
display: flex;
flex-direction: row;
font-size: 15px;
flex-wrap: wrap;
color: #262626;
}
.leftBtn {
margin-top: 15px;
margin-bottom: 15px;
}
.startRead {
width: 250px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: white;
background: linear-gradient(135deg, #69b1ff 0%, #4096ff 100%);
border: none;
border-radius: 9999px;
cursor: pointer;
}
.startRead:hover {
opacity: 0.9;
}
.indexBookImg {
margin-top: 15px;
width: 200px;
height: 200px;
}
.bookImg {
width: 100%;
height: auto;
}
.svgItems {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
margin-top: 20px;
}
/* H5 下隐藏「解锁vip」按钮下方模块无人直播、RPA自动化、聊天机器人 */
@media (max-width: 768px) {
.svgItems {
display: none;
}
}
.svgItem {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
}
.bookImgItem {
width: 350px;
height: 350px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.bookImgText {
margin-top: 20px;
font-size: 30px;
}
.vipcode {
display: flex;
flex-direction: column;
align-items: center;
font-size: 15px;
}
.vipcode button {
margin: 8px 0;
padding: 8px 16px;
cursor: pointer;
}
@media (min-width: 1536px) {
.index {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.indexBook {
width: 100%;
max-width: 1400px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #f0f5ff;
border-radius: 20px;
}
.indexBookLeft .card {
display: none;
}
.indexBookLeft .leftBtn .startRead {
width: 100px;
}
.indexBookImg .bookImg {
margin-bottom: 40px;
}
.svgItems {
width: 100%;
max-width: 1400px;
margin-top: 80px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}

34
app/layout.tsx Normal file
View File

@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
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="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

38
app/lib/payEnv.ts Normal file
View File

@@ -0,0 +1,38 @@
/**
* 支付环境检测PC / H5 / 微信浏览器
* 参考 digital app/lib/payEnv.ts
*/
export type PayEnv = "pc" | "h5" | "wechat";
export type PayChannel = "alipay" | "wxpay";
export function isWeChat(): boolean {
if (typeof navigator === "undefined") return false;
return /MicroMessenger/i.test(navigator.userAgent);
}
export function isMobile(): boolean {
if (typeof navigator === "undefined") return false;
return /AppleWebKit.*Mobile|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
}
export function getPayEnv(): PayEnv {
if (isWeChat()) return "wechat";
if (isMobile()) return "h5";
return "pc";
}
export function getRecommendedChannel(env: PayEnv): PayChannel {
return "wxpay";
}
export function mustUseWxPay(env: PayEnv): boolean {
return env === "wechat";
}
export function getDeviceFromEnv(env: PayEnv): "pc" | "h5" | "wechat" {
if (env === "wechat") return "wechat";
return env === "pc" ? "pc" : "h5";
}

View File

@@ -0,0 +1,75 @@
/**
* 微信 H5 支付完成后可能不跳转,通过 cookie 轮询检测支付状态
* 参考 digital usePayStatusPoll
*/
import { useEffect, useRef, useState } from "react";
const COOKIE_NAME = "nomadvip_pay_order";
const POLL_INTERVAL = 2000;
const MAX_POLLS = 150;
const MAX_AGE_MS = 15 * 60 * 1000;
function getCookie(name: string): string | null {
if (typeof document === "undefined") return null;
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
function clearCookie(name: string): void {
if (typeof document === "undefined") return;
document.cookie = `${name}=; path=/; max-age=0`;
}
function parsePayOrderCookie(raw: string | null): [string | null, boolean] {
if (!raw?.trim()) return [null, false];
const parts = raw.split("|");
const orderId = parts[0]?.trim() || raw.trim();
const ts = parts[1] ? parseInt(parts[1], 10) : NaN;
if (!orderId) return [null, false];
if (Number.isNaN(ts)) return [orderId, false];
if (Date.now() - ts > MAX_AGE_MS) return [orderId, false];
return [orderId, true];
}
export function usePayStatusPoll(onPaid: () => void): boolean {
const [polling, setPolling] = useState(false);
const onPaidRef = useRef(onPaid);
onPaidRef.current = onPaid;
useEffect(() => {
const raw = getCookie(COOKIE_NAME);
const [orderId, isValid] = parsePayOrderCookie(raw);
if (!orderId || !isValid) {
if (raw) clearCookie(COOKIE_NAME);
return;
}
setPolling(true);
let count = 0;
const timer = setInterval(async () => {
count++;
if (count > MAX_POLLS) {
clearInterval(timer);
clearCookie(COOKIE_NAME);
setPolling(false);
return;
}
try {
const res = await fetch(`/api/pay/status?order_id=${encodeURIComponent(orderId)}`);
const data = await res.json();
if (data?.ok && data?.paid) {
clearInterval(timer);
clearCookie(COOKIE_NAME);
setPolling(false);
onPaidRef.current();
}
} catch {
// 继续轮询
}
}, POLL_INTERVAL);
return () => clearInterval(timer);
}, []);
return polling;
}

310
app/page.tsx Normal file
View File

@@ -0,0 +1,310 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import Image from "next/image";
import { marked } from "marked";
import PocketBase from "pocketbase";
import { indexMd } from "./index-content";
import styles from "./index.module.css";
import {
getPayEnv,
getRecommendedChannel,
mustUseWxPay,
getDeviceFromEnv,
} from "@/app/lib/payEnv";
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
const PB_URL =
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
function redirectToPay(params: {
user_id: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
device: "pc" | "h5" | "wechat";
}) {
const form = document.createElement("form");
form.method = "POST";
form.action = "/api/pay";
form.target = "_self";
form.style.display = "none";
const fields: [string, string][] = [
["user_id", params.user_id],
["return_url", params.return_url],
["total_fee", String(params.total_fee)],
["type", params.type],
["channel", params.channel],
["device", params.device],
["html", "1"],
];
fields.forEach(([k, v]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = v;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
function getStorage(key: string): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
}
function setStorage(key: string, value: string): void {
if (typeof window === "undefined") return;
localStorage.setItem(key, value);
}
function removeStorage(key: string): void {
if (typeof window === "undefined") return;
localStorage.removeItem(key);
}
export default function Home() {
const [paidType, setPaidType] = useState<string | null>(null);
const [userName, setUserName] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const contentRef = useRef<HTMLDivElement>(null);
const savePaidType = useCallback((type: string | null) => {
if (type) setStorage("paidType", type);
else removeStorage("paidType");
setPaidType(type);
}, []);
const saveUserName = useCallback((name: string) => {
setStorage("userName", name);
setUserName(name);
}, []);
const getOrCreateUserId = useCallback(() => {
let userId = getStorage("userId");
if (!userId) {
userId = `user${Date.now()}`;
setStorage("userId", userId);
}
return userId;
}, []);
const checkPaymentFromPB = useCallback(async (userId: string) => {
try {
const pb = new PocketBase(PB_URL);
const records = await pb.collection("payments").getList(1, 1, {
filter: `user_id = "${userId}"`,
sort: "-created",
});
if (records.items.length > 0) {
const latestType = records.items[0].type as string;
const dbUserId = records.items[0].user_id as string;
savePaidType(latestType);
saveUserName(dbUserId);
return latestType;
} else {
savePaidType(null);
return null;
}
} catch (err) {
console.error("查询 PocketBase 失败:", err);
return getStorage("paidType");
}
}, [savePaidType, saveUserName]);
const clearAllStorage = useCallback(() => {
try {
if (typeof window !== "undefined" && window.localStorage) {
window.localStorage.clear();
}
removeStorage("userId");
removeStorage("paidType");
removeStorage("userName");
setPaidType(null);
setUserName(null);
setLoading(false);
alert("本地存储已清空");
// 移除 URL 中的 ?type=clean避免刷新时重复触发
if (typeof window !== "undefined" && window.history.replaceState) {
window.history.replaceState({}, "", window.location.pathname);
}
} catch (error) {
console.error("清空存储失败:", error);
alert("清空存储失败");
}
}, []);
const handlePay = useCallback(() => {
const userId = getOrCreateUserId();
const returnUrl = `${window.location.origin}/paid`;
const env = getPayEnv();
const channel = mustUseWxPay(env) ? "wxpay" : getRecommendedChannel(env);
const device = getDeviceFromEnv(env);
redirectToPay({
user_id: userId,
return_url: returnUrl,
total_fee: 18800,
type: "vip",
channel,
device,
});
}, [getOrCreateUserId]);
const handleCopyUsername = useCallback(() => {
const name = userName || "";
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(name);
alert("账号已复制");
}
}, [userName]);
// 微信 H5 支付完成后可能不跳转,轮询检测到已支付则刷新
usePayStatusPoll(() => {
savePaidType("vip");
saveUserName(getStorage("userId") || "");
});
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("type") === "clean") {
clearAllStorage();
return;
}
if (urlParams.get("paid") === "1") {
savePaidType("vip");
saveUserName(getStorage("userId") || "");
}
const init = async () => {
getOrCreateUserId();
const localType = getStorage("paidType");
const localUserName = getStorage("userName");
if (localType && localUserName) {
setPaidType(localType);
setUserName(localUserName);
setLoading(false);
return;
}
const userId = getStorage("userId");
if (userId) {
await checkPaymentFromPB(userId);
}
setLoading(false);
};
init();
}, [clearAllStorage, getOrCreateUserId, checkPaymentFromPB, savePaidType, saveUserName]);
marked.setOptions({ breaks: true, gfm: true });
return (
<div className={styles.index}>
<div className={`${styles.indexBook} animate__animated animate__pulse`}>
<div className={styles.indexBookLeft}>
<div className={styles.leftTitle}></div>
<div className={styles.card}>
<div className={styles.cardItem}>
<div className={styles.cardItemLeft}>
<Image
className={styles.cardItemLeftImg}
src="/images/group.svg"
alt="group"
width={40}
height={40}
/>
</div>
<div className={styles.cardItemRight}>
<div className={styles.cardItemRightUp}></div>
<div className={styles.cardItemRightDown}></div>
</div>
</div>
</div>
<div
ref={contentRef}
className={`markdown-body ${styles.markVip}`}
dangerouslySetInnerHTML={{ __html: marked(indexMd) as string }}
/>
<div className={styles.leftBtn}>
{paidType === "vip" ? (
<div className={styles.vipcode}>
<div
onClick={() => {
window.location.href = "https://qun.hackrobot.cn";
}}
style={{ cursor: "pointer" }}
>
👉星球地址: qun.hackrobot.cn
</div>
<div>:{getStorage("userName")}</div>
<button onClick={handleCopyUsername}></button>
<div>密码:123456</div>
<div>,&quot;&quot;,</div>
</div>
) : (
<button
onClick={() => {
if (paidType) return;
handlePay();
}}
className={styles.startRead}
type="button"
>
🔒 vip会员
</button>
)}
</div>
</div>
<div className={styles.indexBookImg}>
<Image
className={styles.bookImg}
src="/images/touxiang.png"
alt="avatar"
width={200}
height={200}
/>
</div>
</div>
<div className={styles.svgItems}>
<div className={styles.svgItem}>
<div className={styles.bookImgItem}>
<Image
src="/images/laptop.svg"
alt="laptop"
width={80}
height={80}
/>
<div className={styles.bookImgText}></div>
</div>
<div className={styles.bookImgItem}>
<Image
src="/images/travel.svg"
alt="travel"
width={80}
height={80}
/>
<div className={styles.bookImgText}>RPA自动化</div>
</div>
<div className={styles.bookImgItem}>
<Image
src="/images/code.svg"
alt="code"
width={80}
height={80}
/>
<div className={styles.bookImgText}></div>
</div>
</div>
</div>
</div>
);
}

76
app/paid/page.tsx Normal file
View File

@@ -0,0 +1,76 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
export default function PaidPage() {
const [status, setStatus] = useState<"pending" | "success" | "fail">("pending");
const [fromUrl, setFromUrl] = useState<string | null>(null);
usePayStatusPoll(() => setStatus("success"));
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const paid = params.get("paid");
const fail = params.get("fail");
const from = params.get("from");
if (paid === "1") setStatus("success");
else if (fail === "1") setStatus("fail");
if (from) setFromUrl(decodeURIComponent(from));
}, []);
if (status === "pending") {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
<div className="mx-auto mb-5 flex h-16 w-16 animate-spin items-center justify-center rounded-full border-4 border-sky-200 border-t-sky-500" />
<h2 className="text-xl font-bold text-slate-800"></h2>
<p className="mt-3 text-sm text-slate-500">
<br />
</p>
</div>
</div>
);
}
if (status === "success") {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-emerald-50 text-5xl">
</div>
<h2 className="text-2xl font-bold text-slate-800"></h2>
<p className="mt-3 text-slate-500">VIP </p>
<Link
href="/"
className="mt-8 inline-flex items-center gap-2 rounded-full bg-sky-500 px-8 py-3 font-semibold text-white transition-colors hover:bg-sky-600"
>
</Link>
</div>
</div>
);
}
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-amber-50 text-5xl">
</div>
<h2 className="text-2xl font-bold text-slate-800"></h2>
<p className="mt-3 text-slate-500"></p>
<Link
href="/"
className="mt-8 inline-flex items-center gap-2 rounded-full border-2 border-slate-300 px-8 py-3 font-semibold text-slate-700 transition-colors hover:border-slate-400 hover:bg-slate-50"
>
</Link>
</div>
</div>
);
}

76
config/services.config.ts Normal file
View File

@@ -0,0 +1,76 @@
/**
* 服务配置 - 参考 desktop digital 项目
* PocketBase、支付ZPAY、MinIO 可通过环境变量覆盖
*/
const isDev = process.env.NODE_ENV === "development";
export interface PocketBaseConfig {
url: string;
}
export interface PaymentConfig {
apiUrl: string;
provider: "zpay" | "xorpay";
zpaySubmitUrl: string;
/** nomadvip 专用接口路径(固定 ZPAY不影响 digital/cnomadcna */
nomadvipPayPath: string;
}
export interface MinioConfig {
endPoint: string;
port: number;
useSSL: boolean;
bucket: string;
publicUrl: string;
}
export function getPocketBaseConfig(): PocketBaseConfig {
return {
url:
process.env.NEXT_PUBLIC_POCKETBASE_URL ||
process.env.POCKETBASE_URL ||
"https://pocketbase.hackrobot.cn",
};
}
/**
* 根据请求 Host 推断 payjsapi 地址(同主机:8700
* 访问 192.168.41.222:3000 时自动用 192.168.41.222:8700无需在 .env 写死 IP
*/
export function getApiUrlFromRequest(hostHeader: string | null): string {
if (!hostHeader) return isDev ? "http://127.0.0.1:8700" : "https://api.hackrobot.cn";
const hostname = hostHeader.split(":")[0];
return isDev ? `http://${hostname}:8700` : "https://api.hackrobot.cn";
}
export function getPaymentConfig(): PaymentConfig {
const apiUrl =
process.env.PAYMENT_API_URL ||
process.env.NEXT_PUBLIC_API_URL ||
(isDev ? "http://127.0.0.1:8700" : "https://api.hackrobot.cn");
return {
apiUrl,
provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay",
zpaySubmitUrl:
process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php",
nomadvipPayPath: "/nomadvip/payh5",
};
}
export function getMinioConfig(): MinioConfig {
const endPoint = process.env.MINIO_ENDPOINT || "minioweb.hackrobot.cn";
const port = parseInt(process.env.MINIO_PORT || "9035", 10);
const useSSL = process.env.MINIO_USE_SSL === "true";
const bucket = process.env.MINIO_BUCKET || "hackrobot";
const publicUrl =
process.env.MINIO_PUBLIC_URL || `https://${endPoint}/${bucket}`;
return {
endPoint,
port,
useSSL,
bucket,
publicUrl: publicUrl.replace(/\/$/, ""),
};
}

18
eslint.config.mjs Normal file
View 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;

15
next.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// 局域网访问(如手机 192.168.x.x时允许跨域请求 _next 资源
// 需完全重启 dev 服务Ctrl+C 后 pnpm dev后生效
allowedDevOrigins: [
"192.168.41.222",
"192.168.41.222:3000",
"http://192.168.41.222",
"http://192.168.41.222:3000",
"http://192.168.41.222:3001",
],
};
export default nextConfig;

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "nomadvip",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -H 0.0.0.0",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"animate.css": "^4.1.1",
"github-markdown-css": "^5.9.0",
"marked": "^17.0.4",
"next": "16.1.6",
"pocketbase": "^0.26.8",
"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"
}
}

4072
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
ignoredBuiltDependencies:
- sharp
- unrs-resolver

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
public/file.svg Normal file
View 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
View 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

28
public/images/code.svg Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="browserbinary" viewBox="0 0 232 192" enable-background="new 0 0 232 192" xml:space="preserve" width="512" height="512">
<g>
<path fill="#D0E8FF" d="M128,100h16.0156c2.2383,0,3.9922-1.7441,3.9922-3.9688V67.9688c0-2.1875-1.7891-3.9688-3.9922-3.9688H128 c-2.2383,0-3.9922,1.7441-3.9922,3.9688v28.0625C124.0078,98.2188,125.7969,100,128,100z"/>
<path fill="#D0E8FF" d="M8,184h216V40H8V184z M184.0156,160c0,6.5977-5.3789,11.9668-11.9922,11.9668h-16.0156 c-6.6133,0-11.9922-5.3691-11.9922-11.9668v-28.0645c0-6.5996,5.3789-11.9688,11.9922-11.9688h16.0156 c6.6133,0,11.9922,5.3691,11.9922,11.9688V160z M176.0156,56c6.6133,0,11.9922,5.3691,11.9922,11.9688v36.0625 c0,2.209-1.7891,4-4,4s-4-1.791-4-4V67.9688c0-2.1875-1.7891-3.9688-3.9922-3.9688c-2.2109,0-4-1.791-4-4S173.8047,56,176.0156,56z M116.0078,67.9688C116.0078,61.3691,121.3867,56,128,56h16.0156c6.6133,0,11.9922,5.3691,11.9922,11.9688v28.0625 c0,6.5996-5.3789,11.9688-11.9922,11.9688H128c-6.6133,0-11.9922-5.3691-11.9922-11.9688V67.9688z M116.0156,119.9668 c6.6133,0,11.9922,5.3691,11.9922,11.9688V168c0,2.209-1.7891,4-4,4s-4-1.791-4-4v-36.0645c0-2.1875-1.7891-3.9688-3.9922-3.9688 c-2.2109,0-4-1.791-4-4S113.8047,119.9668,116.0156,119.9668z M88.0078,56C94.6211,56,100,61.3691,100,67.9688v36.0625 c0,2.209-1.7891,4-4,4s-4-1.791-4-4V67.9688C92,65.7812,90.2109,64,88.0078,64c-2.2109,0-4-1.791-4-4S85.7969,56,88.0078,56z M88.0078,119.9668c6.6133,0,11.9922,5.3691,11.9922,11.9688V168c0,2.209-1.7891,4-4,4s-4-1.791-4-4v-36.0645 c0-2.1875-1.7891-3.9688-3.9922-3.9688c-2.2109,0-4-1.791-4-4S85.7969,119.9668,88.0078,119.9668z M28,67.9688 C28,61.3691,33.3789,56,39.9922,56h16.0156C62.6211,56,68,61.3691,68,67.9688v28.0625C68,102.6309,62.6211,108,56.0078,108H39.9922 C33.3789,108,28,102.6309,28,96.0312V67.9688z M28,131.9355c0-6.5996,5.3789-11.9688,11.9922-11.9688h16.0156 c6.6133,0,11.9922,5.3691,11.9922,11.9688V160c0,6.5977-5.3789,11.9668-11.9922,11.9668H39.9922 C33.3789,171.9668,28,166.5977,28,160V131.9355z"/>
<path fill="#D0E8FF" d="M39.9922,100h16.0156C58.2461,100,60,98.2559,60,96.0312V67.9688C60,65.7812,58.2109,64,56.0078,64H39.9922 C37.7539,64,36,65.7441,36,67.9688v28.0625C36,98.2188,37.7891,100,39.9922,100z"/>
<path fill="#D0E8FF" d="M39.9922,163.9668h16.0156C58.2461,163.9668,60,162.2246,60,160v-28.0645 c0-2.1875-1.7891-3.9688-3.9922-3.9688H39.9922c-2.2383,0-3.9922,1.7441-3.9922,3.9688V160 C36,162.1875,37.7891,163.9668,39.9922,163.9668z"/>
<path fill="#5CB0FF" d="M8,8v24h216V8H8z M20,28c-4.4102,0-8-3.5879-8-8s3.5898-8,8-8s8,3.5879,8,8S24.4102,28,20,28z M40,28 c-4.4102,0-8-3.5879-8-8s3.5898-8,8-8s8,3.5879,8,8S44.4102,28,40,28z M60,28c-4.4102,0-8-3.5879-8-8s3.5898-8,8-8s8,3.5879,8,8 S64.4102,28,60,28z"/>
<path fill="#D0E8FF" d="M172.0234,127.9668h-16.0156c-2.2383,0-3.9922,1.7441-3.9922,3.9688V160 c0,2.1875,1.7891,3.9668,3.9922,3.9668h16.0156c2.2383,0,3.9922-1.7422,3.9922-3.9668v-28.0645 C176.0156,129.748,174.2266,127.9668,172.0234,127.9668z"/>
<path fill="#1C71DA" d="M0,0v32v8v152h232V40v-8V0H0z M224,184H8V40h216V184z M8,32V8h216v24H8z"/>
<path fill="#1C71DA" d="M20,12c-4.4102,0-8,3.5879-8,8s3.5898,8,8,8s8-3.5879,8-8S24.4102,12,20,12z"/>
<path fill="#1C71DA" d="M40,12c-4.4102,0-8,3.5879-8,8s3.5898,8,8,8s8-3.5879,8-8S44.4102,12,40,12z"/>
<path fill="#1C71DA" d="M60,12c-4.4102,0-8,3.5879-8,8s3.5898,8,8,8s8-3.5879,8-8S64.4102,12,60,12z"/>
<path fill="#1C71DA" d="M39.9922,108h16.0156C62.6211,108,68,102.6309,68,96.0312V67.9688C68,61.3691,62.6211,56,56.0078,56 H39.9922C33.3789,56,28,61.3691,28,67.9688v28.0625C28,102.6309,33.3789,108,39.9922,108z M36,67.9688 C36,65.7441,37.7539,64,39.9922,64h16.0156C58.2109,64,60,65.7812,60,67.9688v28.0625C60,98.2559,58.2461,100,56.0078,100H39.9922 C37.7891,100,36,98.2188,36,96.0312V67.9688z"/>
<path fill="#1C71DA" d="M128,108h16.0156c6.6133,0,11.9922-5.3691,11.9922-11.9688V67.9688 c0-6.5996-5.3789-11.9688-11.9922-11.9688H128c-6.6133,0-11.9922,5.3691-11.9922,11.9688v28.0625 C116.0078,102.6309,121.3867,108,128,108z M124.0078,67.9688C124.0078,65.7441,125.7617,64,128,64h16.0156 c2.2031,0,3.9922,1.7812,3.9922,3.9688v28.0625c0,2.2246-1.7539,3.9688-3.9922,3.9688H128c-2.2031,0-3.9922-1.7812-3.9922-3.9688 V67.9688z"/>
<path fill="#1C71DA" d="M172.0234,119.9668h-16.0156c-6.6133,0-11.9922,5.3691-11.9922,11.9688V160 c0,6.5977,5.3789,11.9668,11.9922,11.9668h16.0156c6.6133,0,11.9922-5.3691,11.9922-11.9668v-28.0645 C184.0156,125.3359,178.6367,119.9668,172.0234,119.9668z M176.0156,160c0,2.2246-1.7539,3.9668-3.9922,3.9668h-16.0156 c-2.2031,0-3.9922-1.7793-3.9922-3.9668v-28.0645c0-2.2246,1.7539-3.9688,3.9922-3.9688h16.0156 c2.2031,0,3.9922,1.7812,3.9922,3.9688V160z"/>
<path fill="#1C71DA" d="M39.9922,171.9668h16.0156C62.6211,171.9668,68,166.5977,68,160v-28.0645 c0-6.5996-5.3789-11.9688-11.9922-11.9688H39.9922c-6.6133,0-11.9922,5.3691-11.9922,11.9688V160 C28,166.5977,33.3789,171.9668,39.9922,171.9668z M36,131.9355c0-2.2246,1.7539-3.9688,3.9922-3.9688h16.0156 c2.2031,0,3.9922,1.7812,3.9922,3.9688V160c0,2.2246-1.7539,3.9668-3.9922,3.9668H39.9922C37.7891,163.9668,36,162.1875,36,160 V131.9355z"/>
<path fill="#1C71DA" d="M88.0078,64C90.2109,64,92,65.7812,92,67.9688v36.0625c0,2.209,1.7891,4,4,4s4-1.791,4-4V67.9688 C100,61.3691,94.6211,56,88.0078,56c-2.2109,0-4,1.791-4,4S85.7969,64,88.0078,64z"/>
<path fill="#1C71DA" d="M116.0156,127.9668c2.2031,0,3.9922,1.7812,3.9922,3.9688V168c0,2.209,1.7891,4,4,4s4-1.791,4-4v-36.0645 c0-6.5996-5.3789-11.9688-11.9922-11.9688c-2.2109,0-4,1.791-4,4S113.8047,127.9668,116.0156,127.9668z"/>
<path fill="#1C71DA" d="M88.0078,127.9668c2.2031,0,3.9922,1.7812,3.9922,3.9688V168c0,2.209,1.7891,4,4,4s4-1.791,4-4v-36.0645 c0-6.5996-5.3789-11.9688-11.9922-11.9688c-2.2109,0-4,1.791-4,4S85.7969,127.9668,88.0078,127.9668z"/>
<path fill="#1C71DA" d="M176.0156,64c2.2031,0,3.9922,1.7812,3.9922,3.9688v36.0625c0,2.209,1.7891,4,4,4s4-1.791,4-4V67.9688 c0-6.5996-5.3789-11.9688-11.9922-11.9688c-2.2109,0-4,1.791-4,4S173.8047,64,176.0156,64z"/>
</g>
<path fill="#FF5D5D" d="M196.252,167.8237c-1.0234,0-2.0469-0.3906-2.8281-1.1714c-1.5625-1.5625-1.5625-4.0952,0-5.6572 l14.1426-14.1421c1.5605-1.5615,4.0938-1.5615,5.6562,0c1.5625,1.5625,1.5625,4.0952,0,5.6572l-14.1426,14.1421 C198.2998,167.4331,197.2754,167.8237,196.252,167.8237z"/>
<path fill="#FF5D5D" d="M210.3945,167.8223c-1.0234,0-2.0479-0.3906-2.8281-1.1714l-14.1426-14.1426 c-1.5625-1.5625-1.5625-4.0947,0-5.6572c1.5605-1.5615,4.0957-1.5615,5.6562,0l14.1426,14.1426 c1.5625,1.5625,1.5625,4.0947,0,5.6572C212.4424,167.4316,211.418,167.8223,210.3945,167.8223z"/>
<path fill="#00D40B" d="M206.252,87.8237c-7.7197,0-14-6.2803-14-14s6.2803-14,14-14s14,6.2803,14,14 S213.9717,87.8237,206.252,87.8237z M206.252,67.8237c-3.3086,0-6,2.6914-6,6s2.6914,6,6,6s6-2.6914,6-6 S209.5605,67.8237,206.252,67.8237z"/>
<path fill="#FFC504" d="M107.5664,50.4492c-1.0234,0-2.0469-0.3906-2.8281-1.1714L93.4238,37.9644 c-0.75-0.75-1.1719-1.7676-1.1719-2.8286s0.4219-2.0786,1.1719-2.8286l11.3145-11.3135c1.5625-1.5615,4.0938-1.5615,5.6562,0 l11.3135,11.3135c1.5625,1.5625,1.5625,4.0947,0,5.6572l-11.3135,11.3135C109.6143,50.0586,108.5898,50.4492,107.5664,50.4492z M101.9092,35.1357l5.6572,5.6567l5.6562-5.6567l-5.6562-5.6567L101.9092,35.1357z"/>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

20
public/images/group.svg Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" ?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" id="Layer_1" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style type="text/css">
.st0{fill:#E6E6E6;}
.st1{fill:#69C9C9;}
.st2{fill:#4D5152;}
</style>
<g>
<path class="st0" d="M56.3,322.3v-10h-26c3.5-49.6,32.8-94,77.1-116.7c16.2,17.2,39.3,28,64.8,28c2.3,0,4.6-0.1,6.9-0.3 c2,16.9,7.7,32.7,16.4,46.5c-25.3,13.1-47.3,31.1-64.9,52.4L56.3,322.3z"/>
</g>
<g>
<path class="st1" d="M262.1,446.4H102.4c3.6-68.3,43.9-129.7,105.3-160.2c20.8,23,50.9,37.5,84.4,37.5c33.4,0,63.5-14.5,84.4-37.5 c61.4,30.5,101.7,91.9,105.3,160.2H322.1v10h-60V446.4z"/>

After

Width:  |  Height:  |  Size: 1.8 KiB

64
public/images/laptop.svg Normal file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<path style="fill:#F6F6F7;" d="M461.228,418.472H50.772V153.386c0-9.446,7.656-17.102,17.102-17.102h376.251
c9.446,0,17.102,7.656,17.102,17.102V418.472z"/>
<g>
<path style="fill:#68727E;" d="M461.228,153.386c0-9.445-7.658-17.102-17.102-17.102H67.875c-9.446,0-17.102,7.658-17.102,17.102
v17.102h410.455V153.386z"/>
<path style="fill:#68727E;" d="M503.983,409.921H8.017v8.551c0,18.891,15.314,34.205,34.205,34.205h427.557
c18.891,0,34.205-15.314,34.205-34.205V409.921z"/>
</g>
<path style="fill:#424F5E;" d="M13.248,436.643c6.052,9.629,16.762,16.033,28.973,16.033h427.557
c12.212,0,22.922-6.405,28.973-16.033H13.248z"/>
<rect x="332.96" y="256" style="fill:#FFC44F;" width="171.023" height="119.716"/>
<path style="fill:#F7AB49;" d="M332.96,281.653l79.843,79.843c3.131,3.131,8.207,3.131,11.338,0l79.842-79.843V256H332.96V281.653z"
/>
<polygon style="fill:#FFC44F;" points="332.96,256 418.472,341.511 503.983,256 "/>
<circle style="fill:#C4DF64;" cx="256" cy="341.511" r="17.102"/>
<path style="fill:#FF8C78;" d="M196.142,136.284c0-42.505-42.113-76.96-94.063-76.96S8.017,93.779,8.017,136.284
c0,42.503,42.113,76.96,94.063,76.96c5.451,0,10.793-0.384,15.99-1.113l52.42,35.317l-17.102-46.656
C179.125,187.061,196.142,163.299,196.142,136.284z"/>
<path style="fill:#DF7A6E;" d="M33.67,136.284c0-38.944,35.356-71.126,81.236-76.245c-4.194-0.468-8.475-0.715-12.827-0.715
c-51.949,0-94.063,34.456-94.063,76.96c0,42.503,42.113,76.96,94.063,76.96c4.339,0,8.605-0.253,12.787-0.719
C69.006,207.391,33.67,175.216,33.67,136.284z"/>
<rect x="50.772" y="392.818" style="fill:#D9DCDF;" width="410.455" height="17.102"/>
<path d="M503.983,383.733c4.428,0,8.017-3.589,8.017-8.017V256c0-4.348-3.67-8.017-8.017-8.017h-34.739v-94.597
c0-13.851-11.268-25.119-25.119-25.119H203.701c-4.869-43.111-48.582-76.96-101.622-76.96C45.792,51.307,0,89.428,0,136.284
c0,28.454,16.887,53.683,42.756,69.114v130.77c0,4.427,3.588,8.017,8.017,8.017s8.017-3.589,8.017-8.017V213.229
c13.151,5.148,27.822,8.032,43.29,8.032c4.671,0,9.371-0.267,14.009-0.797l49.921,33.634c3.09,2.082,7.321,1.719,10.016-0.852
c2.284-2.181,3.078-5.59,1.991-8.555l-14.782-40.325c11.378-7.07,20.685-15.901,27.536-25.888c0.186,0.013,0.372,0.029,0.562,0.029
h261.879v69.478H332.96c-4.364,0-8.017,3.661-8.017,8.017v119.716c0,4.427,3.588,8.017,8.017,8.017h120.251v18.171H58.789v-24.05
c0-4.427-3.588-8.017-8.017-8.017s-8.017,3.589-8.017,8.017v24.049H8.017c-4.428,0-8.017,3.589-8.017,8.017v8.551
c0,23.28,18.941,42.221,42.221,42.221h335.098c4.428,0,8.017-3.589,8.017-8.017c0-4.427-3.588-8.017-8.017-8.017H42.221
c-14.441,0-26.188-11.748-26.188-26.188v-0.534h479.933v0.534c0,14.44-11.747,26.188-26.188,26.188h-51.841
c-4.428,0-8.017,3.589-8.017,8.017c0,4.427,3.588,8.017,8.017,8.017h51.841c23.28,0,42.221-18.941,42.221-42.221v-8.551
c0-4.427-3.588-8.017-8.017-8.017h-34.739v-18.171H503.983z M149.613,193.719c-3.528,1.882-5.13,6.078-3.754,9.832l8.585,23.423
l-31.897-21.49c-3.017-2.032-6.342-1.133-9.697-0.796c-3.578,0.359-7.174,0.541-10.771,0.541c-47.446,0-86.046-30.928-86.046-68.944
s38.6-68.944,86.046-68.944s86.046,30.928,86.046,68.944C188.125,159.383,173.727,180.854,149.613,193.719z M199.236,162.472
c2.26-5.847,3.77-11.933,4.469-18.171h240.42c5.01,0,9.086,4.076,9.086,9.086v9.086H199.236z M484.63,264.017l-66.158,66.158
l-66.158-66.158H484.63z M340.977,275.354l71.825,71.825c3.089,3.088,8.249,3.088,11.338,0l71.826-71.825v92.345h-154.99V275.354z"
/>
<path d="M256,316.392c-13.851,0-25.119,11.268-25.119,25.119c0,13.851,11.268,25.119,25.119,25.119
c13.851,0,25.119-11.268,25.119-25.119C281.119,327.661,269.851,316.392,256,316.392z M256,350.597c-5.01,0-9.086-4.076-9.086-9.086
c0-5.01,4.076-9.086,9.086-9.086c5.01,0,9.086,4.076,9.086,9.086C265.086,346.521,261.01,350.597,256,350.597z"/>
<path d="M288.595,301.085c2.217-3.832,0.908-8.736-2.924-10.954c-8.982-5.196-19.241-7.943-29.67-7.943
c-10.427,0-20.686,2.746-29.667,7.941c-3.833,2.217-5.144,7.121-2.926,10.954c2.216,3.832,7.124,5.144,10.953,2.926
c6.545-3.786,14.028-5.787,21.64-5.787c7.613,0,15.096,2.002,21.641,5.788c1.264,0.732,2.644,1.08,4.007,1.08
C284.415,305.088,287.109,303.652,288.595,301.085z"/>
<path d="M305.692,271.452c2.217-3.832,0.908-8.736-2.926-10.953c-14.155-8.188-30.327-12.516-46.766-12.516
c-16.441,0-32.614,4.328-46.77,12.517c-3.832,2.217-5.142,7.121-2.924,10.954c1.487,2.569,4.178,4.004,6.947,4.004
c1.362,0,2.743-0.348,4.007-1.079c11.719-6.779,25.116-10.363,38.741-10.363c13.624,0,27.02,3.583,38.739,10.361
C298.572,276.595,303.476,275.285,305.692,271.452z"/>
<path d="M317.684,246.911c2.718,0,5.369-1.382,6.874-3.881c2.285-3.792,1.062-8.719-2.73-11.003
c-19.815-11.938-42.578-18.248-65.828-18.248c-22.454,0-44.541,5.911-63.873,17.094c-3.833,2.217-5.144,7.121-2.926,10.954
c2.217,3.833,7.122,5.142,10.953,2.926c16.896-9.774,36.207-14.94,55.845-14.94c20.334,0,40.236,5.514,57.555,15.948
C314.849,246.54,316.275,246.911,317.684,246.911z"/>
<path d="M106.355,128.267h-8.551c-4.428,0-8.017,3.589-8.017,8.017c0,4.427,3.588,8.017,8.017,8.017h8.551
c4.428,0,8.017-3.589,8.017-8.017C114.372,131.857,110.783,128.267,106.355,128.267z"/>
<path d="M72.15,128.267h-8.551c-4.428,0-8.017,3.589-8.017,8.017c0,4.427,3.588,8.017,8.017,8.017h8.551
c4.428,0,8.017-3.589,8.017-8.017C80.167,131.857,76.579,128.267,72.15,128.267z"/>
<path d="M140.56,128.267h-8.551c-4.428,0-8.017,3.589-8.017,8.017c0,4.427,3.588,8.017,8.017,8.017h8.551
c4.428,0,8.017-3.589,8.017-8.017C148.576,131.857,144.988,128.267,140.56,128.267z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
public/images/touxiang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

3
public/images/travel.svg Normal file
View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M885.76 921.28h-106.56l-0.32-26.77333333c0.10666667-0.74666667-0.10666667-68.26666667-7.46666667-171.52-8.96-125.65333333-21.76-243.41333333-167.57333333-334.82666667l24.53333333-26.02666667c78.72 46.93333333 146.24 112 178.88 167.36 42.34666667 72 56.85333333 110.29333333 69.54666667 176.85333334 21.65333333 112.64 9.49333333 211.30666667 8.96 214.93333333z" fill="#755E49" /><path d="M565.65333333 893.40444445c0 14.93333333-1.38666667 29.44-4.05333333 43.52H129.28c-2.66666667-14.08-4.05333333-28.69333333-4.05333333-43.52 0-25.81333333 4.26666667-50.66666667 11.94666666-73.81333334 29.76-88.64 111.78666667-152.32 208.21333334-152.32 96.42666667 0 178.45333333 63.68 208.21333333 152.32 7.89333333 23.14666667 12.05333333 48 12.05333333 73.81333334z" fill="#FF9D00" /><path d="M989.12 926.61333333c0 16.21333333-6.61333333 30.82666667-17.17333333 41.49333334-10.56 10.66666667-25.28 17.17333333-41.49333334 17.17333333h-838.4c-32.42666667 0-58.66666667-26.24-58.66666666-58.66666667 0-16.21333333 6.61333333-30.82666667 17.17333333-41.49333333 10.66666667-10.56 25.28-17.17333333 41.49333333-17.17333333h838.4c32.42666667 0 58.66666667 26.24 58.66666667 58.66666666z" fill="#00B7FF" /><path d="M981.86666667 399.57333333c10.45333333 0 14.50666667-17.17333333 5.86666666-24.42666666-31.14666667-26.24-91.73333333-64.21333333-181.01333333-64.74666667-89.92-0.64-150.29333333 38.50666667-180.90666667 65.28-8.53333333 7.46666667-4.37333333 24.32 5.97333334 24.32l350.08-0.42666667z" fill="#02C652" /><path d="M882.77333333 158.61333333c7.36-7.36-1.81333333-22.4-13.12-21.44-40.64 3.52-110.29333333 19.52-173.76 82.24-64 63.14666667-79.04 133.44-81.81333333 174.08-0.74666667 11.30666667 14.08 20.37333333 21.44 13.01333334l247.25333333-247.89333334z" fill="#02C652" /><path d="M648 391.89333333c7.36-7.36-1.81333333-22.4-13.12-21.44-40.64 3.52-110.29333333 19.52-173.76 82.24-64 63.14666667-79.04 133.44-81.81333333 174.08-0.74666667 11.30666667 14.08 20.37333333 21.44 13.01333334l247.25333333-247.89333334z" fill="#02C652" /><path d="M657.92 44.69333333c0-10.45333333-17.17333333-14.50666667-24.42666667-5.86666666-26.24 31.14666667-64.21333333 91.73333333-64.74666666 181.01333333-0.64 89.92 38.50666667 150.29333333 65.28 180.90666667 7.46666667 8.53333333 24.32 4.37333333 24.32-5.97333334l-0.42666667-350.08z" fill="#02C652" /><path d="M657.92 372.58666667c0-10.45333333-17.17333333-14.50666667-24.42666667-5.86666667-26.24 31.14666667-64.21333333 91.73333333-64.74666666 181.01333333-0.64 89.92 38.50666667 150.29333333 65.28 180.90666667 7.46666667 8.53333333 24.32 4.37333333 24.32-5.97333333l-0.42666667-350.08z" fill="#02C652" /><path d="M642.66666667 402.24c10.45333333 0 14.50666667-17.17333333 5.86666666-24.42666667-31.14666667-26.24-91.73333333-64.21333333-181.01333333-64.74666666-89.92-0.64-150.29333333 38.50666667-180.90666667 65.28-8.53333333 7.46666667-4.37333333 24.32 5.97333334 24.32l350.08-0.42666667z" fill="#02C652" /></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

1
public/next.svg Normal file
View 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
View 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
View 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
View 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"]
}