mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-06-02 07:43:34 +02:00
refactor: migrate application from next.js app to tanstack
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
name: Deploy to Cloudflare Worker
|
||||
name: Deploy to Cloudflare
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -24,9 +25,9 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm pages:build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Deploy to Cloudflare Worker
|
||||
- name: Deploy to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
+12
-4
@@ -3,14 +3,14 @@
|
||||
# ✨ Magic Resume ✨
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||

|
||||

|
||||

|
||||
|
||||
[简体中文](./README.md) | English
|
||||
|
||||
</div>
|
||||
|
||||
Magic Resume is a modern online resume editor that makes creating professional resumes simple and enjoyable. Built with Next.js and Framer Motion, it supports real-time preview and custom themes.
|
||||
Magic Resume is a modern online resume editor that makes creating professional resumes simple and enjoyable. Built with TanStack Start and Framer Motion, it supports real-time preview and custom themes.
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
@@ -18,7 +18,7 @@ Magic Resume is a modern online resume editor that makes creating professional r
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🚀 Built with Next.js 16+
|
||||
- 🚀 Built with TanStack Start
|
||||
- 💫 Smooth animations (Framer Motion)
|
||||
- 🎨 Custom theme support
|
||||
- 📱 Responsive design
|
||||
@@ -30,7 +30,9 @@ Magic Resume is a modern online resume editor that makes creating professional r
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- Next.js 16+
|
||||
- TanStack Start
|
||||
- TanStack Router
|
||||
- Vite
|
||||
- TypeScript
|
||||
- Motion
|
||||
- Tiptap
|
||||
@@ -68,6 +70,12 @@ pnpm dev
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## ☁️ Cloudflare Deploy
|
||||
|
||||
```bash
|
||||
pnpm run cf:deploy
|
||||
```
|
||||
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# ✨ Magic Resume ✨
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||

|
||||

|
||||

|
||||
|
||||
<a href="https://trendshift.io/repositories/13077" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13077" alt="Magic Resume | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
Magic Resume 是一个现代化的在线简历编辑器,让创建专业简历变得简单有趣。基于 Next.js 和 Motion 构建,支持实时预览和自定义主题。
|
||||
Magic Resume 是一个现代化的在线简历编辑器,让创建专业简历变得简单有趣。基于 TanStack Start 和 Motion 构建,支持实时预览和自定义主题。
|
||||
|
||||
## 📸 项目截图
|
||||
|
||||
@@ -21,7 +21,7 @@ Magic Resume 是一个现代化的在线简历编辑器,让创建专业简历
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 🚀 基于 Next.js 16+ 构建
|
||||
- 🚀 基于 TanStack Start 构建
|
||||
- 💫 流畅的动画效果 (Motion)
|
||||
- 🎨 自定义主题支持
|
||||
- 🌙 深色模式
|
||||
@@ -32,7 +32,9 @@ Magic Resume 是一个现代化的在线简历编辑器,让创建专业简历
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- Next.js 16+
|
||||
- TanStack Start
|
||||
- TanStack Router
|
||||
- Vite
|
||||
- TypeScript
|
||||
- Motion
|
||||
- Tiptap
|
||||
@@ -70,6 +72,12 @@ pnpm dev
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## ☁️ Cloudflare 部署
|
||||
|
||||
```bash
|
||||
pnpm run cf:deploy
|
||||
```
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
### Docker Compose
|
||||
|
||||
+5
-7
@@ -1,21 +1,19 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
|
||||
export default defineConfig([
|
||||
...nextVitals,
|
||||
{
|
||||
rules: {
|
||||
"react-hooks/immutability": "off",
|
||||
"react-hooks/preserve-manual-memoization": "off",
|
||||
"react-hooks/purity": "off",
|
||||
"react-hooks/set-state-in-effect": "off",
|
||||
},
|
||||
"react-hooks/set-state-in-effect": "off"
|
||||
}
|
||||
},
|
||||
globalIgnores([
|
||||
".next/**",
|
||||
".open-next/**",
|
||||
".wrangler/**",
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
"node_modules/**"
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
...(process.env.NEXT_STANDALONE === "1" && { output: "standalone" }),
|
||||
};
|
||||
|
||||
export default withNextIntl(config);
|
||||
@@ -1,3 +0,0 @@
|
||||
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config";
|
||||
|
||||
export default defineCloudflareConfig({});
|
||||
+13
-10
@@ -4,15 +4,16 @@
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.3.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"dev": "vite dev --port 3000",
|
||||
"build": "vite build",
|
||||
"start": "vite preview",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"pages:build": "node scripts/opennext-build.mjs",
|
||||
"preview": "pnpm pages:build && wrangler dev",
|
||||
"deploy": "pnpm pages:build && opennextjs-cloudflare deploy"
|
||||
"cf:deploy": "pnpm build && wrangler deploy",
|
||||
"cf:dev": "wrangler dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.13.8",
|
||||
"@heroui/checkbox": "^2.3.30",
|
||||
"@heroui/date-input": "^2.3.30",
|
||||
"@heroui/react": "^2.8.8",
|
||||
@@ -35,6 +36,8 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@sparticuz/chromium": "^131.0.0",
|
||||
"@tanstack/react-router": "^1.159.14",
|
||||
"@tanstack/react-start": "^1.159.14",
|
||||
"@tiptap/core": "^2.4.0",
|
||||
"@tiptap/extension-bullet-list": "^2.10.2",
|
||||
"@tiptap/extension-color": "^2.4.0",
|
||||
@@ -59,8 +62,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.379.0",
|
||||
"mark.js": "^8.11.1",
|
||||
"next": "16.1.6",
|
||||
"next-intl": "^4.8.2",
|
||||
"next-themes": "^0.4.3",
|
||||
"puppeteer": "^23.9.0",
|
||||
"puppeteer-core": "^23.9.0",
|
||||
@@ -78,18 +79,20 @@
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opennextjs/cloudflare": "1.16.3",
|
||||
"@tanstack/router-plugin": "^1.159.14",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"postcss": "^8",
|
||||
"postcss-normalize": "^13.0.1",
|
||||
"sass": "^1.77.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5",
|
||||
"vite": "^7.1.7",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"wrangler": "^4.65.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1143
-5532
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /app/workbench
|
||||
Disallow: /app
|
||||
Disallow: /api
|
||||
|
||||
# Sitemap
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://magicv.art/zh</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://magicv.art/en</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -1,85 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = process.cwd();
|
||||
const proxyPath = path.join(root, "src/proxy.ts");
|
||||
const middlewarePath = path.join(root, "src/middleware.ts");
|
||||
const backupPath = path.join(root, "src/proxy.ts.__opennext_backup__");
|
||||
|
||||
const middlewareContent = [
|
||||
'import handler from "./intl-proxy";',
|
||||
"",
|
||||
"export default handler;",
|
||||
// 'export const runtime = "experimental-edge";',
|
||||
"export const config = {",
|
||||
' matcher: ["/", "/(zh|en)/:path*"],',
|
||||
"};",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
async function fileExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function run(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { stdio: "inherit" });
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed: ${command} ${args.join(" ")}`));
|
||||
}
|
||||
});
|
||||
child.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function restoreProxy() {
|
||||
if (await fileExists(middlewarePath)) {
|
||||
await fs.unlink(middlewarePath);
|
||||
}
|
||||
|
||||
if (await fileExists(backupPath)) {
|
||||
await fs.rename(backupPath, proxyPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!(await fileExists(proxyPath))) {
|
||||
throw new Error("Expected src/proxy.ts to exist before OpenNext build.");
|
||||
}
|
||||
|
||||
if (await fileExists(middlewarePath)) {
|
||||
throw new Error("Expected src/middleware.ts to not exist before OpenNext build.");
|
||||
}
|
||||
|
||||
if (await fileExists(backupPath)) {
|
||||
throw new Error("Found stale backup file: src/proxy.ts.__opennext_backup__");
|
||||
}
|
||||
|
||||
await fs.rename(proxyPath, backupPath);
|
||||
await fs.writeFile(middlewarePath, middlewareContent, "utf8");
|
||||
|
||||
try {
|
||||
await run("pnpm", ["opennextjs-cloudflare", "build"]);
|
||||
} finally {
|
||||
await restoreProxy();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(async (error) => {
|
||||
try {
|
||||
await restoreProxy();
|
||||
} catch (restoreError) {
|
||||
console.error("Failed to restore proxy file after OpenNext build.", restoreError);
|
||||
}
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function GoDashboardAction() {
|
||||
redirect("/app/dashboard");
|
||||
}
|
||||
export async function GoResumesAction() {
|
||||
redirect("/app/dashboard/resumes");
|
||||
}
|
||||
export async function GoTemplatesAction() {
|
||||
redirect("/app/dashboard/templates");
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import {
|
||||
getMessages,
|
||||
getTranslations,
|
||||
setRequestLocale
|
||||
} from "next-intl/server";
|
||||
import Document from "@/components/Document";
|
||||
import { locales } from "@/i18n/config";
|
||||
import { Providers } from "@/app/providers";
|
||||
|
||||
// export const runtime = "edge";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params
|
||||
}: Props): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "common" });
|
||||
const baseUrl = "https://magicv.art";
|
||||
|
||||
return {
|
||||
title: t("title") + " - " + t("subtitle"),
|
||||
description: t("description"),
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1
|
||||
}
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${baseUrl}/${locale}`
|
||||
},
|
||||
openGraph: {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
locale: locale,
|
||||
alternateLocale: locale === "en" ? ["zh"] : ["en"]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params
|
||||
}: Props) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
if (!locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<Document locale={locale}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Providers>{children}</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import FAQSection from "@/components/home/FAQSection";
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<div className="relative bg-gradient-to-b from-[#f8f9fb] to-white dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="relative bg-gradient-to-b from-[#f8f9fb] to-white dark:from-gray-900 ">
|
||||
<LandingHeader />
|
||||
<HeroSection />
|
||||
<FeaturesSection />
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { AIModelType } from "@/config/ai";
|
||||
import { AI_MODEL_CONFIGS } from "@/config/ai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { apiKey, model, content, modelType, apiEndpoint } = body;
|
||||
|
||||
const modelConfig = AI_MODEL_CONFIGS[modelType as AIModelType];
|
||||
if (!modelConfig) {
|
||||
throw new Error("Invalid model type");
|
||||
}
|
||||
|
||||
const response = await fetch(modelConfig.url(apiEndpoint), {
|
||||
method: "POST",
|
||||
headers: modelConfig.headers(apiKey),
|
||||
body: JSON.stringify({
|
||||
model: modelConfig.requiresModelId ? model : modelConfig.defaultModel,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `你是一个专业的简历优化助手。请帮助优化以下文本,使其更加专业和有吸引力。
|
||||
|
||||
优化原则:
|
||||
1. 使用更专业的词汇和表达方式
|
||||
2. 突出关键成就和技能
|
||||
3. 保持简洁清晰
|
||||
4. 使用主动语气
|
||||
5. 保持原有信息的完整性
|
||||
6. 保留我输入的格式
|
||||
|
||||
请直接返回优化后的文本,不要包含任何解释或其他内容。`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content,
|
||||
},
|
||||
],
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
if (!response.body) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes("[DONE]")) continue;
|
||||
if (!line.startsWith("data:")) continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(line.slice(5));
|
||||
const content = data.choices[0]?.delta?.content;
|
||||
if (content) {
|
||||
controller.enqueue(encoder.encode(content));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing JSON:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Stream reading error:", error);
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Polish error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to polish content" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
// resolve image proxy
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const imageUrl = searchParams.get("url");
|
||||
|
||||
if (!imageUrl) {
|
||||
console.error("缺少图片URL参数");
|
||||
return NextResponse.json({ error: "缺少图片URL参数" }, { status: 400 });
|
||||
}
|
||||
|
||||
let parsedUrl;
|
||||
try {
|
||||
parsedUrl = new URL(imageUrl);
|
||||
} catch (e) {
|
||||
console.error(`图片URL格式不正确: ${imageUrl}`);
|
||||
return NextResponse.json({ error: "图片URL格式不正确" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
||||
console.error(`不支持的URL协议: ${parsedUrl.protocol}`);
|
||||
return NextResponse.json(
|
||||
{ error: "只支持HTTP和HTTPS协议" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(imageUrl, {
|
||||
headers: {
|
||||
// 模拟浏览器请求
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
Accept:
|
||||
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
Referer: parsedUrl.origin,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(`获取图片失败: ${error.message || "未知错误"}`);
|
||||
return NextResponse.json(
|
||||
{ error: `获取图片失败: ${error.message || "未知错误"}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
`图片服务器返回错误: ${response.status} ${response.statusText}`
|
||||
);
|
||||
return NextResponse.json(
|
||||
{ error: `获取图片失败: ${response.status} ${response.statusText}` },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
let imageBuffer;
|
||||
try {
|
||||
imageBuffer = await response.arrayBuffer();
|
||||
console.log(`成功获取图片,大小: ${imageBuffer.byteLength} 字节`);
|
||||
} catch (error: any) {
|
||||
console.error(`读取图片内容失败: ${error.message || "未知错误"}`);
|
||||
return NextResponse.json(
|
||||
{ error: `读取图片内容失败: ${error.message || "未知错误"}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (imageBuffer.byteLength === 0) {
|
||||
console.error("图片内容为空");
|
||||
return NextResponse.json({ error: "图片内容为空" }, { status: 400 });
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || "image/jpeg";
|
||||
|
||||
return new NextResponse(imageBuffer, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control":
|
||||
"no-store, no-cache, must-revalidate, proxy-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
"Surrogate-Control": "no-store",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("图片代理未处理的错误:", error);
|
||||
return NextResponse.json(
|
||||
{ error: `处理图片请求时出错: ${error.message || "未知错误"}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TooltipTrigger
|
||||
} from "@/components/ui/tooltip";
|
||||
import Logo from "@/components/shared/Logo";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
|
||||
interface MenuItem {
|
||||
title: string;
|
||||
@@ -34,6 +34,7 @@ interface MenuItem {
|
||||
|
||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const t = useTranslations("dashboard");
|
||||
const locale = useLocale();
|
||||
const sidebarItems: MenuItem[] = [
|
||||
{
|
||||
title: t("sidebar.resumes"),
|
||||
@@ -98,7 +99,7 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
<Logo
|
||||
className="cursor-pointer hover:opacity-80 transition-opacity"
|
||||
size={48}
|
||||
onClick={() => router.push("/")}
|
||||
onClick={() => router.push(`/${locale}`)}
|
||||
/>
|
||||
{open && (
|
||||
<span className="font-bold text-lg tracking-tight">
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages, getTranslations } from "next-intl/server";
|
||||
import Document from "@/components/Document";
|
||||
import { Providers } from "@/app/providers";
|
||||
import Client from "./client";
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations({ namespace: "common" });
|
||||
return {
|
||||
title: t("title") + " - " + t("dashboard"),
|
||||
};
|
||||
}
|
||||
export default async function LocaleLayout({ children }: Props) {
|
||||
const locale = await getLocale();
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<Document locale={locale}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Providers>
|
||||
<Client>{children}</Client>
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
|
||||
export default function Dashboard() {
|
||||
redirect("/app/dashboard/resumes");
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { ReactNode } from "react";
|
||||
import { getLocale, getMessages, getTranslations } from "next-intl/server";
|
||||
import Document from "@/components/Document";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations({ namespace: "common" });
|
||||
return {
|
||||
title: t("title"),
|
||||
};
|
||||
}
|
||||
export default async function LocaleLayout({ children }: Props) {
|
||||
const locale = await getLocale();
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<Document locale={locale}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages, getTranslations } from "next-intl/server";
|
||||
import Document from "@/components/Document";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations({ namespace: "common" });
|
||||
return {
|
||||
title: t("title") + " - " + t("dashboard"),
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
googleBot: {
|
||||
index: false,
|
||||
follow: false,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({ children }: Props) {
|
||||
const locale = await getLocale();
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<Document
|
||||
locale={locale}
|
||||
bodyClassName="overflow-y-hidden w-full"
|
||||
>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Providers>{children}</Providers>
|
||||
<Toaster position="top-center" richColors />
|
||||
</NextIntlClientProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import "./font.css";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://magicv.art"),
|
||||
icons: {
|
||||
icon: "/icon.png",
|
||||
shortcut: "/icon.png",
|
||||
apple: "/icon.png"
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Props) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "Magic Resume",
|
||||
short_name: "Magic Resume",
|
||||
description: "A Progressive Web App built with Next.js",
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
background_color: "#ffffff",
|
||||
theme_color: "#000000",
|
||||
icons: [
|
||||
{
|
||||
src: "/icon.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MetadataRoute } from "next";
|
||||
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = "https://magicv.art/";
|
||||
|
||||
const routes = ["zh", "en"];
|
||||
|
||||
const sitemap: MetadataRoute.Sitemap = routes.map((route) => ({
|
||||
url: `${baseUrl}${route}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 1.0
|
||||
}));
|
||||
|
||||
return sitemap;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export async function revalidatePath() {
|
||||
return;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
type FontResult = {
|
||||
className: string;
|
||||
style: { fontFamily: string };
|
||||
};
|
||||
|
||||
function createFont(family: string): FontResult {
|
||||
return {
|
||||
className: "",
|
||||
style: {
|
||||
fontFamily: family
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function Inter() {
|
||||
return createFont("Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif");
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
type CookieValue = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export async function cookies() {
|
||||
return {
|
||||
get: (name: string): CookieValue | undefined => {
|
||||
if (typeof document === "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cookieItem = document.cookie
|
||||
.split(";")
|
||||
.map((item) => item.trim())
|
||||
.find((item) => item.startsWith(`${name}=`));
|
||||
|
||||
if (!cookieItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
value: decodeURIComponent(cookieItem.slice(name.length + 1))
|
||||
};
|
||||
},
|
||||
set: (name: string, value: string) => {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { CSSProperties, ImgHTMLAttributes } from "react";
|
||||
|
||||
export type StaticImageData = {
|
||||
src: string;
|
||||
width: number;
|
||||
height: number;
|
||||
blurDataURL?: string;
|
||||
};
|
||||
|
||||
type ImageSource = string | StaticImageData;
|
||||
|
||||
type NextImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, "src"> & {
|
||||
src: ImageSource;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
fill?: boolean;
|
||||
priority?: boolean;
|
||||
sizes?: string;
|
||||
quality?: number;
|
||||
placeholder?: "empty" | "blur";
|
||||
blurDataURL?: string;
|
||||
unoptimized?: boolean;
|
||||
loader?: (params: { src: string; width: number; quality?: number }) => string;
|
||||
};
|
||||
|
||||
function resolveSrc(src: ImageSource): string {
|
||||
return typeof src === "string" ? src : src.src;
|
||||
}
|
||||
|
||||
export default function Image({
|
||||
src,
|
||||
alt,
|
||||
width,
|
||||
height,
|
||||
fill,
|
||||
priority,
|
||||
quality,
|
||||
placeholder,
|
||||
blurDataURL,
|
||||
unoptimized,
|
||||
loader,
|
||||
style,
|
||||
...rest
|
||||
}: NextImageProps) {
|
||||
const resolvedSrc = resolveSrc(src);
|
||||
|
||||
const fillStyle: CSSProperties = fill
|
||||
? {
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
...style
|
||||
}
|
||||
: {
|
||||
...style
|
||||
};
|
||||
|
||||
const priorityAttributes = priority
|
||||
? ({ fetchpriority: "high" } as Record<string, string>)
|
||||
: {};
|
||||
|
||||
return (
|
||||
<img
|
||||
src={resolvedSrc}
|
||||
alt={alt}
|
||||
width={fill ? undefined : width}
|
||||
height={fill ? undefined : height}
|
||||
loading={priority ? "eager" : rest.loading}
|
||||
style={fillStyle}
|
||||
{...rest}
|
||||
{...priorityAttributes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { AnchorHTMLAttributes, ReactNode } from "react";
|
||||
import { Link as RouterLink } from "@tanstack/react-router";
|
||||
|
||||
type LinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function isExternal(href: string): boolean {
|
||||
return /^(https?:)?\/\//.test(href) || href.startsWith("mailto:") || href.startsWith("tel:");
|
||||
}
|
||||
|
||||
export default function Link({ href, children, ...props }: LinkProps) {
|
||||
if (isExternal(href)) {
|
||||
return (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RouterLink to={href} {...props}>
|
||||
{children}
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
useNavigate,
|
||||
useRouter as useTanStackRouter,
|
||||
useRouterState
|
||||
} from "@tanstack/react-router";
|
||||
|
||||
type UrlLike = string | URL;
|
||||
|
||||
function normalizeUrl(url: UrlLike): string {
|
||||
if (typeof url === "string") {
|
||||
return url;
|
||||
}
|
||||
return url.pathname + url.search + url.hash;
|
||||
}
|
||||
|
||||
export function useRouter() {
|
||||
const navigate = useNavigate();
|
||||
const router = useTanStackRouter();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
push: (href: UrlLike) => {
|
||||
void navigate({ to: normalizeUrl(href) });
|
||||
},
|
||||
replace: (href: UrlLike) => {
|
||||
void navigate({ to: normalizeUrl(href), replace: true });
|
||||
},
|
||||
back: () => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.history.back();
|
||||
}
|
||||
},
|
||||
forward: () => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.history.forward();
|
||||
}
|
||||
},
|
||||
refresh: () => {
|
||||
void router.invalidate();
|
||||
}
|
||||
}),
|
||||
[navigate, router]
|
||||
);
|
||||
}
|
||||
|
||||
export function usePathname() {
|
||||
return useRouterState({ select: (state) => state.location.pathname });
|
||||
}
|
||||
|
||||
export function redirect(href: UrlLike): never {
|
||||
const target = normalizeUrl(href);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.assign(target);
|
||||
}
|
||||
|
||||
throw new Error(`NEXT_REDIRECT:${target}`);
|
||||
}
|
||||
|
||||
export function notFound(): never {
|
||||
throw new Error("NEXT_NOT_FOUND");
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Inter } from "next/font/google";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
locale: string;
|
||||
bodyClassName?: string;
|
||||
};
|
||||
|
||||
export default function Document({ children, locale, bodyClassName }: Props) {
|
||||
return (
|
||||
<html className={inter.className} lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico?v=2" />
|
||||
</head>
|
||||
<body className={bodyClassName}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default function CTASection() {
|
||||
const t = useTranslations("home");
|
||||
|
||||
return (
|
||||
<section className="py-24 md:py-44 bg-secondary/30 relative overflow-hidden">
|
||||
<section className="py-24 md:py-44 relative overflow-hidden">
|
||||
{/* Decorative background elements */}
|
||||
<div className="absolute top-0 right-0 w-1/3 h-full bg-primary/5 blur-[120px] -z-10 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-1/3 h-full bg-primary/5 blur-[120px] -z-10 -translate-x-1/2" />
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
GoDashboardAction,
|
||||
GoTemplatesAction,
|
||||
GoResumesAction,
|
||||
} from "@/actions/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function GoDashboard({
|
||||
children,
|
||||
type = "dashboard",
|
||||
type = "dashboard"
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
type?: "dashboard" | "templates" | "resumes";
|
||||
}) {
|
||||
const actionMap = {
|
||||
dashboard: GoDashboardAction,
|
||||
resumes: GoResumesAction,
|
||||
templates: GoTemplatesAction,
|
||||
const router = useRouter();
|
||||
|
||||
const pathMap: Record<typeof type, string> = {
|
||||
dashboard: "/app/dashboard",
|
||||
resumes: "/app/dashboard/resumes",
|
||||
templates: "/app/dashboard/templates"
|
||||
};
|
||||
|
||||
return <form action={actionMap[type]}>{children}</form>;
|
||||
return (
|
||||
<div
|
||||
className="contents cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push(pathMap[type]);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const EditButton = (
|
||||
RefAttributes<HTMLButtonElement>
|
||||
) => {
|
||||
return (
|
||||
<Link href={"/dashboard"}>
|
||||
<Link href={"/app/dashboard"}>
|
||||
<Button {...props}>{props.children}</Button>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export function GitHubStars() {
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="relative z-10"
|
||||
className="relative z-10 flex"
|
||||
animate={isHovered ? { rotate: 180 } : { rotate: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Languages } from "lucide-react";
|
||||
@@ -10,14 +11,33 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { locales, localeNames } from "@/i18n/config";
|
||||
import { Link, usePathname } from "@/i18n/routing.public";
|
||||
import { usePathname } from "@/i18n/routing.public";
|
||||
import { persistLocale, withLocale } from "@/i18n/runtime";
|
||||
|
||||
export default function LanguageSwitch() {
|
||||
const locale = useLocale();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleSelectLocale = (nextLocale: string) => {
|
||||
if (nextLocale === locale) {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
persistLocale(nextLocale);
|
||||
setOpen(false);
|
||||
|
||||
const nextPath = withLocale(pathname, nextLocale);
|
||||
|
||||
window.setTimeout(() => {
|
||||
router.push(nextPath);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -33,15 +53,17 @@ export default function LanguageSwitch() {
|
||||
<DropdownMenuItem
|
||||
key={loc}
|
||||
className={locale === loc ? "bg-accent" : ""}
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
handleSelectLocale(loc);
|
||||
}}
|
||||
>
|
||||
<Link className="w-full" href={pathname} locale={loc}>
|
||||
<span className="flex items-center gap-2">
|
||||
{localeNames[loc]}
|
||||
{locale === loc && (
|
||||
<span className="text-xs text-muted-foreground">✓</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
<span className="flex w-full items-center gap-2">
|
||||
{localeNames[loc]}
|
||||
{locale === loc && (
|
||||
<span className="text-xs text-muted-foreground">✓</span>
|
||||
)}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use server";
|
||||
import { setUserLocale } from "@/i18n/db";
|
||||
import { revalidatePath } from "next/cache";
|
||||
export default async function updateLocale(locale: string) {
|
||||
setUserLocale(locale);
|
||||
revalidatePath("/");
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
type ReactNode
|
||||
} from "react";
|
||||
import { defaultLocale, type Locale } from "@/i18n/config";
|
||||
import {
|
||||
getMessagesByLocale,
|
||||
normalizeLocale,
|
||||
type LocaleMessages
|
||||
} from "@/i18n/messages";
|
||||
import { persistLocale } from "@/i18n/runtime";
|
||||
|
||||
type TranslationValues = Record<string, string | number | boolean | null | undefined>;
|
||||
|
||||
type TranslateFn = ((key: string, values?: TranslationValues) => string) & {
|
||||
raw: (key: string) => unknown;
|
||||
};
|
||||
|
||||
type I18nContextValue = {
|
||||
locale: Locale;
|
||||
messages: LocaleMessages;
|
||||
};
|
||||
|
||||
const I18nContext = createContext<I18nContextValue>({
|
||||
locale: defaultLocale,
|
||||
messages: getMessagesByLocale(defaultLocale)
|
||||
});
|
||||
|
||||
function getNestedValue(value: unknown, key: string): unknown {
|
||||
if (!key) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return key.split(".").reduce<unknown>((acc, segment) => {
|
||||
if (acc === null || typeof acc !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (acc as Record<string, unknown>)[segment];
|
||||
}, value);
|
||||
}
|
||||
|
||||
function interpolate(template: string, values?: TranslationValues): string {
|
||||
if (!values) {
|
||||
return template;
|
||||
}
|
||||
|
||||
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
||||
const value = values[key];
|
||||
return value === undefined || value === null ? "" : String(value);
|
||||
});
|
||||
}
|
||||
|
||||
function createTranslator(messages: LocaleMessages, namespace?: string): TranslateFn {
|
||||
const base = namespace ? getNestedValue(messages, namespace) : messages;
|
||||
|
||||
const translate = ((key: string, values?: TranslationValues) => {
|
||||
const value = getNestedValue(base, key);
|
||||
|
||||
if (typeof value === "string") {
|
||||
return interpolate(value, values);
|
||||
}
|
||||
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return key;
|
||||
}) as TranslateFn;
|
||||
|
||||
translate.raw = (key: string) => getNestedValue(base, key);
|
||||
|
||||
return translate;
|
||||
}
|
||||
|
||||
export function NextIntlClientProvider({
|
||||
children,
|
||||
locale,
|
||||
messages
|
||||
}: {
|
||||
children: ReactNode;
|
||||
locale?: string;
|
||||
messages?: LocaleMessages;
|
||||
}) {
|
||||
const normalizedLocale = normalizeLocale(locale);
|
||||
const resolvedMessages = messages ?? getMessagesByLocale(normalizedLocale);
|
||||
|
||||
useEffect(() => {
|
||||
persistLocale(normalizedLocale);
|
||||
}, [normalizedLocale]);
|
||||
|
||||
const value = useMemo<I18nContextValue>(
|
||||
() => ({
|
||||
locale: normalizedLocale,
|
||||
messages: resolvedMessages
|
||||
}),
|
||||
[normalizedLocale, resolvedMessages]
|
||||
);
|
||||
|
||||
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
||||
}
|
||||
|
||||
export function useLocale(): Locale {
|
||||
return useContext(I18nContext).locale;
|
||||
}
|
||||
|
||||
export function useTranslations(namespace?: string): TranslateFn {
|
||||
const { messages } = useContext(I18nContext);
|
||||
|
||||
return useMemo(() => createTranslator(messages, namespace), [messages, namespace]);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { defaultLocale } from "./config";
|
||||
|
||||
const COOKIE_NAME = "NEXT_LOCALE";
|
||||
|
||||
export async function getUserLocale() {
|
||||
const cookieStore = await cookies();
|
||||
return cookieStore.get(COOKIE_NAME)?.value || defaultLocale;
|
||||
}
|
||||
|
||||
export async function setUserLocale(locale: string) {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(COOKIE_NAME, locale);
|
||||
}
|
||||
@@ -62,7 +62,7 @@
|
||||
"content": " 全新的 AI 简历优化功能已上线"
|
||||
},
|
||||
"cta": {
|
||||
"title": "开启你的新职业篇章呀",
|
||||
"title": "开启你的新职业篇章",
|
||||
"description": "立即使用魔方简历,创建一份令人印象深刻的简历",
|
||||
"button": "免费开始使用"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Locale } from "@/i18n/config";
|
||||
import { defaultLocale, locales } from "@/i18n/config";
|
||||
import en from "@/i18n/locales/en.json";
|
||||
import zh from "@/i18n/locales/zh.json";
|
||||
|
||||
const localeMessages = {
|
||||
en,
|
||||
zh
|
||||
} as const;
|
||||
|
||||
export type LocaleMessages = (typeof localeMessages)[Locale];
|
||||
|
||||
export function isSupportedLocale(locale: string): locale is Locale {
|
||||
return locales.includes(locale as Locale);
|
||||
}
|
||||
|
||||
export function getMessagesByLocale(locale: string): LocaleMessages {
|
||||
if (isSupportedLocale(locale)) {
|
||||
return localeMessages[locale];
|
||||
}
|
||||
return localeMessages[defaultLocale];
|
||||
}
|
||||
|
||||
export function normalizeLocale(locale?: string | null): Locale {
|
||||
if (locale && isSupportedLocale(locale)) {
|
||||
return locale;
|
||||
}
|
||||
return defaultLocale;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { defaultLocale, locales } from "./config";
|
||||
import { getUserLocale } from "./db";
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
// Read from potential `[locale]` segment
|
||||
let locale = await requestLocale;
|
||||
|
||||
if (!locale) {
|
||||
// The user is logged in
|
||||
locale = await getUserLocale();
|
||||
}
|
||||
|
||||
// Ensure that the incoming locale is valid
|
||||
if (!locales.includes(locale as any)) {
|
||||
locale = defaultLocale;
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`./locales/${locale}.json`)).default,
|
||||
};
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createNavigation } from "next-intl/navigation";
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
import { defaultLocale, locales } from "./config";
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales,
|
||||
defaultLocale,
|
||||
});
|
||||
|
||||
export const { Link, usePathname } = createNavigation(routing);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Link as RouterLink, useRouterState } from "@tanstack/react-router";
|
||||
import { defaultLocale, locales } from "./config";
|
||||
import { useLocale } from "next-intl";
|
||||
import { withLocale } from "./runtime";
|
||||
|
||||
export const routing = {
|
||||
locales,
|
||||
defaultLocale
|
||||
};
|
||||
|
||||
export function usePathname() {
|
||||
return useRouterState({ select: (state) => state.location.pathname });
|
||||
}
|
||||
|
||||
export function Link({
|
||||
href,
|
||||
locale,
|
||||
...props
|
||||
}: {
|
||||
href: string;
|
||||
locale?: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const currentLocale = useLocale();
|
||||
const nextLocale = locale ?? currentLocale;
|
||||
|
||||
return <RouterLink to={withLocale(href, nextLocale)} {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { defaultLocale, type Locale } from "@/i18n/config";
|
||||
import { isSupportedLocale, normalizeLocale } from "@/i18n/messages";
|
||||
|
||||
export const LOCALE_COOKIE_NAME = "NEXT_LOCALE";
|
||||
export const LOCALE_STORAGE_KEY = "magic-resume-locale";
|
||||
|
||||
function safelyReadCookie(name: string): string | undefined {
|
||||
if (typeof document === "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cookieItem = document.cookie
|
||||
.split(";")
|
||||
.map((item) => item.trim())
|
||||
.find((item) => item.startsWith(`${name}=`));
|
||||
|
||||
if (!cookieItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return decodeURIComponent(cookieItem.slice(name.length + 1));
|
||||
}
|
||||
|
||||
function safelyReadStorage(name: string): string | undefined {
|
||||
if (typeof window === "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.localStorage.getItem(name) ?? undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function persistLocale(locale: string) {
|
||||
if (!isSupportedLocale(locale)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.cookie = `${LOCALE_COOKIE_NAME}=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, locale);
|
||||
} catch {
|
||||
// ignore storage errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocaleFromPath(pathname: string): Locale | undefined {
|
||||
const segment = pathname.split("/").filter(Boolean)[0];
|
||||
if (segment && isSupportedLocale(segment)) {
|
||||
return segment;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveLocale(pathname: string): Locale {
|
||||
return (
|
||||
getLocaleFromPath(pathname) ??
|
||||
normalizeLocale(safelyReadCookie(LOCALE_COOKIE_NAME)) ??
|
||||
normalizeLocale(safelyReadStorage(LOCALE_STORAGE_KEY)) ??
|
||||
defaultLocale
|
||||
);
|
||||
}
|
||||
|
||||
export function withLocale(pathname: string, locale: string): string {
|
||||
const normalizedLocale = normalizeLocale(locale);
|
||||
const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
||||
|
||||
if (normalizedPathname === "/") {
|
||||
return `/${normalizedLocale}`;
|
||||
}
|
||||
|
||||
const segments = normalizedPathname.split("/").filter(Boolean);
|
||||
|
||||
if (segments.length > 0 && isSupportedLocale(segments[0])) {
|
||||
segments[0] = normalizedLocale;
|
||||
return `/${segments.join("/")}`;
|
||||
}
|
||||
|
||||
// 保持应用内路由无 locale 前缀,切换语言仅更新偏好
|
||||
if (normalizedPathname.startsWith("/app") || normalizedPathname.startsWith("/api")) {
|
||||
return normalizedPathname;
|
||||
}
|
||||
|
||||
return `/${normalizedLocale}${normalizedPathname}`;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
import { routing } from "./i18n/routing.public";
|
||||
|
||||
const handler = createMiddleware(routing);
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useRouterState } from "@tanstack/react-router";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { resolveLocale } from "@/i18n/runtime";
|
||||
import { getMessagesByLocale } from "@/i18n/messages";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
export function RouteProviders({ children }: { children: React.ReactNode }) {
|
||||
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
||||
|
||||
const locale = useMemo(() => resolveLocale(pathname), [pathname]);
|
||||
const messages = useMemo(() => getMessagesByLocale(locale), [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale;
|
||||
}, [locale]);
|
||||
|
||||
return (
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<Providers>
|
||||
{children}
|
||||
<Toaster position="top-center" richColors />
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import handler from "./intl-proxy";
|
||||
|
||||
export default handler;
|
||||
|
||||
export const config = {
|
||||
matcher: ["/", "/(zh|en)/:path*"],
|
||||
};
|
||||
@@ -0,0 +1,359 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as AppRouteImport } from './routes/app'
|
||||
import { Route as LocaleRouteImport } from './routes/$locale'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AppIndexRouteImport } from './routes/app.index'
|
||||
import { Route as AppDashboardRouteImport } from './routes/app.dashboard'
|
||||
import { Route as ApiPolishRouteImport } from './routes/api.polish'
|
||||
import { Route as ApiGrammarRouteImport } from './routes/api.grammar'
|
||||
import { Route as AppDashboardIndexRouteImport } from './routes/app.dashboard.index'
|
||||
import { Route as AppWorkbenchIdRouteImport } from './routes/app.workbench.$id'
|
||||
import { Route as AppDashboardTemplatesRouteImport } from './routes/app.dashboard.templates'
|
||||
import { Route as AppDashboardSettingsRouteImport } from './routes/app.dashboard.settings'
|
||||
import { Route as AppDashboardResumesRouteImport } from './routes/app.dashboard.resumes'
|
||||
import { Route as AppDashboardAiRouteImport } from './routes/app.dashboard.ai'
|
||||
import { Route as ApiProxyImageRouteImport } from './routes/api.proxy.image'
|
||||
|
||||
const AppRoute = AppRouteImport.update({
|
||||
id: '/app',
|
||||
path: '/app',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const LocaleRoute = LocaleRouteImport.update({
|
||||
id: '/$locale',
|
||||
path: '/$locale',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AppIndexRoute = AppIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppDashboardRoute = AppDashboardRouteImport.update({
|
||||
id: '/dashboard',
|
||||
path: '/dashboard',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const ApiPolishRoute = ApiPolishRouteImport.update({
|
||||
id: '/api/polish',
|
||||
path: '/api/polish',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ApiGrammarRoute = ApiGrammarRouteImport.update({
|
||||
id: '/api/grammar',
|
||||
path: '/api/grammar',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AppDashboardIndexRoute = AppDashboardIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AppDashboardRoute,
|
||||
} as any)
|
||||
const AppWorkbenchIdRoute = AppWorkbenchIdRouteImport.update({
|
||||
id: '/workbench/$id',
|
||||
path: '/workbench/$id',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppDashboardTemplatesRoute = AppDashboardTemplatesRouteImport.update({
|
||||
id: '/templates',
|
||||
path: '/templates',
|
||||
getParentRoute: () => AppDashboardRoute,
|
||||
} as any)
|
||||
const AppDashboardSettingsRoute = AppDashboardSettingsRouteImport.update({
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
getParentRoute: () => AppDashboardRoute,
|
||||
} as any)
|
||||
const AppDashboardResumesRoute = AppDashboardResumesRouteImport.update({
|
||||
id: '/resumes',
|
||||
path: '/resumes',
|
||||
getParentRoute: () => AppDashboardRoute,
|
||||
} as any)
|
||||
const AppDashboardAiRoute = AppDashboardAiRouteImport.update({
|
||||
id: '/ai',
|
||||
path: '/ai',
|
||||
getParentRoute: () => AppDashboardRoute,
|
||||
} as any)
|
||||
const ApiProxyImageRoute = ApiProxyImageRouteImport.update({
|
||||
id: '/api/proxy/image',
|
||||
path: '/api/proxy/image',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/$locale': typeof LocaleRoute
|
||||
'/app': typeof AppRouteWithChildren
|
||||
'/api/grammar': typeof ApiGrammarRoute
|
||||
'/api/polish': typeof ApiPolishRoute
|
||||
'/app/dashboard': typeof AppDashboardRouteWithChildren
|
||||
'/app/': typeof AppIndexRoute
|
||||
'/api/proxy/image': typeof ApiProxyImageRoute
|
||||
'/app/dashboard/ai': typeof AppDashboardAiRoute
|
||||
'/app/dashboard/resumes': typeof AppDashboardResumesRoute
|
||||
'/app/dashboard/settings': typeof AppDashboardSettingsRoute
|
||||
'/app/dashboard/templates': typeof AppDashboardTemplatesRoute
|
||||
'/app/workbench/$id': typeof AppWorkbenchIdRoute
|
||||
'/app/dashboard/': typeof AppDashboardIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/$locale': typeof LocaleRoute
|
||||
'/api/grammar': typeof ApiGrammarRoute
|
||||
'/api/polish': typeof ApiPolishRoute
|
||||
'/app': typeof AppIndexRoute
|
||||
'/api/proxy/image': typeof ApiProxyImageRoute
|
||||
'/app/dashboard/ai': typeof AppDashboardAiRoute
|
||||
'/app/dashboard/resumes': typeof AppDashboardResumesRoute
|
||||
'/app/dashboard/settings': typeof AppDashboardSettingsRoute
|
||||
'/app/dashboard/templates': typeof AppDashboardTemplatesRoute
|
||||
'/app/workbench/$id': typeof AppWorkbenchIdRoute
|
||||
'/app/dashboard': typeof AppDashboardIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/$locale': typeof LocaleRoute
|
||||
'/app': typeof AppRouteWithChildren
|
||||
'/api/grammar': typeof ApiGrammarRoute
|
||||
'/api/polish': typeof ApiPolishRoute
|
||||
'/app/dashboard': typeof AppDashboardRouteWithChildren
|
||||
'/app/': typeof AppIndexRoute
|
||||
'/api/proxy/image': typeof ApiProxyImageRoute
|
||||
'/app/dashboard/ai': typeof AppDashboardAiRoute
|
||||
'/app/dashboard/resumes': typeof AppDashboardResumesRoute
|
||||
'/app/dashboard/settings': typeof AppDashboardSettingsRoute
|
||||
'/app/dashboard/templates': typeof AppDashboardTemplatesRoute
|
||||
'/app/workbench/$id': typeof AppWorkbenchIdRoute
|
||||
'/app/dashboard/': typeof AppDashboardIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/$locale'
|
||||
| '/app'
|
||||
| '/api/grammar'
|
||||
| '/api/polish'
|
||||
| '/app/dashboard'
|
||||
| '/app/'
|
||||
| '/api/proxy/image'
|
||||
| '/app/dashboard/ai'
|
||||
| '/app/dashboard/resumes'
|
||||
| '/app/dashboard/settings'
|
||||
| '/app/dashboard/templates'
|
||||
| '/app/workbench/$id'
|
||||
| '/app/dashboard/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/$locale'
|
||||
| '/api/grammar'
|
||||
| '/api/polish'
|
||||
| '/app'
|
||||
| '/api/proxy/image'
|
||||
| '/app/dashboard/ai'
|
||||
| '/app/dashboard/resumes'
|
||||
| '/app/dashboard/settings'
|
||||
| '/app/dashboard/templates'
|
||||
| '/app/workbench/$id'
|
||||
| '/app/dashboard'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/$locale'
|
||||
| '/app'
|
||||
| '/api/grammar'
|
||||
| '/api/polish'
|
||||
| '/app/dashboard'
|
||||
| '/app/'
|
||||
| '/api/proxy/image'
|
||||
| '/app/dashboard/ai'
|
||||
| '/app/dashboard/resumes'
|
||||
| '/app/dashboard/settings'
|
||||
| '/app/dashboard/templates'
|
||||
| '/app/workbench/$id'
|
||||
| '/app/dashboard/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
LocaleRoute: typeof LocaleRoute
|
||||
AppRoute: typeof AppRouteWithChildren
|
||||
ApiGrammarRoute: typeof ApiGrammarRoute
|
||||
ApiPolishRoute: typeof ApiPolishRoute
|
||||
ApiProxyImageRoute: typeof ApiProxyImageRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/app': {
|
||||
id: '/app'
|
||||
path: '/app'
|
||||
fullPath: '/app'
|
||||
preLoaderRoute: typeof AppRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/$locale': {
|
||||
id: '/$locale'
|
||||
path: '/$locale'
|
||||
fullPath: '/$locale'
|
||||
preLoaderRoute: typeof LocaleRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/app/': {
|
||||
id: '/app/'
|
||||
path: '/'
|
||||
fullPath: '/app/'
|
||||
preLoaderRoute: typeof AppIndexRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/app/dashboard': {
|
||||
id: '/app/dashboard'
|
||||
path: '/dashboard'
|
||||
fullPath: '/app/dashboard'
|
||||
preLoaderRoute: typeof AppDashboardRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/api/polish': {
|
||||
id: '/api/polish'
|
||||
path: '/api/polish'
|
||||
fullPath: '/api/polish'
|
||||
preLoaderRoute: typeof ApiPolishRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/api/grammar': {
|
||||
id: '/api/grammar'
|
||||
path: '/api/grammar'
|
||||
fullPath: '/api/grammar'
|
||||
preLoaderRoute: typeof ApiGrammarRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/app/dashboard/': {
|
||||
id: '/app/dashboard/'
|
||||
path: '/'
|
||||
fullPath: '/app/dashboard/'
|
||||
preLoaderRoute: typeof AppDashboardIndexRouteImport
|
||||
parentRoute: typeof AppDashboardRoute
|
||||
}
|
||||
'/app/workbench/$id': {
|
||||
id: '/app/workbench/$id'
|
||||
path: '/workbench/$id'
|
||||
fullPath: '/app/workbench/$id'
|
||||
preLoaderRoute: typeof AppWorkbenchIdRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/app/dashboard/templates': {
|
||||
id: '/app/dashboard/templates'
|
||||
path: '/templates'
|
||||
fullPath: '/app/dashboard/templates'
|
||||
preLoaderRoute: typeof AppDashboardTemplatesRouteImport
|
||||
parentRoute: typeof AppDashboardRoute
|
||||
}
|
||||
'/app/dashboard/settings': {
|
||||
id: '/app/dashboard/settings'
|
||||
path: '/settings'
|
||||
fullPath: '/app/dashboard/settings'
|
||||
preLoaderRoute: typeof AppDashboardSettingsRouteImport
|
||||
parentRoute: typeof AppDashboardRoute
|
||||
}
|
||||
'/app/dashboard/resumes': {
|
||||
id: '/app/dashboard/resumes'
|
||||
path: '/resumes'
|
||||
fullPath: '/app/dashboard/resumes'
|
||||
preLoaderRoute: typeof AppDashboardResumesRouteImport
|
||||
parentRoute: typeof AppDashboardRoute
|
||||
}
|
||||
'/app/dashboard/ai': {
|
||||
id: '/app/dashboard/ai'
|
||||
path: '/ai'
|
||||
fullPath: '/app/dashboard/ai'
|
||||
preLoaderRoute: typeof AppDashboardAiRouteImport
|
||||
parentRoute: typeof AppDashboardRoute
|
||||
}
|
||||
'/api/proxy/image': {
|
||||
id: '/api/proxy/image'
|
||||
path: '/api/proxy/image'
|
||||
fullPath: '/api/proxy/image'
|
||||
preLoaderRoute: typeof ApiProxyImageRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AppDashboardRouteChildren {
|
||||
AppDashboardAiRoute: typeof AppDashboardAiRoute
|
||||
AppDashboardResumesRoute: typeof AppDashboardResumesRoute
|
||||
AppDashboardSettingsRoute: typeof AppDashboardSettingsRoute
|
||||
AppDashboardTemplatesRoute: typeof AppDashboardTemplatesRoute
|
||||
AppDashboardIndexRoute: typeof AppDashboardIndexRoute
|
||||
}
|
||||
|
||||
const AppDashboardRouteChildren: AppDashboardRouteChildren = {
|
||||
AppDashboardAiRoute: AppDashboardAiRoute,
|
||||
AppDashboardResumesRoute: AppDashboardResumesRoute,
|
||||
AppDashboardSettingsRoute: AppDashboardSettingsRoute,
|
||||
AppDashboardTemplatesRoute: AppDashboardTemplatesRoute,
|
||||
AppDashboardIndexRoute: AppDashboardIndexRoute,
|
||||
}
|
||||
|
||||
const AppDashboardRouteWithChildren = AppDashboardRoute._addFileChildren(
|
||||
AppDashboardRouteChildren,
|
||||
)
|
||||
|
||||
interface AppRouteChildren {
|
||||
AppDashboardRoute: typeof AppDashboardRouteWithChildren
|
||||
AppIndexRoute: typeof AppIndexRoute
|
||||
AppWorkbenchIdRoute: typeof AppWorkbenchIdRoute
|
||||
}
|
||||
|
||||
const AppRouteChildren: AppRouteChildren = {
|
||||
AppDashboardRoute: AppDashboardRouteWithChildren,
|
||||
AppIndexRoute: AppIndexRoute,
|
||||
AppWorkbenchIdRoute: AppWorkbenchIdRoute,
|
||||
}
|
||||
|
||||
const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
LocaleRoute: LocaleRoute,
|
||||
AppRoute: AppRouteWithChildren,
|
||||
ApiGrammarRoute: ApiGrammarRoute,
|
||||
ApiPolishRoute: ApiPolishRoute,
|
||||
ApiProxyImageRoute: ApiProxyImageRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
export function getRouter() {
|
||||
const router = createTanStackRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
defaultPreload: "intent",
|
||||
defaultPreloadStaleTime: 0
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import LandingPage from "@/app/(public)/[locale]/page";
|
||||
import { defaultLocale } from "@/i18n/config";
|
||||
import { getMessagesByLocale, isSupportedLocale, normalizeLocale } from "@/i18n/messages";
|
||||
import { persistLocale } from "@/i18n/runtime";
|
||||
|
||||
export const Route = createFileRoute("/$locale")({
|
||||
beforeLoad: ({ params }) => {
|
||||
if (!isSupportedLocale(params.locale)) {
|
||||
throw redirect({
|
||||
to: "/$locale",
|
||||
params: {
|
||||
locale: defaultLocale
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
head: ({ params }) => {
|
||||
const locale = normalizeLocale(params.locale);
|
||||
const messages = getMessagesByLocale(locale);
|
||||
const common = messages.common;
|
||||
const baseUrl = "https://magicv.art";
|
||||
const canonical = `${baseUrl}/${locale}`;
|
||||
const alternateLocale = locale === "en" ? "zh" : "en";
|
||||
const ogLocale = locale === "en" ? "en_US" : "zh_CN";
|
||||
|
||||
return {
|
||||
meta: [
|
||||
{
|
||||
title: `${common.title} - ${common.subtitle}`
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
content: common.description
|
||||
},
|
||||
{
|
||||
name: "robots",
|
||||
content: "index,follow"
|
||||
},
|
||||
{
|
||||
property: "og:type",
|
||||
content: "website"
|
||||
},
|
||||
{
|
||||
property: "og:site_name",
|
||||
content: common.title
|
||||
},
|
||||
{
|
||||
property: "og:title",
|
||||
content: `${common.title} - ${common.subtitle}`
|
||||
},
|
||||
{
|
||||
property: "og:description",
|
||||
content: common.description
|
||||
},
|
||||
{
|
||||
property: "og:url",
|
||||
content: canonical
|
||||
},
|
||||
{
|
||||
property: "og:locale",
|
||||
content: ogLocale
|
||||
},
|
||||
{
|
||||
name: "twitter:card",
|
||||
content: "summary_large_image"
|
||||
},
|
||||
{
|
||||
name: "twitter:title",
|
||||
content: `${common.title} - ${common.subtitle}`
|
||||
},
|
||||
{
|
||||
name: "twitter:description",
|
||||
content: common.description
|
||||
}
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "canonical",
|
||||
href: canonical
|
||||
},
|
||||
{
|
||||
rel: "alternate",
|
||||
hrefLang: "zh",
|
||||
href: `${baseUrl}/zh`
|
||||
},
|
||||
{
|
||||
rel: "alternate",
|
||||
hrefLang: "en",
|
||||
href: `${baseUrl}/en`
|
||||
},
|
||||
{
|
||||
rel: "alternate",
|
||||
hrefLang: "x-default",
|
||||
href: `${baseUrl}/${defaultLocale}`
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
component: LocalePage
|
||||
});
|
||||
|
||||
function LocalePage() {
|
||||
const { locale } = Route.useParams();
|
||||
|
||||
useEffect(() => {
|
||||
persistLocale(locale);
|
||||
}, [locale]);
|
||||
|
||||
return <LandingPage />;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
ErrorComponent,
|
||||
HeadContent,
|
||||
Scripts,
|
||||
createRootRoute
|
||||
} from "@tanstack/react-router";
|
||||
import { RouteProviders } from "@/providers/RouteProviders";
|
||||
import appCss from "@/styles.css?url";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: "utf-8"
|
||||
},
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1"
|
||||
},
|
||||
{
|
||||
name: "theme-color",
|
||||
content: "#1B1B18"
|
||||
}
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: appCss
|
||||
},
|
||||
{
|
||||
rel: "icon",
|
||||
href: "/icon.png"
|
||||
}
|
||||
]
|
||||
}),
|
||||
errorComponent: ({ error }) => (
|
||||
<RootDocument>
|
||||
<ErrorComponent error={error} />
|
||||
</RootDocument>
|
||||
),
|
||||
shellComponent: RootDocument
|
||||
});
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="zh" suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<RouteProviders>{children}</RouteProviders>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,38 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { AIModelType } from "@/config/ai";
|
||||
import { AI_MODEL_CONFIGS } from "@/config/ai";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AIModelType, AI_MODEL_CONFIGS } from "@/config/ai";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { apiKey, model, content, modelType, apiEndpoint } = body;
|
||||
export const Route = createFileRoute("/api/grammar")({
|
||||
server: {
|
||||
handlers: {
|
||||
POST: async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { apiKey, model, content, modelType, apiEndpoint } = body as {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
content: string;
|
||||
modelType: AIModelType;
|
||||
apiEndpoint?: string;
|
||||
};
|
||||
|
||||
const modelConfig = AI_MODEL_CONFIGS[modelType as AIModelType];
|
||||
if (!modelConfig) {
|
||||
throw new Error("Invalid model type");
|
||||
}
|
||||
const modelConfig = AI_MODEL_CONFIGS[modelType as AIModelType];
|
||||
if (!modelConfig) {
|
||||
return Response.json({ error: "Invalid model type" }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await fetch(modelConfig.url(apiEndpoint), {
|
||||
method: "POST",
|
||||
headers: modelConfig.headers(apiKey),
|
||||
const response = await fetch(modelConfig.url(apiEndpoint), {
|
||||
method: "POST",
|
||||
headers: modelConfig.headers(apiKey),
|
||||
body: JSON.stringify({
|
||||
model: modelConfig.requiresModelId ? model : modelConfig.defaultModel,
|
||||
response_format: {
|
||||
type: "json_object"
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `你是一个专业的中文简历校对助手。你的任务是**仅**找出简历中的**错别字**和**标点符号错误。
|
||||
|
||||
body: JSON.stringify({
|
||||
model: modelConfig.requiresModelId ? model : modelConfig.defaultModel,
|
||||
response_format: {
|
||||
type: "json_object",
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `你是一个专业的中文简历校对助手。你的任务是**仅**找出简历中的**错别字**和**标点符号错误**。
|
||||
|
||||
**严格禁止**:
|
||||
1. ❌ **禁止**提供任何风格、语气、润色或改写建议。如果句子在语法上是正确的(即使读起来不够优美),也**绝对不要**报错。
|
||||
2. ❌ **禁止**报告“无明显错误”或类似的信息。如果没有发现错别字或标点错误,"errors" 数组必须为空。
|
||||
@@ -34,7 +41,7 @@ export async function POST(req: NextRequest) {
|
||||
**仅检查以下两类错误**:
|
||||
1. ✅ **错别字**:例如将“作为”写成“做为”,将“经理”写成“经里”。
|
||||
2. ✅ **严重标点错误**:仅报告重复标点(如“,,”)或完全错误的符号位置。
|
||||
|
||||
|
||||
**重要例外(绝不报错)**:
|
||||
- ❌ **忽略中英文标点混用**:在技术简历中,中文内容使用英文标点(如使用英文逗号, 代替中文逗号,或使用英文句点. 代替中文句号)是**完全接受**的风格。**绝对不要**报告此类“错误”。
|
||||
- ❌ **忽略空格使用**:不要报告中英文之间的空格遗漏或多余。
|
||||
@@ -51,24 +58,24 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
再次强调:**只找错别字和标点错误,不要做任何润色!**`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: content,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error("Error in grammar check:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to check grammar" },
|
||||
{ status: 500 }
|
||||
);
|
||||
再次强调:**只找错别字和标点错误,不要做任何润色!**`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return Response.json(data);
|
||||
} catch (error) {
|
||||
console.error("Error in grammar check:", error);
|
||||
return Response.json({ error: "Failed to check grammar" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AIModelType, AI_MODEL_CONFIGS } from "@/config/ai";
|
||||
|
||||
export const Route = createFileRoute("/api/polish")({
|
||||
server: {
|
||||
handlers: {
|
||||
POST: async ({ request }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { apiKey, model, content, modelType, apiEndpoint } = body as {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
content: string;
|
||||
modelType: AIModelType;
|
||||
apiEndpoint?: string;
|
||||
};
|
||||
|
||||
const modelConfig = AI_MODEL_CONFIGS[modelType as AIModelType];
|
||||
if (!modelConfig) {
|
||||
return Response.json({ error: "Invalid model type" }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await fetch(modelConfig.url(apiEndpoint), {
|
||||
method: "POST",
|
||||
headers: modelConfig.headers(apiKey),
|
||||
body: JSON.stringify({
|
||||
model: modelConfig.requiresModelId ? model : modelConfig.defaultModel,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `你是一个专业的简历优化助手。请帮助优化以下文本,使其更加专业和有吸引力。
|
||||
|
||||
优化原则:
|
||||
1. 使用更专业的词汇和表达方式
|
||||
2. 突出关键成就和技能
|
||||
3. 保持简洁清晰
|
||||
4. 使用主动语气
|
||||
5. 保持原有信息的完整性
|
||||
6. 保留我输入的格式
|
||||
|
||||
请直接返回优化后的文本,不要包含任何解释或其他内容。`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content
|
||||
}
|
||||
],
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
if (!response.body) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes("[DONE]")) continue;
|
||||
if (!line.startsWith("data:")) continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(line.slice(5));
|
||||
const contentDelta = data.choices[0]?.delta?.content;
|
||||
if (contentDelta) {
|
||||
controller.enqueue(encoder.encode(contentDelta));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing JSON:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Stream reading error:", error);
|
||||
controller.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Polish error:", error);
|
||||
return Response.json({ error: "Failed to polish content" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
function validateImageUrl(imageUrl: string | null) {
|
||||
if (!imageUrl) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 400,
|
||||
error: "缺少图片URL参数"
|
||||
} as const;
|
||||
}
|
||||
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
parsedUrl = new URL(imageUrl);
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
status: 400,
|
||||
error: "图片URL格式不正确"
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
||||
return {
|
||||
ok: false,
|
||||
status: 400,
|
||||
error: "只支持HTTP和HTTPS协议"
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
parsedUrl
|
||||
} as const;
|
||||
}
|
||||
|
||||
function buildHeaders(parsedUrl: URL) {
|
||||
return {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
Referer: parsedUrl.origin
|
||||
};
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/api/proxy/image")({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async ({ request }) => {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const imageUrl = searchParams.get("url");
|
||||
|
||||
const validation = validateImageUrl(imageUrl);
|
||||
if (!validation.ok) {
|
||||
return Response.json({ error: validation.error }, { status: validation.status });
|
||||
}
|
||||
|
||||
const response = await fetch(imageUrl!, {
|
||||
headers: buildHeaders(validation.parsedUrl)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return Response.json(
|
||||
{ error: `获取图片失败: ${response.status} ${response.statusText}` },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const imageBuffer = await response.arrayBuffer();
|
||||
if (imageBuffer.byteLength === 0) {
|
||||
return Response.json({ error: "图片内容为空" }, { status: 400 });
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || "image/jpeg";
|
||||
|
||||
return new Response(imageBuffer, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
"Surrogate-Control": "no-store",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type"
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("图片代理未处理的错误:", error);
|
||||
return Response.json(
|
||||
{ error: `处理图片请求时出错: ${error?.message || "未知错误"}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
},
|
||||
HEAD: async ({ request }) => {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const imageUrl = searchParams.get("url");
|
||||
|
||||
const validation = validateImageUrl(imageUrl);
|
||||
if (!validation.ok) {
|
||||
return new Response(null, { status: validation.status });
|
||||
}
|
||||
|
||||
const response = await fetch(imageUrl!, {
|
||||
method: "HEAD",
|
||||
headers: buildHeaders(validation.parsedUrl)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(null, { status: response.status });
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
const contentType = response.headers.get("content-type");
|
||||
const contentLength = response.headers.get("content-length");
|
||||
|
||||
if (contentType) {
|
||||
headers.set("content-type", contentType);
|
||||
}
|
||||
|
||||
if (contentLength) {
|
||||
headers.set("content-length", contentLength);
|
||||
}
|
||||
|
||||
headers.set("Access-Control-Allow-Origin", "*");
|
||||
headers.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
||||
headers.set("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(null, { status: 500 });
|
||||
}
|
||||
},
|
||||
OPTIONS: async () => {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import AISettingsPage from "@/app/app/dashboard/ai/page";
|
||||
|
||||
export const Route = createFileRoute("/app/dashboard/ai")({
|
||||
component: AISettingsPage
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/app/dashboard/")({
|
||||
beforeLoad: () => {
|
||||
throw redirect({
|
||||
to: "/app/dashboard/resumes"
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import ResumesPage from "@/app/app/dashboard/resumes/page";
|
||||
|
||||
export const Route = createFileRoute("/app/dashboard/resumes")({
|
||||
component: ResumesPage
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import SettingsPage from "@/app/app/dashboard/settings/page";
|
||||
|
||||
export const Route = createFileRoute("/app/dashboard/settings")({
|
||||
component: SettingsPage
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import TemplatesPage from "@/app/app/dashboard/templates/page";
|
||||
|
||||
export const Route = createFileRoute("/app/dashboard/templates")({
|
||||
component: TemplatesPage
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
import DashboardLayout from "@/app/app/dashboard/client";
|
||||
|
||||
export const Route = createFileRoute("/app/dashboard")({
|
||||
component: DashboardRoute
|
||||
});
|
||||
|
||||
function DashboardRoute() {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Outlet />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/app/")({
|
||||
beforeLoad: () => {
|
||||
throw redirect({
|
||||
to: "/app/dashboard"
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/app")({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
name: "robots",
|
||||
content: "noindex,nofollow"
|
||||
}
|
||||
]
|
||||
}),
|
||||
component: AppRoute
|
||||
});
|
||||
|
||||
function AppRoute() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useEffect } from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import WorkbenchPage from "@/app/app/workbench/[id]/page";
|
||||
|
||||
export const Route = createFileRoute("/app/workbench/$id")({
|
||||
component: WorkbenchRoute
|
||||
});
|
||||
|
||||
function WorkbenchRoute() {
|
||||
useEffect(() => {
|
||||
document.body.classList.add("overflow-y-hidden", "w-full");
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove("overflow-y-hidden", "w-full");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <WorkbenchPage />;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { resolveLocale } from "@/i18n/runtime";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
beforeLoad: ({ location }) => {
|
||||
const locale = resolveLocale(location.pathname);
|
||||
|
||||
throw redirect({
|
||||
to: "/$locale",
|
||||
params: {
|
||||
locale
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
@import "./app/globals.css";
|
||||
@import "./app/font.css";
|
||||
@import "./styles/tiptap.scss";
|
||||
+10
-28
@@ -1,41 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"target": "ES2022",
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", ".next", ".open-next"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||
import viteReact from "@vitejs/plugin-react";
|
||||
import viteTsConfigPaths from "vite-tsconfig-paths";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { cloudflare } from "@cloudflare/vite-plugin";
|
||||
|
||||
const config = defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
"next/navigation": fileURLToPath(
|
||||
new URL("./src/compat/next/navigation.ts", import.meta.url)
|
||||
),
|
||||
"next/link": fileURLToPath(
|
||||
new URL("./src/compat/next/link.tsx", import.meta.url)
|
||||
),
|
||||
"next/image": fileURLToPath(
|
||||
new URL("./src/compat/next/image.tsx", import.meta.url)
|
||||
),
|
||||
"next/font/google": fileURLToPath(
|
||||
new URL("./src/compat/next/font/google.ts", import.meta.url)
|
||||
),
|
||||
"next/cache": fileURLToPath(
|
||||
new URL("./src/compat/next/cache.ts", import.meta.url)
|
||||
),
|
||||
"next/headers": fileURLToPath(
|
||||
new URL("./src/compat/next/headers.ts", import.meta.url)
|
||||
),
|
||||
"next-intl": fileURLToPath(
|
||||
new URL("./src/i18n/client.tsx", import.meta.url)
|
||||
)
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
||||
viteTsConfigPaths({
|
||||
projects: ["./tsconfig.json"]
|
||||
}),
|
||||
tanstackStart(),
|
||||
viteReact()
|
||||
]
|
||||
});
|
||||
|
||||
export default config;
|
||||
+3
-14
@@ -1,15 +1,4 @@
|
||||
name = "sorafm"
|
||||
main = ".open-next/worker.js"
|
||||
name = "magic-resume"
|
||||
main = "@tanstack/react-start/server-entry"
|
||||
compatibility_date = "2025-12-01"
|
||||
compatibility_flags = ["nodejs_compat", "global_fetch_strictly_public"]
|
||||
|
||||
[assets]
|
||||
directory = ".open-next/assets"
|
||||
binding = "ASSETS"
|
||||
|
||||
[[services]]
|
||||
binding = "WORKER_SELF_REFERENCE"
|
||||
service = "sorafm"
|
||||
|
||||
[images]
|
||||
binding = "IMAGES"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
Reference in New Issue
Block a user