feat: add changelog page and update navigation links
@@ -8,6 +8,7 @@
|
|||||||
"home": "Home",
|
"home": "Home",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
|
"changelog": "Changelog",
|
||||||
"tools": "Tools",
|
"tools": "Tools",
|
||||||
"jsonProcessing": "JSON Processing",
|
"jsonProcessing": "JSON Processing",
|
||||||
"dailyTools": "Daily Tools",
|
"dailyTools": "Daily Tools",
|
||||||
@@ -43,6 +44,10 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"title": "Contact DevLab",
|
"title": "Contact DevLab",
|
||||||
"description": "Get in touch via GitHub Issues for support, bug reports, or feature requests."
|
"description": "Get in touch via GitHub Issues for support, bug reports, or feature requests."
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"title": "Changelog",
|
||||||
|
"description": "A complete history of updates and improvements to DevLab."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
@@ -170,5 +175,13 @@
|
|||||||
"scale": {
|
"scale": {
|
||||||
"title": "BMI Categories"
|
"title": "BMI Categories"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"title": "Changelog",
|
||||||
|
"description": "All notable changes to DevLab are documented here.",
|
||||||
|
"featureType": "Feature",
|
||||||
|
"fixType": "Fix",
|
||||||
|
"refactorType": "Refactor",
|
||||||
|
"choreType": "Chore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"home": "首页",
|
"home": "首页",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
"contact": "联系",
|
"contact": "联系",
|
||||||
|
"changelog": "更新日志",
|
||||||
"tools": "工具",
|
"tools": "工具",
|
||||||
"jsonProcessing": "JSON 处理",
|
"jsonProcessing": "JSON 处理",
|
||||||
"dailyTools": "日常小工具",
|
"dailyTools": "日常小工具",
|
||||||
@@ -43,6 +44,10 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"title": "联系 DevLab",
|
"title": "联系 DevLab",
|
||||||
"description": "通过 GitHub Issues 反馈问题或提交功能需求。"
|
"description": "通过 GitHub Issues 反馈问题或提交功能需求。"
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"title": "更新日志",
|
||||||
|
"description": "DevLab 完整的更新和改进历史记录。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
@@ -170,5 +175,13 @@
|
|||||||
"scale": {
|
"scale": {
|
||||||
"title": "BMI 分类"
|
"title": "BMI 分类"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"title": "更新日志",
|
||||||
|
"description": "DevLab 所有值得关注的变更都记录在此。",
|
||||||
|
"featureType": "功能",
|
||||||
|
"fixType": "修复",
|
||||||
|
"refactorType": "重构",
|
||||||
|
"choreType": "维护"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ export default function HeroLayout() {
|
|||||||
>
|
>
|
||||||
{t("navigation.contact")}
|
{t("navigation.contact")}
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/changelog"
|
||||||
|
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||||
|
>
|
||||||
|
{t("navigation.changelog")}
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,22 +38,24 @@ export default function ToolsLayout() {
|
|||||||
<nav className="flex space-x-8">
|
<nav className="flex space-x-8">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
>
|
|
||||||
{t("navigation.home")}
|
{t("navigation.home")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/about"
|
to="/about"
|
||||||
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
>
|
|
||||||
{t("navigation.about")}
|
{t("navigation.about")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
>
|
|
||||||
{t("navigation.contact")}
|
{t("navigation.contact")}
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/changelog"
|
||||||
|
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
|
||||||
|
{t("navigation.changelog")}
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
@@ -66,8 +68,7 @@ export default function ToolsLayout() {
|
|||||||
<aside
|
<aside
|
||||||
className={`bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col transition-all duration-200 ${
|
className={`bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col transition-all duration-200 ${
|
||||||
collapsed ? "w-16" : "w-56"
|
collapsed ? "w-16" : "w-56"
|
||||||
}`}
|
}`}>
|
||||||
>
|
|
||||||
<div className="px-3 py-3 border-b border-slate-200 flex items-center justify-between">
|
<div className="px-3 py-3 border-b border-slate-200 flex items-center justify-between">
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
@@ -80,8 +81,7 @@ export default function ToolsLayout() {
|
|||||||
className="w-8 h-8 rounded-md border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colours text-sm"
|
className="w-8 h-8 rounded-md border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colours text-sm"
|
||||||
aria-label={
|
aria-label={
|
||||||
collapsed ? t("navigation.expandToolsMenu") : t("navigation.collapseToolsMenu")
|
collapsed ? t("navigation.expandToolsMenu") : t("navigation.collapseToolsMenu")
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
{collapsed ? "»" : "«"}
|
{collapsed ? "»" : "«"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,8 +105,7 @@ export default function ToolsLayout() {
|
|||||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900"
|
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
title={collapsed ? `${group.title} · ${tool.label}` : tool.label}
|
title={collapsed ? `${group.title} · ${tool.label}` : tool.label}>
|
||||||
>
|
|
||||||
{collapsed ? tool.shortLabel : tool.label}
|
{collapsed ? tool.shortLabel : tool.label}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import Seo from "@/components/seo"
|
||||||
|
|
||||||
|
interface ChangeEntry {
|
||||||
|
type: "feat" | "fix" | "refactor" | "chore"
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
date?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChangelogVersion {
|
||||||
|
version: string
|
||||||
|
date: string
|
||||||
|
entries: ChangeEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGELOG_DATA: ChangelogVersion[] = [
|
||||||
|
{
|
||||||
|
version: "1.1.0",
|
||||||
|
date: "2026-02-24",
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
title: "JSON Grid",
|
||||||
|
description: "Convert JSON arrays into sortable table view with CSV export functionality"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
title: "Tools Layout with Collapsible Menu",
|
||||||
|
description: "Added a dedicated tools layout with categorised sidebar menu for quick tool switching",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
title: "Copy Selected JSON Feature",
|
||||||
|
description: "Added ability to copy JSONPath selected data as raw JSON with one-click action",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
title: "Tool Categories",
|
||||||
|
description: "Organised tools into JSON Processing (JSON Viewer, JSON Grid) and Daily Tools (BMI Calculator)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "fix",
|
||||||
|
title: "Live Application Link",
|
||||||
|
description: "Updated live application link in README",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "1.0.0",
|
||||||
|
date: "2026-01-19",
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
title: "JSON Viewer with JSONPath",
|
||||||
|
description: "Full-featured JSON viewer with JSONPath query support and CSV export",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
title: "BMI Calculator",
|
||||||
|
description: "Calculate Body Mass Index with category guidance and health advice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
title: "Internationalization",
|
||||||
|
description: "Support for English (GB) and Simplified Chinese with language switcher",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
type ChangeTypeKey = "feat" | "fix" | "refactor" | "chore"
|
||||||
|
|
||||||
|
export default function Changelog() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const changeTypeLabels = useMemo(() => {
|
||||||
|
const labels: Record<ChangeTypeKey, { label: string; colour: string }> = {
|
||||||
|
feat: { label: t("changelog.featureType"), colour: "bg-emerald-100 text-emerald-700" },
|
||||||
|
fix: { label: t("changelog.fixType"), colour: "bg-blue-100 text-blue-700" },
|
||||||
|
refactor: { label: t("changelog.refactorType"), colour: "bg-purple-100 text-purple-700" },
|
||||||
|
chore: { label: t("changelog.choreType"), colour: "bg-slate-100 text-slate-700" },
|
||||||
|
}
|
||||||
|
return labels
|
||||||
|
}, [t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Seo
|
||||||
|
title={t("seo.changelog.title")}
|
||||||
|
description={t("seo.changelog.description")}
|
||||||
|
path="/changelog"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white shadow-sm border-b">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900">{t("changelog.title")}</h1>
|
||||||
|
<p className="mt-2 text-lg text-gray-600">{t("changelog.description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline */}
|
||||||
|
<div className="max-w-4xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="space-y-8">
|
||||||
|
{CHANGELOG_DATA.map((changelog) => (
|
||||||
|
<div key={changelog.version} className="relative">
|
||||||
|
{/* Version header */}
|
||||||
|
<div className="flex items-baseline gap-4 mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">v{changelog.version}</h2>
|
||||||
|
<time className="text-sm text-gray-500">{changelog.date}</time>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Changes list */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{changelog.entries.map((entry, idx) => {
|
||||||
|
const typeInfo = changeTypeLabels[entry.type as ChangeTypeKey]
|
||||||
|
return (
|
||||||
|
<div key={`${changelog.version}-${idx}`} className="bg-white rounded-lg border border-slate-200 p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className={`inline-block px-2.5 py-0.5 rounded text-xs font-semibold ${typeInfo.colour}`}>
|
||||||
|
{typeInfo.label}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-base font-semibold text-gray-900">{entry.title}</h3>
|
||||||
|
{entry.description && (
|
||||||
|
<p className="mt-1 text-sm text-gray-600">{entry.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -36,6 +36,10 @@ const router = createBrowserRouter(
|
|||||||
path: "contact",
|
path: "contact",
|
||||||
lazy: lazy(() => import("@/page/contact")),
|
lazy: lazy(() => import("@/page/contact")),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "changelog",
|
||||||
|
lazy: lazy(() => import("@/page/changelog")),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||