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:
+14
-20
@@ -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>
|
||||
|
||||
+135
-101
@@ -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,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 (
|
||||
<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 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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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">
|
||||
{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 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-sm text-gray-700 max-w-md mx-auto">
|
||||
{t(`bmi.advice.${bmiCategory}`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Height Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 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 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-base"
|
||||
min="50"
|
||||
max="300"
|
||||
/>
|
||||
<span className="absolute right-3 top-3 text-gray-500 text-sm">cm</span>
|
||||
{/* BMI Scale */}
|
||||
<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">< 18.5</span>
|
||||
</div>
|
||||
<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 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 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">≥ 30.0</span>
|
||||
</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>
|
||||
|
||||
{/* 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">< 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">≥ 30.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+24
-24
@@ -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>
|
||||
|
||||
+35
-31
@@ -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 */}
|
||||
<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>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Start visualising and querying your JSON data with our intuitive JSONPath-based tool.
|
||||
</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
|
||||
</Link>
|
||||
</div>
|
||||
{/* 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">{t("home.getStarted")}</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{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">
|
||||
{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>
|
||||
<p className="text-gray-600 mb-6">
|
||||
BMI Calculator.
|
||||
</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
|
||||
</Link>
|
||||
<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">{t("home.getStarted")}</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{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">
|
||||
{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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user