feat: enhance internationalization and update application branding

- Updated application title and page titles from "DevHub" to "DevLab" across multiple locales.
- Added new translations for the home, about, contact, and JSON viewer pages to support enhanced user experience.
- Integrated language switcher in the layout for improved accessibility.
- Revised BMI calculator and contact form to reflect new branding and improved user guidance.
This commit is contained in:
2026-01-19 15:17:03 +08:00
parent 0d783b605e
commit 11d0cc5765
8 changed files with 422 additions and 214 deletions
+88 -4
View File
@@ -1,17 +1,100 @@
{
"app": {
"title": "DevHub",
"pageTitle": "DevHub",
"title": "DevLab",
"pageTitle": "DevLab",
"copyright": "© {{year}} OnixByte. Built with React & TypeScript."
},
"navigation": {
"home": "Home"
"home": "Home",
"about": "About",
"contact": "Contact"
},
"language": {
"switch": "Switch Language",
"english": "English (Great Britain)",
"chinese": "简体中文"
},
"home": {
"title": "DevLab",
"description": "A collection of powerful, privacy-focused developer tools. All processing happens locally in your browser.",
"getStarted": "Get Started",
"getStartedDescription": "Start visualising and querying your JSON data with our intuitive JSONPath-based tool.",
"openJsonViewer": "Open JSON Viewer",
"openBmiCalculator": "Open BMI Calculator",
"bmiCalculatorDescription": "Calculate your Body Mass Index (BMI) to assess your weight status and health. Get instant results with personalised advice based on your BMI category.",
"features": {
"tools": {
"title": "🛠️ Developer Tools",
"description": "A growing collection of practical tools for developers and everyday use."
},
"privacy": {
"title": "🔒 Privacy First",
"description": "All processing happens locally in your browser. No data is ever sent to servers."
},
"free": {
"title": "✨ Free & Open",
"description": "Completely free to use with no limitations. Open source and transparent."
}
}
},
"jsonViewer": {
"jsonSource": "JSON Source",
"jsonPathExpression": "JSONPath Expression",
"invalidSyntax": "— Invalid syntax",
"placeholder": "e.g. $..roles",
"visualisedResult": "Visualised Result",
"matches": "matches",
"copied": "Copied!",
"copyAsCsv": "Copy as CSV",
"error": "Error:"
},
"about": {
"title": "About DevLab",
"description": "A powerful, privacy-focused tool for debugging and visualising complex JSON data structures.",
"openSource": {
"title": "📦 Open Source & Deployment",
"description": "This project is open source and available on GitHub. You can view the source code, contribute, or deploy your own instance.",
"viewOnGitHub": "View on GitHub",
"starUs": "⭐ Star us on GitHub:",
"starUsDescription": "If you find this project helpful, we'd greatly appreciate it if you could give us a star on GitHub. It helps others discover the project and motivates us to keep improving!",
"selfHosting": "💡 Self-Hosting:",
"selfHostingDescription": "You can deploy this application yourself! The repository includes all necessary configuration files. Simply clone the repository, install dependencies, and deploy to your preferred hosting platform (Vercel, Netlify, or any static hosting service)."
},
"privacy": {
"title": "🔒 Privacy & Data Security",
"description": "We believe that your data belongs to you. This application is designed as a purely client-side tool. All JSON parsing, path evaluation, and visual rendering are performed locally within your browser. No data is ever uploaded to any server, ensuring that sensitive configuration or user data remains entirely private."
}
},
"contact": {
"title": "Get in Touch",
"description": "The best way to reach us is via our GitHub repository.",
"whyGitHub": {
"title": "🤝 Why GitHub Issues?",
"description": "We use GitHub to track all bug reports and feature requests. This ensures our development process remains transparent and that your feedback is properly prioritised by the community."
},
"howItWorks": {
"title": "How it works:",
"step1": "Fill in the enquiry details in the form provided.",
"step2": "Click \"Prepare GitHub Issue\".",
"step3": "You will be redirected to GitHub with the data pre-filled.",
"step4": "Simply hit \"Submit new issue\" on their site."
},
"email": "Alternatively, you can email us directly at:",
"form": {
"subject": "Subject",
"subjectPlaceholder": "Bug report / Feature request",
"type": "Enquiry Type",
"typeHelp": "Selecting a type helps us categorise and prioritise your issue.",
"message": "Message Details",
"messagePlaceholder": "Describe your enquiry here...",
"submit": "Prepare GitHub Issue"
},
"types": {
"helpWanted": "General Enquiry",
"enhancement": "Feature Request",
"bug": "Bug"
}
},
"bmi": {
"title": "BMI Calculator",
"description": "Calculate your Body Mass Index (BMI) to assess your weight status and health.",
@@ -26,7 +109,8 @@
"calculate": "Calculate BMI",
"reset": "Reset",
"result": {
"title": "Your BMI Result"
"title": "Your BMI Result",
"emptyState": "Enter your weight and height to calculate your BMI"
},
"category": {
"underweight": "Underweight",
+88 -4
View File
@@ -1,17 +1,100 @@
{
"app": {
"title": "DevHub",
"pageTitle": "DevHub",
"title": "DevLab",
"pageTitle": "DevLab",
"copyright": "© {{year}} OnixByte。 使用 React 和 TypeScript 构建。"
},
"navigation": {
"home": "首页"
"home": "首页",
"about": "关于",
"contact": "联系"
},
"language": {
"switch": "切换语言",
"english": "English (Great Britain)",
"chinese": "简体中文"
},
"home": {
"title": "DevLab",
"description": "一系列强大的、注重隐私的开发者工具集合。所有处理都在您的浏览器本地进行。",
"getStarted": "开始使用",
"getStartedDescription": "使用我们直观的基于 JSONPath 的工具开始可视化和查询您的 JSON 数据。",
"openJsonViewer": "打开 JSON 查看器",
"openBmiCalculator": "打开 BMI 计算器",
"bmiCalculatorDescription": "计算您的身体质量指数(BMI)以评估您的体重状态和健康状况。根据您的 BMI 分类获得即时结果和个性化建议。",
"features": {
"tools": {
"title": "🛠️ 开发者工具",
"description": "不断增长的实用工具集合,适用于开发者和日常使用。"
},
"privacy": {
"title": "🔒 隐私优先",
"description": "所有处理都在您的浏览器本地进行。数据永远不会发送到服务器。"
},
"free": {
"title": "✨ 免费开源",
"description": "完全免费使用,无任何限制。开源且透明。"
}
}
},
"jsonViewer": {
"jsonSource": "JSON 源",
"jsonPathExpression": "JSONPath 表达式",
"invalidSyntax": "— 语法无效",
"placeholder": "例如:$..roles",
"visualisedResult": "可视化结果",
"matches": "个匹配",
"copied": "已复制!",
"copyAsCsv": "复制为 CSV",
"error": "错误:"
},
"about": {
"title": "关于 DevLab",
"description": "一个强大的、注重隐私的工具,用于调试和可视化复杂的 JSON 数据结构。",
"openSource": {
"title": "📦 开源与部署",
"description": "这个项目是开源的,可在 GitHub 上获取。您可以查看源代码、贡献代码或部署您自己的实例。",
"viewOnGitHub": "在 GitHub 上查看",
"starUs": "⭐ 在 GitHub 上为我们加星:",
"starUsDescription": "如果您觉得这个项目有帮助,我们非常感激您能在 GitHub 上为我们加星。这有助于其他人发现这个项目,并激励我们不断改进!",
"selfHosting": "💡 自托管:",
"selfHostingDescription": "您可以自己部署这个应用程序!仓库包含所有必要的配置文件。只需克隆仓库、安装依赖,然后部署到您首选的托管平台(Vercel、Netlify 或任何静态托管服务)。"
},
"privacy": {
"title": "🔒 隐私与数据安全",
"description": "我们相信您的数据属于您。此应用程序设计为纯客户端工具。所有 JSON 解析、路径评估和可视化渲染都在您的浏览器本地执行。数据永远不会上传到任何服务器,确保敏感配置或用户数据完全私密。"
}
},
"contact": {
"title": "联系我们",
"description": "联系我们的最佳方式是通过我们的 GitHub 仓库。",
"whyGitHub": {
"title": "🤝 为什么使用 GitHub Issues",
"description": "我们使用 GitHub 来跟踪所有错误报告和功能请求。这确保我们的开发过程保持透明,并且您的反馈会被社区正确优先处理。"
},
"howItWorks": {
"title": "使用方法:",
"step1": "在提供的表单中填写查询详情。",
"step2": "点击 \"准备 GitHub Issue\"。",
"step3": "您将被重定向到 GitHub,数据已预填。",
"step4": "只需在他们的网站上点击 \"提交新问题\"。"
},
"email": "或者,您可以直接发送电子邮件至:",
"form": {
"subject": "主题",
"subjectPlaceholder": "错误报告 / 功能请求",
"type": "查询类型",
"typeHelp": "选择类型有助于我们分类和优先处理您的问题。",
"message": "消息详情",
"messagePlaceholder": "在此描述您的查询...",
"submit": "准备 GitHub Issue"
},
"types": {
"helpWanted": "一般查询",
"enhancement": "功能请求",
"bug": "错误"
}
},
"bmi": {
"title": "BMI 计算器",
"description": "计算您的身体质量指数(BMI)以评估您的体重状态和健康状况。",
@@ -26,7 +109,8 @@
"calculate": "计算 BMI",
"reset": "重置",
"result": {
"title": "您的 BMI 结果"
"title": "您的 BMI 结果",
"emptyState": "请输入您的体重和身高以计算 BMI"
},
"category": {
"underweight": "体重过轻",
+11 -5
View File
@@ -1,6 +1,8 @@
import { Outlet, Link, useLocation } from "react-router-dom"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import dayjs from "dayjs"
import LanguageSwitcher from "@/components/language-switcher"
/**
* Main application component that serves as the root layout.
@@ -8,6 +10,7 @@ import dayjs from "dayjs"
*/
export default function HeroLayout() {
const today = useMemo(() => dayjs(), [])
const { t } = useTranslation()
return (
<div className="h-screen bg-gray-50 flex flex-col overflow-hidden">
@@ -17,29 +20,32 @@ export default function HeroLayout() {
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<h1 className="text-xl font-semibold text-gray-900">
DevLab
{t("app.title")}
</h1>
</div>
<div className="flex items-center gap-4">
<nav className="flex space-x-8">
<Link
to="/"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Home
{t("navigation.home")}
</Link>
<Link
to="/about"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
About
{t("navigation.about")}
</Link>
<Link
to="/contact"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Contact
{t("navigation.contact")}
</Link>
</nav>
<LanguageSwitcher />
</div>
</div>
</div>
</header>
@@ -53,7 +59,7 @@ export default function HeroLayout() {
<footer className="bg-white border-t shrink-0">
<div className="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
<p className="text-center text-sm text-gray-500">
© 2024-{today.year()} OnixByte. Built with React & TypeScript.
{t("app.copyright", { year: today.year() })}
</p>
</div>
</footer>
+14 -20
View File
@@ -1,31 +1,33 @@
import { useTranslation } from "react-i18next"
/**
* About page component that displays information about the DevLab application.
*/
export default function About() {
const { t } = useTranslation()
return (
<div className="space-y-8 max-w-6xl mx-auto">
{/* Page Header */}
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">About DevLab</h1>
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("about.title")}</h1>
<p className="mt-4 text-lg text-gray-600">
A powerful, privacy-focused tool for debugging and visualising complex JSON data
structures.
{t("about.description")}
</p>
</div>
{/* GitHub & Deployment */}
<div className="bg-white shadow rounded-lg p-6 border border-gray-100">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<span className="mr-2">📦</span> Open Source & Deployment
<span className="mr-2">📦</span> {t("about.openSource.title")}
</h2>
<div className="space-y-4 text-gray-700">
<p>
This project is open source and available on GitHub. You can view the source code,
contribute, or deploy your own instance.
{t("about.openSource.description")}
</p>
<div className="flex items-center gap-3">
<a
href="https://github.com/onixbyte/json-visualiser"
href="https://github.com/onixbyte/dev-lab"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition-colours font-medium">
@@ -36,22 +38,17 @@ export default function About() {
clipRule="evenodd"
/>
</svg>
View on GitHub
{t("about.openSource.viewOnGitHub")}
</a>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-amber-900">
<strong> Star us on GitHub:</strong> If you find this project helpful, we'd greatly
appreciate it if you could give us a star on GitHub. It helps others discover the
project and motivates us to keep improving!
<strong>{t("about.openSource.starUs")}</strong> {t("about.openSource.starUsDescription")}
</p>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-lg p-4 mt-4">
<p className="text-blue-900">
<strong>💡 Self-Hosting:</strong> You can deploy this application yourself! The
repository includes all necessary configuration files. Simply clone the repository,
install dependencies, and deploy to your preferred hosting platform (Vercel, Netlify,
or any static hosting service).
<strong>{t("about.openSource.selfHosting")}</strong> {t("about.openSource.selfHostingDescription")}
</p>
</div>
</div>
@@ -60,13 +57,10 @@ export default function About() {
{/* Privacy Commitment */}
<div className="bg-indigo-50 border border-indigo-100 rounded-lg p-6 text-indigo-900">
<h2 className="text-xl font-semibold mb-3 flex items-center">
<span className="mr-2">🔒</span> Privacy & Data Security
<span className="mr-2">🔒</span> {t("about.privacy.title")}
</h2>
<p className="leading-relaxed">
We believe that your data belongs to you. This application is designed as a{" "}
<strong>purely client-side tool</strong>. All JSON parsing, path evaluation, and visual
rendering are performed locally within your browser. No data is ever uploaded to any
server, ensuring that sensitive configuration or user data remains entirely private.
{t("about.privacy.description")}
</p>
</div>
</div>
+75 -41
View File
@@ -2,7 +2,7 @@ import { useState } from "react"
import { useTranslation } from "react-i18next"
/**
* Home page component that displays the BMI calculator.
* BMI Calculator page component that displays the BMI calculator tool.
*/
export default function BmiCalculator() {
const { t } = useTranslation()
@@ -55,21 +55,39 @@ export default function BmiCalculator() {
}
}
return (
<div className="w-full max-w-3xl mx-auto p-4 sm:p-6">
<div className="w-full space-y-6">
{/* Header */}
<div className="text-center">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">{t("bmi.title")}</h1>
<p className="text-sm sm:text-base text-gray-600">{t("bmi.description")}</p>
</div>
const getBmiBgColour = () => {
switch (bmiCategory) {
case "underweight":
return "bg-blue-50 border-blue-100"
case "normal":
return "bg-green-50 border-green-100"
case "overweight":
return "bg-yellow-50 border-yellow-100"
case "obese":
return "bg-red-50 border-red-100"
default:
return "bg-gray-50 border-gray-100"
}
}
return (
<div className="h-full flex gap-4 overflow-hidden">
{/* Left panel - 30% */}
<div className="w-[30%] flex flex-col gap-4 min-h-0">
{/* Input Form - fills remaining height */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex-1 flex flex-col min-h-0">
<div className="bg-slate-50 px-4 py-2 border-b border-slate-200 shrink-0">
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
{t("bmi.title")}
</span>
</div>
<div className="flex-1 p-4 overflow-auto min-h-0">
<div className="space-y-4">
<p className="text-sm text-gray-600 mb-4">{t("bmi.description")}</p>
{/* Calculator Form */}
<div className="bg-white rounded-lg shadow-md p-4 sm:p-6 w-full mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 mb-6">
{/* Weight Input */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">
{t("bmi.weight.label")}
</label>
<div className="relative">
@@ -78,17 +96,17 @@ export default function BmiCalculator() {
value={weight}
onChange={(e) => setWeight(e.target.value)}
placeholder={t("bmi.weight.placeholder")}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-base"
className="w-full p-3 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all shadow-sm"
min="1"
max="500"
/>
<span className="absolute right-3 top-3 text-gray-500 text-sm">kg</span>
<span className="absolute right-3 top-3 text-gray-500 text-xs">kg</span>
</div>
</div>
{/* Height Input */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">
{t("bmi.height.label")}
</label>
<div className="relative">
@@ -97,66 +115,81 @@ export default function BmiCalculator() {
value={height}
onChange={(e) => setHeight(e.target.value)}
placeholder={t("bmi.height.placeholder")}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-base"
className="w-full p-3 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all shadow-sm"
min="50"
max="300"
/>
<span className="absolute right-3 top-3 text-gray-500 text-sm">cm</span>
</div>
<span className="absolute right-3 top-3 text-gray-500 text-xs">cm</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
<div className="flex flex-col gap-2 pt-2">
<button
onClick={calculateBMI}
disabled={!weight || !height}
className="flex-1 bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors text-base">
className="w-full bg-indigo-600 text-white py-2.5 px-4 rounded-lg text-sm font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:bg-slate-300 disabled:cursor-not-allowed transition-colours">
{t("bmi.calculate")}
</button>
<button
onClick={resetCalculator}
className="sm:w-auto px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors text-base">
className="w-full border border-slate-200 text-slate-700 py-2.5 px-4 rounded-lg text-sm font-medium hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-500 transition-colours">
{t("bmi.reset")}
</button>
</div>
</div>
</div>
</div>
</div>
{/* BMI Result */}
{bmi !== null && (
<div className="bg-white rounded-lg shadow-md p-4 sm:p-6 w-full mx-auto">
<div className="text-center">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 mb-4">
{/* Right result panel - 70% */}
<div className="w-[70%] bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden min-h-0">
<div className="bg-slate-50 px-4 py-2 border-b border-slate-200 shrink-0">
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
{t("bmi.result.title")}
</h2>
<div className={`text-3xl sm:text-4xl font-bold mb-2 ${getBmiColour()}`}>{bmi}</div>
<div className={`text-base sm:text-lg font-medium mb-4 ${getBmiColour()}`}>
</span>
</div>
<div className="flex-1 p-6 overflow-auto min-h-0">
{bmi === null ? (
<div className="flex items-center justify-center h-full">
<div className="text-center text-gray-400">
<p className="text-sm">{t("bmi.description")}</p>
<p className="text-xs mt-2">{t("bmi.result.emptyState")}</p>
</div>
</div>
) : (
<div className="space-y-6">
{/* BMI Value Display */}
<div className={`text-center p-6 rounded-lg border ${getBmiBgColour()}`}>
<div className={`text-5xl font-bold mb-2 ${getBmiColour()}`}>{bmi}</div>
<div className={`text-lg font-semibold mb-3 ${getBmiColour()}`}>
{t(`bmi.category.${bmiCategory}`)}
</div>
<div className="text-gray-600 text-sm sm:text-base px-4">
<div className="text-sm text-gray-700 max-w-md mx-auto">
{t(`bmi.advice.${bmiCategory}`)}
</div>
</div>
{/* BMI Scale */}
<div className="mt-6 p-3 sm:p-4 bg-gray-50 rounded-lg">
<h3 className="text-sm font-medium text-gray-700 mb-3">{t("bmi.scale.title")}</h3>
<div className="space-y-2 text-xs sm:text-sm">
<div className="flex justify-between items-center">
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
<h3 className="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3">
{t("bmi.scale.title")}
</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between items-center py-1.5 px-2 rounded hover:bg-white transition-colours">
<span className="text-blue-600 font-medium">{t("bmi.category.underweight")}</span>
<span className="text-gray-600">&lt; 18.5</span>
</div>
<div className="flex justify-between items-center">
<div className="flex justify-between items-center py-1.5 px-2 rounded hover:bg-white transition-colours">
<span className="text-green-600 font-medium">{t("bmi.category.normal")}</span>
<span className="text-gray-600">18.5 - 24.9</span>
</div>
<div className="flex justify-between items-center">
<span className="text-yellow-600 font-medium">
{t("bmi.category.overweight")}
</span>
<div className="flex justify-between items-center py-1.5 px-2 rounded hover:bg-white transition-colours">
<span className="text-yellow-600 font-medium">{t("bmi.category.overweight")}</span>
<span className="text-gray-600">25.0 - 29.9</span>
</div>
<div className="flex justify-between items-center">
<div className="flex justify-between items-center py-1.5 px-2 rounded hover:bg-white transition-colours">
<span className="text-red-600 font-medium">{t("bmi.category.obese")}</span>
<span className="text-gray-600">&ge; 30.0</span>
</div>
@@ -166,5 +199,6 @@ export default function BmiCalculator() {
)}
</div>
</div>
</div>
)
}
+24 -24
View File
@@ -1,11 +1,13 @@
import React from "react"
import { useTranslation } from "react-i18next"
/**
* Contact page component that encourages manual GitHub Issue submission.
*/
export default function Contact() {
const { t } = useTranslation()
const owner = "onixbyte"
const repo = "dev-hub"
const repo = "dev-lab"
const handleRedirect = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
@@ -27,7 +29,7 @@ ${message}
`.trim()
const githubUrl = `https://github.com/${owner}/${repo}/issues/new?title=${encodeURIComponent(
`[${type == "help wanted" ? "General Enquiry" : (type == "enhancement" ? "Feature request" : "Bug")}] ${subject}`
`[${type == "help wanted" ? t("contact.types.helpWanted") : (type == "enhancement" ? t("contact.types.enhancement") : t("contact.types.bug"))}] ${subject}`
)}&body=${encodeURIComponent(issueBody)}&labels=${type}`
// Open in a new tab so they don't lose their place on your site
@@ -38,9 +40,9 @@ ${message}
<div className="max-w-5xl mx-auto py-12 px-4 sm:px-6">
{/* Header */}
<div className="text-center mb-12">
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">Get in Touch</h1>
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("contact.title")}</h1>
<p className="mt-4 text-lg text-gray-600">
The best way to reach us is via our GitHub repository.
{t("contact.description")}
</p>
</div>
@@ -49,32 +51,30 @@ ${message}
<div className="space-y-8">
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
<span className="mr-2">🤝</span> Why GitHub Issues?
<span className="mr-2">🤝</span> {t("contact.whyGitHub.title")}
</h2>
<p className="text-gray-600 leading-relaxed">
We use GitHub to track all bug reports and feature requests. This ensures our
development process remains <span className="font-bold">transparent</span> and that
your feedback is properly prioritised by the community.
{t("contact.whyGitHub.description")}
</p>
</section>
<section className="bg-blue-50 border border-blue-100 p-6 rounded-2xl">
<h3 className="text-blue-900 font-bold mb-2">How it works:</h3>
<h3 className="text-blue-900 font-bold mb-2">{t("contact.howItWorks.title")}</h3>
<ol className="list-decimal list-inside text-blue-800 text-sm space-y-2">
<li>Fill in the enquiry details in the form provided.</li>
<li>{t("contact.howItWorks.step1")}</li>
<li>
Click <strong>"Prepare GitHub Issue"</strong>.
{t("contact.howItWorks.step2")}
</li>
<li>You will be redirected to GitHub with the data pre-filled.</li>
<li>{t("contact.howItWorks.step3")}</li>
<li>
Simply hit <strong>"Submit new issue"</strong> on their site.
{t("contact.howItWorks.step4")}
</li>
</ol>
</section>
<div className="pt-4 border-t border-gray-100">
<p className="text-sm text-gray-500 italic">
Alternatively, you can email us directly at: <br />
{t("contact.email")} <br />
<a
href="mailto:real@zihluwang.me"
className="text-blue-600 font-medium hover:underline">
@@ -89,7 +89,7 @@ ${message}
<form onSubmit={handleRedirect} className="space-y-5">
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-1">
Subject
{t("contact.form.subject")}
</label>
<input
type="text"
@@ -97,7 +97,7 @@ ${message}
name="subject"
required
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:bg-white outline-none transition-all"
placeholder="Bug report / Feature request"
placeholder={t("contact.form.subjectPlaceholder")}
/>
</div>
@@ -105,7 +105,7 @@ ${message}
<label
htmlFor="type"
className="block text-sm font-semibold text-gray-700 mb-1.5 ml-1">
Enquiry Type
{t("contact.form.type")}
</label>
<div className="relative group">
<select
@@ -113,9 +113,9 @@ ${message}
name="type"
required
className="w-full appearance-none px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-700 text-sm cursor-pointer transition-all duration-200 hover:border-gray-300 hover:bg-gray-100/50 focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 focus:bg-white outline-none">
<option value="help wanted">General Enquiry</option>
<option value="enhancement">Feature Request</option>
<option value="bug">Bug</option>
<option value="help wanted">{t("contact.types.helpWanted")}</option>
<option value="enhancement">{t("contact.types.enhancement")}</option>
<option value="bug">{t("contact.types.bug")}</option>
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 group-hover:text-gray-600 transition-colors">
@@ -128,13 +128,13 @@ ${message}
</div>
</div>
<p className="mt-2 ml-1 text-[11px] text-gray-400">
Selecting a type helps us categorise and prioritise your issue.
{t("contact.form.typeHelp")}
</p>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
Message Details
{t("contact.form.message")}
</label>
<textarea
id="message"
@@ -142,7 +142,7 @@ ${message}
rows={4}
required
className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:bg-white outline-none transition-all resize-none"
placeholder="Describe your enquiry here..."
placeholder={t("contact.form.messagePlaceholder")}
/>
</div>
@@ -152,7 +152,7 @@ ${message}
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.297 24 12.017c0-6.627-5.373-12-12-12" />
</svg>
Prepare GitHub Issue
{t("contact.form.submit")}
</button>
</form>
</div>
+20 -16
View File
@@ -1,63 +1,67 @@
import { Link } from "react-router-dom"
import { useTranslation } from "react-i18next"
/**
* Home page component that displays the main landing content.
*/
export default function Home() {
const { t } = useTranslation()
return (
<div className="space-y-8 max-w-6xl mx-auto">
{/* Page Header */}
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">DevLab</h1>
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("home.title")}</h1>
<p className="mt-4 text-lg text-gray-600">
A powerful, privacy-focused tool for debugging and visualising complex JSON data
structures.
{t("home.description")}
</p>
</div>
{/* Main CTA */}
{/* Main CTA - Two columns */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white shadow rounded-lg p-8 border border-gray-100 text-center">
<h2 className="text-2xl font-semibold text-gray-900 mb-4">Get Started</h2>
<h2 className="text-2xl font-semibold text-gray-900 mb-4">{t("home.getStarted")}</h2>
<p className="text-gray-600 mb-6">
Start visualising and querying your JSON data with our intuitive JSONPath-based tool.
{t("home.getStartedDescription")}
</p>
<Link
to="/json-viewer"
className="inline-flex items-center gap-2 px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colours font-medium">
Open JSON Viewer
{t("home.openJsonViewer")}
</Link>
</div>
<div className="bg-white shadow rounded-lg p-8 border border-gray-100 text-center">
<h2 className="text-2xl font-semibold text-gray-900 mb-4">Get Started</h2>
<h2 className="text-2xl font-semibold text-gray-900 mb-4">{t("home.getStarted")}</h2>
<p className="text-gray-600 mb-6">
BMI Calculator.
{t("home.bmiCalculatorDescription")}
</p>
<Link
to="/bmi-calculator"
className="inline-flex items-center gap-2 px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colours font-medium">
Open BMI Calculator
{t("home.openBmiCalculator")}
</Link>
</div>
</div>
{/* Features */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white shadow rounded-lg p-6 border border-gray-100">
<h3 className="text-lg font-semibold text-gray-900 mb-2">🔍 JSONPath Queries</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t("home.features.tools.title")}</h3>
<p className="text-gray-600 text-sm">
Use powerful JSONPath expressions to query and filter your JSON data structures.
{t("home.features.tools.description")}
</p>
</div>
<div className="bg-white shadow rounded-lg p-6 border border-gray-100">
<h3 className="text-lg font-semibold text-gray-900 mb-2">🎨 Visual Highlighting</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t("home.features.privacy.title")}</h3>
<p className="text-gray-600 text-sm">
See matching paths highlighted in real-time as you type your queries.
{t("home.features.privacy.description")}
</p>
</div>
<div className="bg-white shadow rounded-lg p-6 border border-gray-100">
<h3 className="text-lg font-semibold text-gray-900 mb-2">🔒 Privacy First</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t("home.features.free.title")}</h3>
<p className="text-gray-600 text-sm">
All processing happens locally in your browser. No data is ever sent to servers.
{t("home.features.free.description")}
</p>
</div>
</div>
+10 -8
View File
@@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import jp from "jsonpath"
import JsonTreeNode from "@/components/json-tree-node"
@@ -6,6 +7,7 @@ import JsonTreeNode from "@/components/json-tree-node"
* JSON Viewer page component that displays the JSON visualisation tool in DevLab.
*/
export default function JsonViewer() {
const { t } = useTranslation()
const initialData = {
centre_id: "LON-01",
location: "London",
@@ -78,7 +80,7 @@ export default function JsonViewer() {
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex-1 flex flex-col min-h-0">
<div className="bg-slate-50 px-4 py-2 border-b border-slate-200 shrink-0">
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
{t("jsonViewer.jsonSource")}
</span>
</div>
<textarea
@@ -92,9 +94,9 @@ export default function JsonViewer() {
{/* JSONPath Expression - fixed height */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 shrink-0">
<label className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider mb-2">
<span className="text-slate-500">JSONPath Expression</span>
<span className="text-slate-500">{t("jsonViewer.jsonPathExpression")}</span>
{result.queryError && (
<span className="text-red-500 normal-case"> Invalid syntax</span>
<span className="text-red-500 normal-case">{t("jsonViewer.invalidSyntax")}</span>
)}
</label>
<input
@@ -106,7 +108,7 @@ export default function JsonViewer() {
}`}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="e.g. $..roles"
placeholder={t("jsonViewer.placeholder")}
/>
</div>
</div>
@@ -115,18 +117,18 @@ export default function JsonViewer() {
<div className="w-[70%] bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden min-h-0">
<div className="bg-slate-50 px-4 py-2 border-b border-slate-200 flex justify-between items-center shrink-0">
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
Visualised Result
{t("jsonViewer.visualisedResult")}
</span>
<div className="flex items-center gap-2">
<span className="text-xs font-medium px-2 py-0.5 bg-indigo-100 text-indigo-700 rounded-full">
{result.matchedPaths.length} matches
{result.matchedPaths.length} {t("jsonViewer.matches")}
</span>
<button
onClick={copyAsCsv}
disabled={result.matchedValues.length === 0 || !!result.error}
className="text-xs font-medium px-3 py-1 bg-emerald-500 text-white rounded-lg hover:bg-emerald-600 disabled:bg-slate-300 disabled:cursor-not-allowed transition-colours"
>
{copied ? "Copied!" : "Copy as CSV"}
{copied ? t("jsonViewer.copied") : t("jsonViewer.copyAsCsv")}
</button>
</div>
</div>
@@ -134,7 +136,7 @@ export default function JsonViewer() {
<div className="flex-1 p-6 overflow-auto font-mono text-sm leading-relaxed min-h-0">
{result.error && (
<div className="bg-red-50 text-red-600 p-4 rounded-lg border border-red-100 text-xs mb-4">
<strong>Error:</strong> {result.error}
<strong>{t("jsonViewer.error")}</strong> {result.error}
</div>
)}
{result.parsed && (