feat: add changelog feature with localized support and update navigation links

This commit is contained in:
JOYCEQL
2025-03-07 13:41:02 +08:00
committed by qingchen
parent bf846c0bd7
commit 4cf7c97dc4
8 changed files with 285 additions and 10 deletions
@@ -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>
);
}
+16 -5
View File
@@ -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)}
+34 -5
View File
@@ -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"
+121
View File
@@ -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;
+1
View File
@@ -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",
+1
View File
@@ -67,6 +67,7 @@
"footer": {
"copyright": " 2025 魔方简历. 保留所有权利."
},
"changelog": "更新日志",
"faq": {
"title": "常见问题",
"items": [
+25
View File
@@ -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 速度优化"],
},
],
},
];
}