mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: next-intl i18n config
This commit is contained in:
@@ -5,26 +5,17 @@ const withNextIntl = createNextIntlPlugin();
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/",
|
||||
destination: "/en",
|
||||
permanent: true
|
||||
}
|
||||
];
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/generate-pdf",
|
||||
destination:
|
||||
"http://1255612844-0z3iovadu8.ap-chengdu.tencentscf.com/generate-pdf"
|
||||
}
|
||||
"http://1255612844-0z3iovadu8.ap-chengdu.tencentscf.com/generate-pdf",
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(config);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import {
|
||||
getMessages,
|
||||
getTranslations,
|
||||
setRequestLocale,
|
||||
} from "next-intl/server";
|
||||
import { ReactNode } from "react";
|
||||
import Document from "@/components/Document";
|
||||
import { locales } from "@/i18n/config";
|
||||
import { Providers } from "@/app/providers";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
params: { locale: string };
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: Props): Promise<Metadata> {
|
||||
const t = await getTranslations({ locale, namespace: "common" });
|
||||
|
||||
return {
|
||||
title: t("title"),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params: { locale },
|
||||
}: Props) {
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
// Ensure that the incoming locale is valid
|
||||
if (!locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Providing all messages to the client
|
||||
// side is the easiest way to get started
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<Document locale={locale}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Providers>{children}</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
+7
-3
@@ -5,14 +5,18 @@ import { Button } from "@/components/ui/button";
|
||||
import Header from "@/components/home/Header";
|
||||
import FeaturesAnimation from "@/components/home/FeaturesAnimation";
|
||||
import HeroAnimation from "@/components/home/HeroAnimation";
|
||||
import { setRequestLocale } from "next-intl/server";
|
||||
|
||||
export default function LandingPage() {
|
||||
type Props = {
|
||||
params: { locale: string };
|
||||
};
|
||||
|
||||
export default function LandingPage({ params: { locale } }: Props) {
|
||||
const t = useTranslations("home");
|
||||
|
||||
setRequestLocale(locale);
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<section className="relative pt-32 pb-16">
|
||||
<div className="absolute inset-0 -z-10 h-full w-full bg-white dark:bg-black bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:14px_24px]" />
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
@@ -1,27 +0,0 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextIntlClientProvider, useMessages } from "next-intl";
|
||||
import { locales } from "@/i18n/config";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export default function LocaleLayout({
|
||||
children,
|
||||
params: { locale }
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const messages = useMessages();
|
||||
|
||||
if (!locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
);
|
||||
}
|
||||
+15
-13
@@ -22,21 +22,23 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import Logo from "@/components/shared/Logo";
|
||||
|
||||
const sidebarItems = [
|
||||
{
|
||||
title: "简历",
|
||||
url: "/dashboard/resumes",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
title: "设置",
|
||||
url: "/dashboard/settings",
|
||||
icon: Settings,
|
||||
},
|
||||
];
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const t = useTranslations("dashboard");
|
||||
const sidebarItems = [
|
||||
{
|
||||
title: t("sidebar.resumes"),
|
||||
url: "/app/dashboard/resumes",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
title: t("sidebar.settings"),
|
||||
url: "/app/dashboard/settings",
|
||||
icon: Settings,
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [open, setOpen] = useState(true);
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages, getTranslations } from "next-intl/server";
|
||||
import { ReactNode } from "react";
|
||||
import Document from "@/components/Document";
|
||||
import Client from "./client";
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: Props): Promise<Metadata> {
|
||||
const t = await getTranslations({ locale, 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}>
|
||||
<Client>{children}</Client>
|
||||
</NextIntlClientProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Dashboard() {
|
||||
redirect("/dashboard/resumes");
|
||||
redirect("/app/dashboard/resumes");
|
||||
}
|
||||
+17
-9
@@ -16,6 +16,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { getConfig, getFileHandle, verifyPermission } from "@/utils/fileSystem";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const ResumesList = () => {
|
||||
return <ResumeWorkbench />;
|
||||
@@ -32,6 +33,7 @@ const ResumeWorkbench = () => {
|
||||
} = useResumeStore();
|
||||
const router = useRouter();
|
||||
const [hasConfiguredFolder, setHasConfiguredFolder] = React.useState(false);
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
const syncResumesFromFiles = async () => {
|
||||
@@ -104,7 +106,9 @@ const ResumeWorkbench = () => {
|
||||
{hasConfiguredFolder ? (
|
||||
<Alert className="mb-6" variant="default">
|
||||
<AlertDescription className="flex items-center justify-between">
|
||||
<span className="text-green-600">已设置备份文件夹</span>
|
||||
<span className="text-green-600">
|
||||
{t("dashboard.resumes.synced")}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="ml-4 bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white"
|
||||
@@ -113,7 +117,7 @@ const ResumeWorkbench = () => {
|
||||
}}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
去查看
|
||||
{t("dashboard.resumes.view")}
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -148,7 +152,7 @@ const ResumeWorkbench = () => {
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-indigo-500 to-purple-500 text-transparent bg-clip-text">
|
||||
我的简历
|
||||
{t("dashboard.resumes.myResume")}
|
||||
</h1>
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
@@ -156,7 +160,7 @@ const ResumeWorkbench = () => {
|
||||
className="bg-gradient-to-r from-indigo-500 to-purple-500 text-white hover:from-indigo-600 hover:to-purple-600"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建简历
|
||||
{t("dashboard.resumes.create")}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
@@ -188,8 +192,12 @@ const ResumeWorkbench = () => {
|
||||
>
|
||||
<Plus className="h-8 w-8 text-indigo-500" />
|
||||
</motion.div>
|
||||
<CardTitle className="text-xl">创建新简历</CardTitle>
|
||||
<CardDescription className="mt-2">从头开始创建</CardDescription>
|
||||
<CardTitle className="text-xl">
|
||||
{t("dashboard.resumes.newResume")}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
{t("dashboard.resumes.newResumeDescription")}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
@@ -242,10 +250,10 @@ const ResumeWorkbench = () => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveResume(id);
|
||||
router.push(`/workbench/${id}`);
|
||||
router.push(`/app/workbench/${id}`);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
{t("common.edit")}
|
||||
</Button>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
@@ -261,7 +269,7 @@ const ResumeWorkbench = () => {
|
||||
deleteResume(resume);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages, getTranslations } from "next-intl/server";
|
||||
import { ReactNode } from "react";
|
||||
import Document from "@/components/Document";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
params: { locale: string };
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: Props): Promise<Metadata> {
|
||||
const t = await getTranslations({ locale, namespace: "common" });
|
||||
return {
|
||||
title: t("title"),
|
||||
};
|
||||
}
|
||||
export default async function LocaleLayout({ children }: Props) {
|
||||
const locale = await getLocale();
|
||||
|
||||
// Providing all messages to the client
|
||||
// side is the easiest way to get started
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<Document locale={locale}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages, getTranslations } from "next-intl/server";
|
||||
import { ReactNode } from "react";
|
||||
import Document from "@/components/Document";
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: Props): Promise<Metadata> {
|
||||
const t = await getTranslations({ locale, 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}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</Document>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Providers } from "./providers";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { ReactNode } from "react";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "魔法简历",
|
||||
description: "极度自由的在线简历编辑器"
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Providers>{children}</Providers>
|
||||
<Toaster richColors position="top-center" />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
export default function RootLayout({ children }: Props) {
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Inter } from "next/font/google";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function Document({ children, locale }: Props) {
|
||||
return (
|
||||
<html className={inter.className} lang={locale} suppressHydrationWarning>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@/components/ui/hover-card";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface EditorHeaderProps {
|
||||
isMobile?: boolean;
|
||||
@@ -28,7 +29,7 @@ export function EditorHeader({ isMobile }: EditorHeaderProps) {
|
||||
const themeConfig = getThemeConfig();
|
||||
const { errors, selectError } = useGrammarCheck();
|
||||
const router = useRouter();
|
||||
|
||||
const t = useTranslations();
|
||||
const visibleSections = menuSections
|
||||
?.filter((section) => section.enabled)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
@@ -46,10 +47,10 @@ export function EditorHeader({ isMobile }: EditorHeaderProps) {
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => {
|
||||
router.push("/dashboard");
|
||||
router.push("/app/dashboard");
|
||||
}}
|
||||
>
|
||||
<span className="text-lg font-semibold">Magic Resume</span>
|
||||
<span className="text-lg font-semibold">{t("common.title")}</span>
|
||||
<div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -57,10 +57,10 @@ export default function Header() {
|
||||
initial={{ y: 0, opacity: 1 }}
|
||||
animate={{
|
||||
y: isVisible ? 0 : -100,
|
||||
opacity: isVisible ? 1 : 0
|
||||
opacity: isVisible ? 1 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2
|
||||
duration: 0.2,
|
||||
}}
|
||||
>
|
||||
<div className="mt-4 rounded-full bg-background/70 backdrop-blur-[8px] border border-border/50">
|
||||
@@ -79,7 +79,7 @@ export default function Header() {
|
||||
</div>
|
||||
</ThemeToggle>
|
||||
<GitHubStars />
|
||||
<Link href="/dashboard">
|
||||
<Link href="/app/dashboard">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-primary hover:opacity-90 text-white h-8 text-sm rounded-full px-4"
|
||||
|
||||
@@ -6,16 +6,17 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { locales, localeNames } from "@/i18n/config";
|
||||
import updateLocale from "./UpdateLocale";
|
||||
|
||||
export default function LanguageSwitch() {
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLanguageChange = (newLocale: string) => {
|
||||
const handleLanguageChange = async (newLocale: string) => {
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath === `/${locale}`) {
|
||||
router.push(`/${newLocale}`);
|
||||
@@ -23,6 +24,8 @@ export default function LanguageSwitch() {
|
||||
const newPath = currentPath.replace(`/${locale}/`, `/${newLocale}/`);
|
||||
router.push(newPath);
|
||||
}
|
||||
await updateLocale(newLocale);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
import { setUserLocale } from "@/i18n/db";
|
||||
import { revalidatePath } from "next/cache";
|
||||
export default async function updateLocale(locale: string) {
|
||||
setUserLocale(locale);
|
||||
revalidatePath("/app");
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { DEFAULT_FIELD_ORDER } from ".";
|
||||
import { GlobalSettings, DEFAULT_CONFIG, ResumeData } from "../types/resume";
|
||||
const initialGlobalSettings: GlobalSettings = {
|
||||
baseFontSize: 16,
|
||||
pagePadding: 32,
|
||||
paragraphSpacing: 12,
|
||||
lineHeight: 1.5,
|
||||
sectionSpacing: 10,
|
||||
headerSize: 18,
|
||||
subheaderSize: 16,
|
||||
useIconMode: true,
|
||||
themeColor: "#2563eb",
|
||||
};
|
||||
|
||||
export const initialResumeState = {
|
||||
title: "新建简历",
|
||||
basic: {
|
||||
name: "张三",
|
||||
title: "高级前端工程师",
|
||||
employementStatus: "全职",
|
||||
email: "zhangsan@example.com",
|
||||
phone: "13800138000",
|
||||
location: "北京市朝阳区",
|
||||
birthDate: "1995-01",
|
||||
fieldOrder: DEFAULT_FIELD_ORDER,
|
||||
icons: {
|
||||
email: "Mail",
|
||||
phone: "Phone",
|
||||
birthDate: "CalendarRange",
|
||||
employementStatus: "Briefcase",
|
||||
location: "MapPin",
|
||||
},
|
||||
photoConfig: DEFAULT_CONFIG,
|
||||
customFields: [
|
||||
{
|
||||
id: "personal",
|
||||
label: "个人网站",
|
||||
value: "https://zhangsan.dev",
|
||||
icon: "Globe",
|
||||
},
|
||||
{
|
||||
id: "github",
|
||||
label: "GitHub",
|
||||
value: "https://github.com/zhangsan",
|
||||
icon: "Github",
|
||||
},
|
||||
],
|
||||
photo: "/avatar.png",
|
||||
githubKey: "",
|
||||
githubUseName: "",
|
||||
githubContributionsVisible: false,
|
||||
},
|
||||
education: [
|
||||
{
|
||||
id: "1",
|
||||
school: "北京大学",
|
||||
major: "计算机科学与技术",
|
||||
degree: "本科",
|
||||
startDate: "2013-09",
|
||||
endDate: "2017-06",
|
||||
visible: true,
|
||||
gpa: "3.8/4.0",
|
||||
location: "北京",
|
||||
description: `<ul class="custom-list">
|
||||
<li>主修课程:数据结构、算法设计、操作系统、计算机网络、Web开发技术</li>
|
||||
<li>专业排名前 5%,连续三年获得一等奖学金</li>
|
||||
<li>担任计算机协会技术部部长,组织多次技术分享会</li>
|
||||
<li>参与开源项目贡献,获得 GitHub Campus Expert 认证</li>
|
||||
</ul>`,
|
||||
},
|
||||
],
|
||||
skillContent: `<div class="skill-content">
|
||||
<ul class="custom-list">
|
||||
<li>前端框架:精通 React、Vue.js,熟悉 Next.js、Nuxt.js 等 SSR 框架</li>
|
||||
<li>开发语言:TypeScript、JavaScript(ES6+)、HTML5、CSS3</li>
|
||||
<li>UI/样式:精通 TailwindCSS、Sass/Less、CSS Module、Styled-components</li>
|
||||
<li>状态管理:Redux、Vuex、Zustand、Jotai、React Query</li>
|
||||
<li>工程化工具:Webpack、Vite、Rollup、Babel、ESLint</li>
|
||||
<li>测试工具:Jest、React Testing Library、Cypress</li>
|
||||
<li>性能优化:熟悉浏览器渲染原理、性能指标监控、代码分割、懒加载等优化技术</li>
|
||||
<li>版本控制:Git、SVN</li>
|
||||
<li>技术管理:具备团队管理经验,主导过多个大型项目的技术选型和架构设计</li>
|
||||
<li>技术分享:定期组织团队技术分享,主导建设团队技术博客</li>
|
||||
<li>敏捷开发:熟悉 Scrum 开发流程,具有良好的项目把控能力</li>
|
||||
<li>英语能力:CET-6 分数 560,具备良好的英文文档阅读和技术交流能力</li>
|
||||
</ul>
|
||||
</div>`,
|
||||
experience: [
|
||||
{
|
||||
id: "1",
|
||||
company: "字节跳动",
|
||||
position: "高级前端工程师",
|
||||
date: "2021.07 - 至今",
|
||||
visible: true,
|
||||
details: `<ul class="custom-list">
|
||||
<li>负责抖音创作者平台的开发与维护,主导多个核心功能的技术方案设计</li>
|
||||
<li>优化项目工程化配置,将构建时间从 8 分钟优化至 2 分钟,提升团队开发效率</li>
|
||||
<li>设计并实现组件库,提升代码复用率达 70%,显著减少开发时间</li>
|
||||
<li>主导性能优化项目,使平台首屏加载时间减少 50%,接入 APM 监控系统</li>
|
||||
<li>指导初级工程师,组织技术分享会,提升团队整体技术水平</li>
|
||||
</ul>`,
|
||||
},
|
||||
],
|
||||
draggingProjectId: null,
|
||||
projects: [
|
||||
{
|
||||
id: "p1",
|
||||
name: "抖音创作者中台",
|
||||
role: "前端负责人",
|
||||
date: "2022.06 - 2023.12",
|
||||
description:
|
||||
"基于 React 的创作者数据分析和内容管理平台,服务百万级创作者群体,包含数据分析、内容管理、收益管理等多个子系统。",
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: "p2",
|
||||
name: "微信小程序开发者工具",
|
||||
role: "核心开发者",
|
||||
date: "2020.03 - 2021.06",
|
||||
description:
|
||||
"为开发者提供小程序开发、调试和发布的一站式解决方案。基于 Electron 构建的跨平台桌面应用。",
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: "p3",
|
||||
name: "前端监控平台",
|
||||
role: "技术负责人",
|
||||
date: "2021.09 - 2022.03",
|
||||
description:
|
||||
"一个完整的前端监控解决方案,包含错误监控、性能监控、用户行为分析等功能。",
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
menuSections: [
|
||||
{ id: "basic", title: "基本信息", icon: "👤", enabled: true, order: 0 },
|
||||
{ id: "skills", title: "专业技能", icon: "⚡", enabled: true, order: 1 },
|
||||
{
|
||||
id: "experience",
|
||||
title: "工作经验",
|
||||
icon: "💼",
|
||||
enabled: true,
|
||||
order: 2,
|
||||
},
|
||||
|
||||
{
|
||||
id: "projects",
|
||||
title: "项目经历",
|
||||
icon: "🚀",
|
||||
enabled: true,
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
id: "education",
|
||||
title: "教育经历",
|
||||
icon: "🎓",
|
||||
enabled: true,
|
||||
order: 4,
|
||||
},
|
||||
],
|
||||
customData: {},
|
||||
activeSection: "basic",
|
||||
globalSettings: initialGlobalSettings,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { defaultLocale } from "./config";
|
||||
|
||||
// This cookie name is used by `next-intl` on the public pages too. By
|
||||
// reading/writing to this locale, we can ensure that the user's locale
|
||||
// is consistent across public and private pages. In case you save the
|
||||
// locale of registered users in a database, you can of course also use
|
||||
// that instead when the user is logged in.
|
||||
const COOKIE_NAME = "NEXT_LOCALE";
|
||||
|
||||
export async function getUserLocale() {
|
||||
return cookies().get(COOKIE_NAME)?.value || defaultLocale;
|
||||
}
|
||||
|
||||
export async function setUserLocale(locale: string) {
|
||||
cookies().set(COOKIE_NAME, locale);
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"common": {
|
||||
"title": "Magic Resume",
|
||||
"dashboard": "Dashboard",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"home": {
|
||||
"header": {
|
||||
"title": "Magic Resume",
|
||||
@@ -29,5 +35,22 @@
|
||||
"footer": {
|
||||
"copyright": "© 2024 Magic Resume. All rights reserved."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"sidebar": {
|
||||
"resumes": "Resumes",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"resumes": {
|
||||
"synced": "Synced Files",
|
||||
"view": "View",
|
||||
"myResume": "My Resumes",
|
||||
"create": "Create Resume",
|
||||
"newResume": "New Resume",
|
||||
"newResumeDescription": "Create a new resume to get started."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"common": {
|
||||
"title": "魔法简历",
|
||||
"dashboard": "仪表板",
|
||||
"edit": "编辑",
|
||||
"delete": "删除"
|
||||
},
|
||||
"home": {
|
||||
"header": {
|
||||
"title": "魔法简历",
|
||||
@@ -29,5 +35,22 @@
|
||||
"footer": {
|
||||
"copyright": "© 2024 Magic Resume. All rights reserved."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"sidebar": {
|
||||
"resumes": "简历",
|
||||
"settings": "设置"
|
||||
},
|
||||
"resumes": {
|
||||
"synced": "已备份文件夹",
|
||||
"view": "去查看",
|
||||
"myResume": "我的简历",
|
||||
"create": "创建简历",
|
||||
"newResume": "新建简历",
|
||||
"newResumeDescription": "创建一个新简历以开始。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { defaultLocale, locales } from "./config";
|
||||
import { getUserLocale } from "./db";
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
const locale = await 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 {
|
||||
messages: (await import(`./locales/${locale}.json`)).default
|
||||
locale,
|
||||
messages: (await import(`./locales/${locale}.json`)).default,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
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);
|
||||
@@ -1,12 +1,9 @@
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
import { locales, defaultLocale } from "./i18n/config";
|
||||
import { routing } from "./i18n/routing.public";
|
||||
|
||||
export default createMiddleware({
|
||||
locales,
|
||||
defaultLocale,
|
||||
localePrefix: "always"
|
||||
});
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
matcher: ["/", "/(zh|en)"]
|
||||
// Match only public pathnames
|
||||
matcher: ["/", "/(zh|en)/:path*"],
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
MenuSection,
|
||||
} from "../types/resume";
|
||||
import { DEFAULT_TEMPLATES } from "@/config";
|
||||
import { initialResumeState } from "@/config/initialResumeData";
|
||||
interface ResumeStore {
|
||||
resumes: Record<string, ResumeData>;
|
||||
activeResumeId: string | null;
|
||||
@@ -58,159 +59,6 @@ interface ResumeStore {
|
||||
setTemplate: (templateId: string) => void;
|
||||
}
|
||||
|
||||
const initialGlobalSettings: GlobalSettings = {
|
||||
baseFontSize: 16,
|
||||
pagePadding: 32,
|
||||
paragraphSpacing: 12,
|
||||
lineHeight: 1.5,
|
||||
sectionSpacing: 10,
|
||||
headerSize: 18,
|
||||
subheaderSize: 16,
|
||||
useIconMode: true,
|
||||
themeColor: "#2563eb",
|
||||
};
|
||||
|
||||
const initialResumeState: Omit<
|
||||
ResumeData,
|
||||
"id" | "createdAt" | "updatedAt" | "templateId"
|
||||
> = {
|
||||
title: "新建简历",
|
||||
basic: {
|
||||
name: "张三",
|
||||
title: "高级前端工程师",
|
||||
employementStatus: "全职",
|
||||
email: "zhangsan@example.com",
|
||||
phone: "13800138000",
|
||||
location: "北京市朝阳区",
|
||||
birthDate: "1995-01",
|
||||
fieldOrder: DEFAULT_FIELD_ORDER,
|
||||
icons: {
|
||||
email: "Mail",
|
||||
phone: "Phone",
|
||||
birthDate: "CalendarRange",
|
||||
employementStatus: "Briefcase",
|
||||
location: "MapPin",
|
||||
},
|
||||
photoConfig: DEFAULT_CONFIG,
|
||||
customFields: [
|
||||
{
|
||||
id: "personal",
|
||||
label: "个人网站",
|
||||
value: "https://zhangsan.dev",
|
||||
icon: "Globe",
|
||||
},
|
||||
{
|
||||
id: "github",
|
||||
label: "GitHub",
|
||||
value: "https://github.com/zhangsan",
|
||||
icon: "Github",
|
||||
},
|
||||
],
|
||||
photo: "/avatar.png",
|
||||
githubKey: "",
|
||||
githubUseName: "",
|
||||
githubContributionsVisible: false,
|
||||
},
|
||||
education: [
|
||||
{
|
||||
id: "1",
|
||||
school: "北京大学",
|
||||
major: "计算机科学与技术",
|
||||
degree: "本科",
|
||||
startDate: "2013-09",
|
||||
endDate: "2017-06",
|
||||
visible: true,
|
||||
gpa: "3.8/4.0",
|
||||
location: "北京",
|
||||
description: `<ul class="custom-list">
|
||||
<li>主修课程:数据结构、算法设计、操作系统、计算机网络、Web开发技术</li>
|
||||
<li>专业排名前 5%,连续三年获得一等奖学金</li>
|
||||
<li>担任计算机协会技术部部长,组织多次技术分享会</li>
|
||||
<li>参与开源项目贡献,获得 GitHub Campus Expert 认证</li>
|
||||
</ul>`,
|
||||
},
|
||||
],
|
||||
skillContent: `<div class="skill-content">
|
||||
<ul class="custom-list">
|
||||
<li>前端框架:精通 React、Vue.js,熟悉 Next.js、Nuxt.js 等 SSR 框架</li>
|
||||
<li>开发语言:TypeScript、JavaScript(ES6+)、HTML5、CSS3</li>
|
||||
<li>UI/样式:精通 TailwindCSS、Sass/Less、CSS Module、Styled-components</li>
|
||||
<li>状态管理:Redux、Vuex、Zustand、Jotai、React Query</li>
|
||||
<li>工程化工具:Webpack、Vite、Rollup、Babel、ESLint</li>
|
||||
<li>测试工具:Jest、React Testing Library、Cypress</li>
|
||||
<li>性能优化:熟悉浏览器渲染原理、性能指标监控、代码分割、懒加载等优化技术</li>
|
||||
<li>版本控制:Git、SVN</li>
|
||||
<li>技术管理:具备团队管理经验,主导过多个大型项目的技术选型和架构设计</li>
|
||||
<li>技术分享:定期组织团队技术分享,主导建设团队技术博客</li>
|
||||
<li>敏捷开发:熟悉 Scrum 开发流程,具有良好的项目把控能力</li>
|
||||
<li>英语能力:CET-6 分数 560,具备良好的英文文档阅读和技术交流能力</li>
|
||||
</ul>
|
||||
</div>`,
|
||||
experience: [
|
||||
{
|
||||
id: "1",
|
||||
company: "字节跳动",
|
||||
position: "高级前端工程师",
|
||||
date: "2021.07 - 至今",
|
||||
visible: true,
|
||||
details: `<ul class="custom-list">
|
||||
<li>负责抖音创作者平台的开发与维护,主导多个核心功能的技术方案设计</li>
|
||||
<li>优化项目工程化配置,将构建时间从 8 分钟优化至 2 分钟,提升团队开发效率</li>
|
||||
<li>设计并实现组件库,提升代码复用率达 70%,显著减少开发时间</li>
|
||||
<li>主导性能优化项目,使平台首屏加载时间减少 50%,接入 APM 监控系统</li>
|
||||
<li>指导初级工程师,组织技术分享会,提升团队整体技术水平</li>
|
||||
</ul>`,
|
||||
},
|
||||
],
|
||||
draggingProjectId: null,
|
||||
projects: [
|
||||
{
|
||||
id: "p1",
|
||||
name: "抖音创作者中台",
|
||||
role: "前端负责人",
|
||||
date: "2022.06 - 2023.12",
|
||||
description:
|
||||
"基于 React 的创作者数据分析和内容管理平台,服务百万级创作者群体,包含数据分析、内容管理、收益管理等多个子系统。",
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: "p2",
|
||||
name: "微信小程序开发者工具",
|
||||
role: "核心开发者",
|
||||
date: "2020.03 - 2021.06",
|
||||
description:
|
||||
"为开发者提供小程序开发、调试和发布的一站式解决方案。基于 Electron 构建的跨平台桌面应用。",
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: "p3",
|
||||
name: "前端监控平台",
|
||||
role: "技术负责人",
|
||||
date: "2021.09 - 2022.03",
|
||||
description:
|
||||
"一个完整的前端监控解决方案,包含错误监控、性能监控、用户行为分析等功能。",
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
menuSections: [
|
||||
{ id: "basic", title: "基本信息", icon: "👤", enabled: true, order: 0 },
|
||||
{ id: "skills", title: "专业技能", icon: "⚡", enabled: true, order: 1 },
|
||||
{
|
||||
id: "experience",
|
||||
title: "工作经验",
|
||||
icon: "💼",
|
||||
enabled: true,
|
||||
order: 2,
|
||||
},
|
||||
|
||||
{ id: "projects", title: "项目经历", icon: "🚀", enabled: true, order: 3 },
|
||||
{ id: "education", title: "教育经历", icon: "🎓", enabled: true, order: 4 },
|
||||
],
|
||||
customData: {},
|
||||
activeSection: "basic",
|
||||
globalSettings: initialGlobalSettings,
|
||||
};
|
||||
|
||||
// 同步简历到文件系统
|
||||
const syncResumeToFile = async (
|
||||
resumeData: ResumeData,
|
||||
|
||||
Reference in New Issue
Block a user