mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-06-01 23:38:48 +02:00
feat: add changelog feature with localized support and update navigation links
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, getTranslations } from "next-intl/server";
|
||||
import LandingHeader from "@/components/home/LandingHeader";
|
||||
import Footer from "@/components/home/Footer";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
params: { locale: string };
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: Props): Promise<Metadata> {
|
||||
const t = await getTranslations({ locale });
|
||||
|
||||
return {
|
||||
title: t("home.changelog") + " - " + t("common.title"),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ChangelogLayout({
|
||||
children,
|
||||
params: { locale },
|
||||
}: Props) {
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<div className="min-h-screen flex flex-col bg-gradient-to-b from-[#f8f9fb] to-white dark:from-gray-900 dark:to-gray-800">
|
||||
<LandingHeader />
|
||||
<main className="flex-grow py-16">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</NextIntlClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import ChangelogTimeline from "@/components/shared/ChangelogTimeline";
|
||||
import { getChangelog } from "@/lib/getChangelog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function ChangelogPage() {
|
||||
const t = useTranslations("home");
|
||||
const changelogEntries = getChangelog();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const locale = pathname.split("/")[1];
|
||||
|
||||
return (
|
||||
<div className="container max-w-5xl mx-auto px-4 md:px-6 py-8">
|
||||
<div className="mb-10 flex flex-col items-center relative">
|
||||
<button
|
||||
onClick={() => router.push(`/${locale}/`)}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 p-2.5 rounded-md bg-primary/10 hover:bg-primary/15 transition-colors flex items-center gap-2"
|
||||
aria-label="返回"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5 text-primary" />
|
||||
</button>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/70 inline-block mb-4">
|
||||
{t("changelog")}
|
||||
</h1>
|
||||
<div className="w-20 h-1 bg-gradient-to-r from-primary/80 to-primary/20 mx-auto rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative p-8 mb-12 overflow-hidden rounded-xl",
|
||||
"before:absolute before:inset-0 before:bg-gradient-to-br before:from-primary/5 before:via-primary/3 before:to-transparent before:rounded-xl",
|
||||
"after:absolute after:inset-0 after:bg-white/40 dark:after:bg-gray-900/40 after:backdrop-blur-sm after:rounded-xl after:-z-10",
|
||||
"border border-white/20 dark:border-gray-700/30 shadow-sm"
|
||||
)}
|
||||
>
|
||||
<div className="relative z-10 mx-auto">
|
||||
<ChangelogTimeline entries={changelogEntries} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Menu, Moon, Sun, X } from "lucide-react";
|
||||
import { FileText, Menu, Moon, Sun, X } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Logo from "@/components/shared/Logo";
|
||||
@@ -16,6 +17,8 @@ import GoDashboard from "./GoDashboard";
|
||||
|
||||
export default function LandingHeader() {
|
||||
const t = useTranslations("home");
|
||||
const pathname = usePathname();
|
||||
const locale = pathname.split("/")[1]; // 从路径中获取语言代码
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -23,12 +26,14 @@ export default function LandingHeader() {
|
||||
<ScrollHeader>
|
||||
<div className="mx-auto max-w-[1200px] px-4">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => (window.location.href = `/${locale}/`)}
|
||||
>
|
||||
<Logo size={32} />
|
||||
<span className="font-bold text-[24px]">{t("header.title")}</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
<LanguageSwitch />
|
||||
<ThemeToggle>
|
||||
@@ -39,6 +44,14 @@ export default function LandingHeader() {
|
||||
</ThemeToggle>
|
||||
<GitHubStars />
|
||||
|
||||
<Link
|
||||
href={`/${locale}/changelog`}
|
||||
className="flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-full bg-primary/10 text-primary hover:bg-primary/15 transition-colors"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
{t("changelog") || "更新日志"}
|
||||
</Link>
|
||||
|
||||
<GoDashboard>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -49,7 +62,6 @@ export default function LandingHeader() {
|
||||
</GoDashboard>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
className="md:hidden p-2 hover:bg-accent rounded-md"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
@@ -64,7 +76,6 @@ export default function LandingHeader() {
|
||||
</div>
|
||||
</ScrollHeader>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<MobileMenu
|
||||
isOpen={isMenuOpen}
|
||||
onClose={() => setIsMenuOpen(false)}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Sun, Moon } from "lucide-react";
|
||||
import { Sun, Moon, FileText } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ThemeToggle from "@/components/shared/ThemeToggle";
|
||||
import LanguageSwitch from "@/components/shared/LanguageSwitch";
|
||||
@@ -13,9 +13,22 @@ interface MobileMenuProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
buttonText: string;
|
||||
extraItems?: Array<{
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
component: React.ReactNode;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function MobileMenu({ isOpen, onClose, buttonText }: MobileMenuProps) {
|
||||
export default function MobileMenu({
|
||||
isOpen,
|
||||
onClose,
|
||||
buttonText,
|
||||
extraItems = [],
|
||||
}: MobileMenuProps) {
|
||||
const t = useTranslations("home");
|
||||
const locale = useLocale();
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
@@ -27,7 +40,6 @@ export default function MobileMenu({ isOpen, onClose, buttonText }: MobileMenuPr
|
||||
>
|
||||
<div className="bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t border-b dark:border-gray-800">
|
||||
<nav className="mx-auto max-w-[1200px] px-4 py-6 flex flex-col gap-6">
|
||||
{/* Controls */}
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
<LanguageSwitch />
|
||||
<ThemeToggle>
|
||||
@@ -39,7 +51,24 @@ export default function MobileMenu({ isOpen, onClose, buttonText }: MobileMenuPr
|
||||
<GitHubStars />
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-center">
|
||||
<Link
|
||||
href={`/${locale}/changelog`}
|
||||
className="flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-full bg-primary/10 text-primary hover:bg-primary/15 transition-colors"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
{t("changelog")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{extraItems && extraItems.length > 0 && (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
{extraItems.map((item, index) => (
|
||||
<div key={index}>{item.component}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 px-4">
|
||||
<Button
|
||||
size="default"
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TimelineEntry } from "@/lib/getChangelog";
|
||||
|
||||
interface ChangelogTimelineProps {
|
||||
entries?: TimelineEntry[];
|
||||
}
|
||||
|
||||
const ChangelogTimeline = ({ entries = [] }: ChangelogTimelineProps) => {
|
||||
const getSectionTypeInfo = (title: string) => {
|
||||
const lowerTitle = title.toLowerCase();
|
||||
if (lowerTitle.includes("新增")) {
|
||||
return {
|
||||
type: "added",
|
||||
label: "新增",
|
||||
className:
|
||||
"bg-green-50 text-green-600 dark:bg-green-900/20 dark:text-green-400",
|
||||
icon: "✨",
|
||||
};
|
||||
} else if (lowerTitle.includes("变更")) {
|
||||
return {
|
||||
type: "changed",
|
||||
label: "变更",
|
||||
className:
|
||||
"bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400",
|
||||
icon: "🔄",
|
||||
};
|
||||
} else if (lowerTitle.includes("修复")) {
|
||||
return {
|
||||
type: "fixed",
|
||||
label: "修复",
|
||||
className:
|
||||
"bg-amber-50 text-amber-600 dark:bg-amber-900/20 dark:text-amber-400",
|
||||
icon: "🛠️",
|
||||
};
|
||||
} else if (lowerTitle.includes("移除")) {
|
||||
return {
|
||||
type: "removed",
|
||||
label: "移除",
|
||||
className:
|
||||
"bg-red-50 text-red-600 dark:bg-red-900/20 dark:text-red-400",
|
||||
icon: "🗑️",
|
||||
};
|
||||
} else if (lowerTitle.includes("优化")) {
|
||||
return {
|
||||
type: "optimized",
|
||||
label: "优化",
|
||||
className:
|
||||
"bg-purple-50 text-purple-600 dark:bg-purple-900/20 dark:text-purple-400",
|
||||
icon: "⚡",
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "other",
|
||||
label: title,
|
||||
className: "bg-gray-50 text-gray-600 dark:bg-gray-800 dark:text-gray-300",
|
||||
icon: "📝",
|
||||
};
|
||||
};
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-gray-500 py-12 text-lg">
|
||||
暂无更新记录
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-14">
|
||||
{entries.map((entry, entryIndex) => (
|
||||
<div key={entryIndex} className="group relative">
|
||||
<div className="mb-4 flex items-center">
|
||||
<div className="h-3 w-3 rounded-full bg-gradient-to-br from-primary to-primary/70 mr-2.5 shadow-sm relative z-10"></div>
|
||||
<span className="text-sm font-medium text-primary/80 dark:text-primary/70">
|
||||
{entry.date}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pl-5 border-l border-primary/20 dark:border-primary/10 space-y-10 relative -mt-5 pt-5 ml-[6px]">
|
||||
{entry.sections.map((section, sIndex) => {
|
||||
const { label, className, icon } = getSectionTypeInfo(
|
||||
section.title
|
||||
);
|
||||
return (
|
||||
<div key={sIndex} className="space-y-4">
|
||||
<Badge
|
||||
className={cn(
|
||||
"capitalize text-xs font-medium px-2.5 py-1 rounded-md",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{icon} {label}
|
||||
</Badge>
|
||||
<ul className="space-y-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
{section.items.map((item, iIndex) => (
|
||||
<li
|
||||
key={iIndex}
|
||||
className="flex items-baseline gap-2.5 group/item hover:text-primary/90 transition-colors duration-200"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary/30 dark:bg-primary/20 mt-1.5 flex-shrink-0 group-hover/item:bg-primary/60 transition-colors duration-200"></span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangelogTimeline;
|
||||
@@ -61,6 +61,7 @@
|
||||
"footer": {
|
||||
"copyright": " 2025 Magic Resume. All rights reserved."
|
||||
},
|
||||
"changelog": "Changelog",
|
||||
"cta": {
|
||||
"title": "Start Your New Career Chapter",
|
||||
"description": "Start using Magic Resume now to create an impressive resume",
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"footer": {
|
||||
"copyright": " 2025 魔方简历. 保留所有权利."
|
||||
},
|
||||
"changelog": "更新日志",
|
||||
"faq": {
|
||||
"title": "常见问题",
|
||||
"items": [
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface TimelineEntry {
|
||||
date: string;
|
||||
sections: {
|
||||
title: string;
|
||||
items: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export function getChangelog(): TimelineEntry[] {
|
||||
return [
|
||||
{
|
||||
date: "2025-03-07",
|
||||
sections: [
|
||||
{
|
||||
title: "新增",
|
||||
items: ["工作台 Dock 栏支持复制简历", "仪表盘简历模板支持预览大图"],
|
||||
},
|
||||
{
|
||||
title: "优化",
|
||||
items: ["服务端导出PDF 速度优化"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user