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": { "app": {
"title": "DevHub", "title": "DevLab",
"pageTitle": "DevHub", "pageTitle": "DevLab",
"copyright": "© {{year}} OnixByte. Built with React & TypeScript." "copyright": "© {{year}} OnixByte. Built with React & TypeScript."
}, },
"navigation": { "navigation": {
"home": "Home" "home": "Home",
"about": "About",
"contact": "Contact"
}, },
"language": { "language": {
"switch": "Switch Language", "switch": "Switch Language",
"english": "English (Great Britain)", "english": "English (Great Britain)",
"chinese": "简体中文" "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": { "bmi": {
"title": "BMI Calculator", "title": "BMI Calculator",
"description": "Calculate your Body Mass Index (BMI) to assess your weight status and health.", "description": "Calculate your Body Mass Index (BMI) to assess your weight status and health.",
@@ -26,7 +109,8 @@
"calculate": "Calculate BMI", "calculate": "Calculate BMI",
"reset": "Reset", "reset": "Reset",
"result": { "result": {
"title": "Your BMI Result" "title": "Your BMI Result",
"emptyState": "Enter your weight and height to calculate your BMI"
}, },
"category": { "category": {
"underweight": "Underweight", "underweight": "Underweight",
+88 -4
View File
@@ -1,17 +1,100 @@
{ {
"app": { "app": {
"title": "DevHub", "title": "DevLab",
"pageTitle": "DevHub", "pageTitle": "DevLab",
"copyright": "© {{year}} OnixByte。 使用 React 和 TypeScript 构建。" "copyright": "© {{year}} OnixByte。 使用 React 和 TypeScript 构建。"
}, },
"navigation": { "navigation": {
"home": "首页" "home": "首页",
"about": "关于",
"contact": "联系"
}, },
"language": { "language": {
"switch": "切换语言", "switch": "切换语言",
"english": "English (Great Britain)", "english": "English (Great Britain)",
"chinese": "简体中文" "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": { "bmi": {
"title": "BMI 计算器", "title": "BMI 计算器",
"description": "计算您的身体质量指数(BMI)以评估您的体重状态和健康状况。", "description": "计算您的身体质量指数(BMI)以评估您的体重状态和健康状况。",
@@ -26,7 +109,8 @@
"calculate": "计算 BMI", "calculate": "计算 BMI",
"reset": "重置", "reset": "重置",
"result": { "result": {
"title": "您的 BMI 结果" "title": "您的 BMI 结果",
"emptyState": "请输入您的体重和身高以计算 BMI"
}, },
"category": { "category": {
"underweight": "体重过轻", "underweight": "体重过轻",
+28 -22
View File
@@ -1,6 +1,8 @@
import { Outlet, Link, useLocation } from "react-router-dom" import { Outlet, Link, useLocation } from "react-router-dom"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import dayjs from "dayjs" import dayjs from "dayjs"
import LanguageSwitcher from "@/components/language-switcher"
/** /**
* Main application component that serves as the root layout. * Main application component that serves as the root layout.
@@ -8,6 +10,7 @@ import dayjs from "dayjs"
*/ */
export default function HeroLayout() { export default function HeroLayout() {
const today = useMemo(() => dayjs(), []) const today = useMemo(() => dayjs(), [])
const { t } = useTranslation()
return ( return (
<div className="h-screen bg-gray-50 flex flex-col overflow-hidden"> <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 justify-between items-center h-16">
<div className="flex items-center"> <div className="flex items-center">
<h1 className="text-xl font-semibold text-gray-900"> <h1 className="text-xl font-semibold text-gray-900">
DevLab {t("app.title")}
</h1> </h1>
</div> </div>
<nav className="flex space-x-8"> <div className="flex items-center gap-4">
<Link <nav className="flex space-x-8">
to="/" <Link
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium" to="/"
> className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
Home >
</Link> {t("navigation.home")}
<Link </Link>
to="/about" <Link
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium" to="/about"
> className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
About >
</Link> {t("navigation.about")}
<Link </Link>
to="/contact" <Link
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium" to="/contact"
> className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
Contact >
</Link> {t("navigation.contact")}
</nav> </Link>
</nav>
<LanguageSwitcher />
</div>
</div> </div>
</div> </div>
</header> </header>
@@ -53,7 +59,7 @@ export default function HeroLayout() {
<footer className="bg-white border-t shrink-0"> <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"> <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"> <p className="text-center text-sm text-gray-500">
© 2024-{today.year()} OnixByte. Built with React & TypeScript. {t("app.copyright", { year: today.year() })}
</p> </p>
</div> </div>
</footer> </footer>
+14 -20
View File
@@ -1,31 +1,33 @@
import { useTranslation } from "react-i18next"
/** /**
* About page component that displays information about the DevLab application. * About page component that displays information about the DevLab application.
*/ */
export default function About() { export default function About() {
const { t } = useTranslation()
return ( return (
<div className="space-y-8 max-w-6xl mx-auto"> <div className="space-y-8 max-w-6xl mx-auto">
{/* Page Header */} {/* Page Header */}
<div className="text-center"> <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"> <p className="mt-4 text-lg text-gray-600">
A powerful, privacy-focused tool for debugging and visualising complex JSON data {t("about.description")}
structures.
</p> </p>
</div> </div>
{/* GitHub & Deployment */} {/* GitHub & Deployment */}
<div className="bg-white shadow rounded-lg p-6 border border-gray-100"> <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"> <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> </h2>
<div className="space-y-4 text-gray-700"> <div className="space-y-4 text-gray-700">
<p> <p>
This project is open source and available on GitHub. You can view the source code, {t("about.openSource.description")}
contribute, or deploy your own instance.
</p> </p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<a <a
href="https://github.com/onixbyte/json-visualiser" href="https://github.com/onixbyte/dev-lab"
target="_blank" target="_blank"
rel="noopener noreferrer" 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"> 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" clipRule="evenodd"
/> />
</svg> </svg>
View on GitHub {t("about.openSource.viewOnGitHub")}
</a> </a>
</div> </div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4"> <div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-amber-900"> <p className="text-amber-900">
<strong> Star us on GitHub:</strong> If you find this project helpful, we'd greatly <strong>{t("about.openSource.starUs")}</strong> {t("about.openSource.starUsDescription")}
appreciate it if you could give us a star on GitHub. It helps others discover the
project and motivates us to keep improving!
</p> </p>
</div> </div>
<div className="bg-blue-50 border border-blue-100 rounded-lg p-4 mt-4"> <div className="bg-blue-50 border border-blue-100 rounded-lg p-4 mt-4">
<p className="text-blue-900"> <p className="text-blue-900">
<strong>💡 Self-Hosting:</strong> You can deploy this application yourself! The <strong>{t("about.openSource.selfHosting")}</strong> {t("about.openSource.selfHostingDescription")}
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).
</p> </p>
</div> </div>
</div> </div>
@@ -60,13 +57,10 @@ export default function About() {
{/* Privacy Commitment */} {/* Privacy Commitment */}
<div className="bg-indigo-50 border border-indigo-100 rounded-lg p-6 text-indigo-900"> <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"> <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> </h2>
<p className="leading-relaxed"> <p className="leading-relaxed">
We believe that your data belongs to you. This application is designed as a{" "} {t("about.privacy.description")}
<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.
</p> </p>
</div> </div>
</div> </div>
+135 -101
View File
@@ -2,7 +2,7 @@ import { useState } from "react"
import { useTranslation } from "react-i18next" 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() { export default function BmiCalculator() {
const { t } = useTranslation() const { t } = useTranslation()
@@ -55,115 +55,149 @@ export default function BmiCalculator() {
} }
} }
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 ( return (
<div className="w-full max-w-3xl mx-auto p-4 sm:p-6"> <div className="h-full flex gap-4 overflow-hidden">
<div className="w-full space-y-6"> {/* Left panel - 30% */}
{/* Header */} <div className="w-[30%] flex flex-col gap-4 min-h-0">
<div className="text-center"> {/* Input Form - fills remaining height */}
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">{t("bmi.title")}</h1> <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex-1 flex flex-col min-h-0">
<p className="text-sm sm:text-base text-gray-600">{t("bmi.description")}</p> <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>
{/* Weight Input */}
<div>
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">
{t("bmi.weight.label")}
</label>
<div className="relative">
<input
type="number"
value={weight}
onChange={(e) => setWeight(e.target.value)}
placeholder={t("bmi.weight.placeholder")}
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-xs">kg</span>
</div>
</div>
{/* Height Input */}
<div>
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-2">
{t("bmi.height.label")}
</label>
<div className="relative">
<input
type="number"
value={height}
onChange={(e) => setHeight(e.target.value)}
placeholder={t("bmi.height.placeholder")}
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-xs">cm</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col gap-2 pt-2">
<button
onClick={calculateBMI}
disabled={!weight || !height}
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="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>
{/* 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")}
</span>
</div> </div>
{/* Calculator Form */} <div className="flex-1 p-6 overflow-auto min-h-0">
<div className="bg-white rounded-lg shadow-md p-4 sm:p-6 w-full mx-auto"> {bmi === null ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 mb-6"> <div className="flex items-center justify-center h-full">
{/* Weight Input */} <div className="text-center text-gray-400">
<div> <p className="text-sm">{t("bmi.description")}</p>
<label className="block text-sm font-medium text-gray-700 mb-2"> <p className="text-xs mt-2">{t("bmi.result.emptyState")}</p>
{t("bmi.weight.label")}
</label>
<div className="relative">
<input
type="number"
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"
min="1"
max="500"
/>
<span className="absolute right-3 top-3 text-gray-500 text-sm">kg</span>
</div> </div>
</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-sm text-gray-700 max-w-md mx-auto">
{t(`bmi.advice.${bmiCategory}`)}
</div>
</div>
{/* Height Input */} {/* BMI Scale */}
<div> <div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
<label className="block text-sm font-medium text-gray-700 mb-2"> <h3 className="text-xs font-semibold uppercase tracking-wider text-slate-500 mb-3">
{t("bmi.height.label")} {t("bmi.scale.title")}
</label> </h3>
<div className="relative"> <div className="space-y-2 text-sm">
<input <div className="flex justify-between items-center py-1.5 px-2 rounded hover:bg-white transition-colours">
type="number" <span className="text-blue-600 font-medium">{t("bmi.category.underweight")}</span>
value={height} <span className="text-gray-600">&lt; 18.5</span>
onChange={(e) => setHeight(e.target.value)} </div>
placeholder={t("bmi.height.placeholder")} <div className="flex justify-between items-center py-1.5 px-2 rounded hover:bg-white transition-colours">
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" <span className="text-green-600 font-medium">{t("bmi.category.normal")}</span>
min="50" <span className="text-gray-600">18.5 - 24.9</span>
max="300" </div>
/> <div className="flex justify-between items-center py-1.5 px-2 rounded hover:bg-white transition-colours">
<span className="absolute right-3 top-3 text-gray-500 text-sm">cm</span> <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 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>
</div>
</div> </div>
</div> </div>
</div> )}
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
<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">
{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">
{t("bmi.reset")}
</button>
</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">
{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()}`}>
{t(`bmi.category.${bmiCategory}`)}
</div>
<div className="text-gray-600 text-sm sm:text-base px-4">
{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">
<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">
<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>
<span className="text-gray-600">25.0 - 29.9</span>
</div>
<div className="flex justify-between items-center">
<span className="text-red-600 font-medium">{t("bmi.category.obese")}</span>
<span className="text-gray-600">&ge; 30.0</span>
</div>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
) )
+24 -24
View File
@@ -1,11 +1,13 @@
import React from "react" import React from "react"
import { useTranslation } from "react-i18next"
/** /**
* Contact page component that encourages manual GitHub Issue submission. * Contact page component that encourages manual GitHub Issue submission.
*/ */
export default function Contact() { export default function Contact() {
const { t } = useTranslation()
const owner = "onixbyte" const owner = "onixbyte"
const repo = "dev-hub" const repo = "dev-lab"
const handleRedirect = (e: React.FormEvent<HTMLFormElement>) => { const handleRedirect = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
@@ -27,7 +29,7 @@ ${message}
`.trim() `.trim()
const githubUrl = `https://github.com/${owner}/${repo}/issues/new?title=${encodeURIComponent( 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}` )}&body=${encodeURIComponent(issueBody)}&labels=${type}`
// Open in a new tab so they don't lose their place on your site // 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"> <div className="max-w-5xl mx-auto py-12 px-4 sm:px-6">
{/* Header */} {/* Header */}
<div className="text-center mb-12"> <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"> <p className="mt-4 text-lg text-gray-600">
The best way to reach us is via our GitHub repository. {t("contact.description")}
</p> </p>
</div> </div>
@@ -49,32 +51,30 @@ ${message}
<div className="space-y-8"> <div className="space-y-8">
<section> <section>
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center"> <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> </h2>
<p className="text-gray-600 leading-relaxed"> <p className="text-gray-600 leading-relaxed">
We use GitHub to track all bug reports and feature requests. This ensures our {t("contact.whyGitHub.description")}
development process remains <span className="font-bold">transparent</span> and that
your feedback is properly prioritised by the community.
</p> </p>
</section> </section>
<section className="bg-blue-50 border border-blue-100 p-6 rounded-2xl"> <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"> <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> <li>
Click <strong>"Prepare GitHub Issue"</strong>. {t("contact.howItWorks.step2")}
</li> </li>
<li>You will be redirected to GitHub with the data pre-filled.</li> <li>{t("contact.howItWorks.step3")}</li>
<li> <li>
Simply hit <strong>"Submit new issue"</strong> on their site. {t("contact.howItWorks.step4")}
</li> </li>
</ol> </ol>
</section> </section>
<div className="pt-4 border-t border-gray-100"> <div className="pt-4 border-t border-gray-100">
<p className="text-sm text-gray-500 italic"> <p className="text-sm text-gray-500 italic">
Alternatively, you can email us directly at: <br /> {t("contact.email")} <br />
<a <a
href="mailto:real@zihluwang.me" href="mailto:real@zihluwang.me"
className="text-blue-600 font-medium hover:underline"> className="text-blue-600 font-medium hover:underline">
@@ -89,7 +89,7 @@ ${message}
<form onSubmit={handleRedirect} className="space-y-5"> <form onSubmit={handleRedirect} className="space-y-5">
<div> <div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-1">
Subject {t("contact.form.subject")}
</label> </label>
<input <input
type="text" type="text"
@@ -97,7 +97,7 @@ ${message}
name="subject" name="subject"
required 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" 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> </div>
@@ -105,7 +105,7 @@ ${message}
<label <label
htmlFor="type" htmlFor="type"
className="block text-sm font-semibold text-gray-700 mb-1.5 ml-1"> className="block text-sm font-semibold text-gray-700 mb-1.5 ml-1">
Enquiry Type {t("contact.form.type")}
</label> </label>
<div className="relative group"> <div className="relative group">
<select <select
@@ -113,9 +113,9 @@ ${message}
name="type" name="type"
required 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"> 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="help wanted">{t("contact.types.helpWanted")}</option>
<option value="enhancement">Feature Request</option> <option value="enhancement">{t("contact.types.enhancement")}</option>
<option value="bug">Bug</option> <option value="bug">{t("contact.types.bug")}</option>
</select> </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"> <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>
</div> </div>
<p className="mt-2 ml-1 text-[11px] text-gray-400"> <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> </p>
</div> </div>
<div> <div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
Message Details {t("contact.form.message")}
</label> </label>
<textarea <textarea
id="message" id="message"
@@ -142,7 +142,7 @@ ${message}
rows={4} rows={4}
required 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" 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> </div>
@@ -152,7 +152,7 @@ ${message}
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24"> <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" /> <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> </svg>
Prepare GitHub Issue {t("contact.form.submit")}
</button> </button>
</form> </form>
</div> </div>
+35 -31
View File
@@ -1,63 +1,67 @@
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { useTranslation } from "react-i18next"
/** /**
* Home page component that displays the main landing content. * Home page component that displays the main landing content.
*/ */
export default function Home() { export default function Home() {
const { t } = useTranslation()
return ( return (
<div className="space-y-8 max-w-6xl mx-auto"> <div className="space-y-8 max-w-6xl mx-auto">
{/* Page Header */} {/* Page Header */}
<div className="text-center"> <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"> <p className="mt-4 text-lg text-gray-600">
A powerful, privacy-focused tool for debugging and visualising complex JSON data {t("home.description")}
structures.
</p> </p>
</div> </div>
{/* Main CTA */} {/* Main CTA - Two columns */}
<div className="bg-white shadow rounded-lg p-8 border border-gray-100 text-center"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<h2 className="text-2xl font-semibold text-gray-900 mb-4">Get Started</h2> <div className="bg-white shadow rounded-lg p-8 border border-gray-100 text-center">
<p className="text-gray-600 mb-6"> <h2 className="text-2xl font-semibold text-gray-900 mb-4">{t("home.getStarted")}</h2>
Start visualising and querying your JSON data with our intuitive JSONPath-based tool. <p className="text-gray-600 mb-6">
</p> {t("home.getStartedDescription")}
<Link </p>
to="/json-viewer" <Link
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"> to="/json-viewer"
Open 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">
</Link> {t("home.openJsonViewer")}
</div> </Link>
</div>
<div className="bg-white shadow rounded-lg p-8 border border-gray-100 text-center"> <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"> <p className="text-gray-600 mb-6">
BMI Calculator. {t("home.bmiCalculatorDescription")}
</p> </p>
<Link <Link
to="/bmi-calculator" 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"> 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> </Link>
</div>
</div> </div>
{/* Features */} {/* Features */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <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"> <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"> <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> </p>
</div> </div>
<div className="bg-white shadow rounded-lg p-6 border border-gray-100"> <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"> <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> </p>
</div> </div>
<div className="bg-white shadow rounded-lg p-6 border border-gray-100"> <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"> <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> </p>
</div> </div>
</div> </div>
+10 -8
View File
@@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from "react" import { useCallback, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import jp from "jsonpath" import jp from "jsonpath"
import JsonTreeNode from "@/components/json-tree-node" 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. * JSON Viewer page component that displays the JSON visualisation tool in DevLab.
*/ */
export default function JsonViewer() { export default function JsonViewer() {
const { t } = useTranslation()
const initialData = { const initialData = {
centre_id: "LON-01", centre_id: "LON-01",
location: "London", 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-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"> <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"> <span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
{t("jsonViewer.jsonSource")}
</span> </span>
</div> </div>
<textarea <textarea
@@ -92,9 +94,9 @@ export default function JsonViewer() {
{/* JSONPath Expression - fixed height */} {/* JSONPath Expression - fixed height */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 shrink-0"> <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"> <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 && ( {result.queryError && (
<span className="text-red-500 normal-case"> Invalid syntax</span> <span className="text-red-500 normal-case">{t("jsonViewer.invalidSyntax")}</span>
)} )}
</label> </label>
<input <input
@@ -106,7 +108,7 @@ export default function JsonViewer() {
}`} }`}
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="e.g. $..roles" placeholder={t("jsonViewer.placeholder")}
/> />
</div> </div>
</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="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"> <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"> <span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
Visualised Result {t("jsonViewer.visualisedResult")}
</span> </span>
<div className="flex items-center gap-2"> <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"> <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> </span>
<button <button
onClick={copyAsCsv} onClick={copyAsCsv}
disabled={result.matchedValues.length === 0 || !!result.error} 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" 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> </button>
</div> </div>
</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"> <div className="flex-1 p-6 overflow-auto font-mono text-sm leading-relaxed min-h-0">
{result.error && ( {result.error && (
<div className="bg-red-50 text-red-600 p-4 rounded-lg border border-red-100 text-xs mb-4"> <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> </div>
)} )}
{result.parsed && ( {result.parsed && (