updated for mobile view and dark theme
This commit is contained in:
12
index.html
12
index.html
@@ -6,6 +6,18 @@
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NearleXpress Developer Docs</title>
|
||||
<script>
|
||||
// Apply persisted theme before paint to avoid a flash of the wrong theme
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (stored === 'dark' || (!stored && prefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -67,7 +67,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1478,7 +1477,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@@ -2337,7 +2335,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -2694,7 +2691,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -2916,7 +2912,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
|
||||
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3405,7 +3400,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3531,7 +3525,6 @@
|
||||
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -3625,7 +3618,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
55
src/App.jsx
55
src/App.jsx
@@ -1,7 +1,9 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Menu, Layers } from 'lucide-react'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import Introduction from './components/Introduction'
|
||||
import TopicView from './components/TopicView'
|
||||
import ThemeToggle from './components/ThemeToggle'
|
||||
import { legacyTopics, restTopics, LEGACY_BASE_URL, REST_BASE_URL } from './data/topics'
|
||||
|
||||
const processTopics = (topics, baseUrl, type) =>
|
||||
@@ -26,31 +28,68 @@ function filterTopics(topics, q) {
|
||||
export default function App() {
|
||||
const [activeTopic, setActiveTopic] = useState(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
const visibleLegacy = useMemo(() => filterTopics(allLegacy, searchQuery.trim().toLowerCase()), [searchQuery])
|
||||
const visibleRest = useMemo(() => filterTopics(allRest, searchQuery.trim().toLowerCase()), [searchQuery])
|
||||
|
||||
// Picking a topic (or Introduction) should also close the drawer on mobile
|
||||
const selectTopic = (t) => {
|
||||
setActiveTopic(t)
|
||||
setSidebarOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 bg-grid-pattern flex relative overflow-hidden">
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-dark-900 bg-grid-pattern flex relative overflow-hidden transition-colors">
|
||||
|
||||
{/* Ambient background glows */}
|
||||
<div className="absolute top-0 -left-4 w-72 h-72 bg-brand-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob"></div>
|
||||
<div className="absolute top-0 -right-4 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-2000"></div>
|
||||
<div className="absolute -bottom-8 left-20 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-4000"></div>
|
||||
<div className="absolute top-0 -left-4 w-72 h-72 bg-brand-300 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-3xl opacity-20 dark:opacity-10 animate-blob"></div>
|
||||
<div className="absolute top-0 -right-4 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-3xl opacity-20 dark:opacity-10 animate-blob animation-delay-2000"></div>
|
||||
<div className="absolute -bottom-8 left-20 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-3xl opacity-20 dark:opacity-10 animate-blob animation-delay-4000"></div>
|
||||
|
||||
{/* Theme toggle (floating, top-right) */}
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Mobile top bar */}
|
||||
<header className="md:hidden fixed top-0 left-0 right-0 h-16 z-30 glass flex items-center gap-3 px-4">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
aria-label="Open navigation"
|
||||
className="w-10 h-10 flex items-center justify-center rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100/60 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
<button onClick={() => selectTopic(null)} className="flex items-center gap-2.5 text-slate-900 dark:text-slate-100 font-bold text-lg">
|
||||
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-brand-500 to-indigo-600 flex items-center justify-center shadow-glow">
|
||||
<Layers className="text-white w-4 h-4" />
|
||||
</div>
|
||||
<span className="tracking-tight">NearleXpress</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Mobile drawer backdrop */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="md:hidden fixed inset-0 z-30 bg-slate-900/40 dark:bg-black/60 backdrop-blur-sm animate-fade-in"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Sidebar
|
||||
legacyTopics={visibleLegacy}
|
||||
restTopics={visibleRest}
|
||||
activeTopic={activeTopic}
|
||||
setActiveTopic={setActiveTopic}
|
||||
setActiveTopic={selectTopic}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
isOpen={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
/>
|
||||
|
||||
<main className="ml-[280px] flex-1 min-h-screen relative z-10">
|
||||
<main className="md:ml-[280px] flex-1 min-h-screen relative z-10 pt-16 md:pt-0">
|
||||
{activeTopic
|
||||
? (
|
||||
<div className="max-w-[1200px] mx-auto p-12 lg:p-16 opacity-0 animate-fade-in-up">
|
||||
<div className="max-w-[1200px] mx-auto p-5 sm:p-8 lg:p-16 opacity-0 animate-fade-in-up">
|
||||
<TopicView topic={activeTopic} searchQuery={searchQuery} />
|
||||
</div>
|
||||
)
|
||||
@@ -59,7 +98,7 @@ export default function App() {
|
||||
<Introduction
|
||||
allLegacy={allLegacy}
|
||||
allRest={allRest}
|
||||
setActiveTopic={setActiveTopic}
|
||||
setActiveTopic={selectTopic}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -114,20 +114,20 @@ export default function EndpointCard({ endpoint, baseUrl, isLegacy, onSend, resu
|
||||
}
|
||||
|
||||
const methodColor = {
|
||||
GET: 'bg-emerald-100/50 text-emerald-700 border-emerald-200',
|
||||
POST: 'bg-blue-100/50 text-blue-700 border-blue-200',
|
||||
PUT: 'bg-amber-100/50 text-amber-700 border-amber-200',
|
||||
DELETE: 'bg-red-100/50 text-red-700 border-red-200',
|
||||
PATCH: 'bg-purple-100/50 text-purple-700 border-purple-200',
|
||||
}[endpoint.method] || 'bg-slate-100/50 text-slate-700 border-slate-200'
|
||||
GET: 'bg-emerald-100/50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20',
|
||||
POST: 'bg-blue-100/50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-500/20',
|
||||
PUT: 'bg-amber-100/50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-500/20',
|
||||
DELETE: 'bg-red-100/50 dark:bg-red-500/10 text-red-700 dark:text-red-400 border-red-200 dark:border-red-500/20',
|
||||
PATCH: 'bg-purple-100/50 dark:bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-200 dark:border-purple-500/20',
|
||||
}[endpoint.method] || 'bg-slate-100/50 dark:bg-slate-500/10 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-500/20'
|
||||
|
||||
const isOk = result?.kind === 'response' && result.ok
|
||||
const isErr = result?.kind === 'response' && !result.ok
|
||||
const isNet = result?.kind === 'network-error'
|
||||
|
||||
return (
|
||||
<div className="py-16 border-t border-slate-200/60 group/row" id={endpoint.name}>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-12 lg:gap-16">
|
||||
<div className="py-10 sm:py-16 border-t border-slate-200/60 dark:border-white/10 group/row" id={endpoint.name}>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6 sm:gap-12 lg:gap-16">
|
||||
|
||||
{/* Left Column: Description & Parameters */}
|
||||
<div className="xl:col-span-5 space-y-8">
|
||||
@@ -136,39 +136,39 @@ export default function EndpointCard({ endpoint, baseUrl, isLegacy, onSend, resu
|
||||
<span className={`px-2.5 py-1 rounded text-xs font-bold tracking-widest border ${methodColor}`}>
|
||||
{endpoint.method}
|
||||
</span>
|
||||
<h3 className="text-2xl font-bold text-slate-900 tracking-tight">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-slate-100 tracking-tight break-words">
|
||||
{endpoint.name}
|
||||
</h3>
|
||||
</div>
|
||||
{endpoint.description && (
|
||||
<p className="text-[15px] text-slate-600 leading-relaxed">{endpoint.description}</p>
|
||||
<p className="text-[15px] text-slate-600 dark:text-slate-400 leading-relaxed">{endpoint.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* URL bar */}
|
||||
<div className="bg-white border border-slate-200/80 rounded-xl p-3.5 font-mono text-sm shadow-sm flex items-start gap-2 group-hover/row:border-brand-300 transition-colors min-w-0">
|
||||
<div className="bg-white dark:bg-dark-800 border border-slate-200/80 dark:border-white/10 rounded-xl p-3.5 font-mono text-sm shadow-sm flex items-start gap-2 group-hover/row:border-brand-300 dark:group-hover/row:border-brand-500/40 transition-colors min-w-0">
|
||||
<Server size={14} className="text-brand-400 shrink-0 mt-0.5" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs text-slate-400 truncate">{baseUrl}</div>
|
||||
<div className="font-semibold text-slate-800 break-all">{path}</div>
|
||||
<div className="text-xs text-slate-400 dark:text-slate-500 truncate">{baseUrl}</div>
|
||||
<div className="font-semibold text-slate-800 dark:text-slate-200 break-all">{path}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Query Parameters */}
|
||||
{paramDefs.length > 0 && (
|
||||
<div className="pt-2">
|
||||
<h4 className="text-xs font-bold text-slate-400 mb-4 uppercase tracking-widest flex items-center gap-2">
|
||||
<span className="w-4 h-[1px] bg-slate-300"></span> Query Parameters
|
||||
<h4 className="text-xs font-bold text-slate-400 dark:text-slate-500 mb-4 uppercase tracking-widest flex items-center gap-2">
|
||||
<span className="w-4 h-[1px] bg-slate-300 dark:bg-white/10"></span> Query Parameters
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
{paramDefs.map((p) => (
|
||||
<div key={p.name} className="flex flex-col gap-2">
|
||||
<label className="text-sm font-semibold text-slate-800 flex justify-between items-center">
|
||||
<label className="text-sm font-semibold text-slate-800 dark:text-slate-200 flex justify-between items-center">
|
||||
{p.name}
|
||||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-mono">{p.type || 'string'}</span>
|
||||
<span className="text-[10px] text-slate-400 dark:text-slate-500 uppercase tracking-wider font-mono">{p.type || 'string'}</span>
|
||||
</label>
|
||||
<input
|
||||
className="w-full px-3.5 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 transition-all shadow-sm text-slate-700"
|
||||
className="w-full px-3.5 py-2.5 bg-white dark:bg-dark-800 border border-slate-200 dark:border-white/10 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 transition-all shadow-sm text-slate-700 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500"
|
||||
value={values[p.name] ?? ''}
|
||||
placeholder={`Enter ${p.name}...`}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -18,32 +18,32 @@ export default function Introduction({ allLegacy, allRest, setActiveTopic }) {
|
||||
const allTopics = [...allLegacy, ...allRest]
|
||||
|
||||
return (
|
||||
<div className="max-w-[1000px] mx-auto px-12 py-20 lg:px-24 lg:py-28">
|
||||
<div className="max-w-[1000px] mx-auto px-5 py-12 sm:px-12 sm:py-20 lg:px-24 lg:py-28">
|
||||
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-50 border border-brand-100 text-brand-600 text-sm font-medium mb-8">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-50 dark:bg-brand-500/10 border border-brand-100 dark:border-brand-500/20 text-brand-600 dark:text-brand-400 text-sm font-medium mb-8">
|
||||
<Zap size={14} className="fill-current" />
|
||||
v1.0 Developer API
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-slate-900 mb-6 tracking-tight">
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-slate-900 dark:text-slate-100 mb-6 tracking-tight">
|
||||
<span className="text-gradient">NearleXpress API</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-slate-600 mb-12 max-w-2xl leading-relaxed">
|
||||
<p className="text-lg sm:text-xl text-slate-600 dark:text-slate-400 mb-12 max-w-2xl leading-relaxed">
|
||||
A comprehensive platform for managing tenants, users, partners, customers,
|
||||
orders, deliveries, products, invoices, and payments across the Express network.
|
||||
</p>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest text-slate-400 mb-4">Base URLs</h2>
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500 mb-4">Base URLs</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-10">
|
||||
<div className="bg-white border border-slate-200/60 rounded-xl p-4 shadow-sm">
|
||||
<div className="text-[11px] text-brand-600 font-bold uppercase tracking-widest mb-1.5">GraphQL API</div>
|
||||
<code className="font-mono text-sm text-slate-700">{LEGACY_BASE_URL}</code>
|
||||
<div className="bg-white dark:bg-dark-800 border border-slate-200/60 dark:border-white/10 rounded-xl p-4 shadow-sm">
|
||||
<div className="text-[11px] text-brand-600 dark:text-brand-400 font-bold uppercase tracking-widest mb-1.5">GraphQL API</div>
|
||||
<code className="font-mono text-sm text-slate-700 dark:text-slate-300 break-all">{LEGACY_BASE_URL}</code>
|
||||
</div>
|
||||
<div className="bg-white border border-slate-200/60 rounded-xl p-4 shadow-sm">
|
||||
<div className="text-[11px] text-indigo-600 font-bold uppercase tracking-widest mb-1.5">REST API</div>
|
||||
<code className="font-mono text-sm text-slate-700">{REST_BASE_URL}</code>
|
||||
<div className="bg-white dark:bg-dark-800 border border-slate-200/60 dark:border-white/10 rounded-xl p-4 shadow-sm">
|
||||
<div className="text-[11px] text-indigo-600 dark:text-indigo-400 font-bold uppercase tracking-widest mb-1.5">REST API</div>
|
||||
<code className="font-mono text-sm text-slate-700 dark:text-slate-300 break-all">{REST_BASE_URL}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,14 +55,14 @@ export default function Introduction({ allLegacy, allRest, setActiveTopic }) {
|
||||
<div
|
||||
key={topic.uniqueId}
|
||||
onClick={() => setActiveTopic(topic)}
|
||||
className="group relative bg-white p-6 rounded-2xl border border-slate-200/60 shadow-sm hover:shadow-xl hover:shadow-brand-500/5 hover:border-brand-200 transition-all duration-300 cursor-pointer overflow-hidden"
|
||||
className="group relative bg-white dark:bg-dark-800 p-6 rounded-2xl border border-slate-200/60 dark:border-white/10 shadow-sm hover:shadow-xl hover:shadow-brand-500/5 dark:hover:shadow-brand-500/10 hover:border-brand-200 dark:hover:border-brand-500/30 transition-all duration-300 cursor-pointer overflow-hidden"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-brand-50 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-brand-50 dark:from-brand-500/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-2 group-hover:text-brand-600 transition-colors flex items-center gap-2">
|
||||
<div className="p-1.5 bg-brand-50 text-brand-600 rounded-md group-hover:bg-brand-100 transition-colors">
|
||||
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2 group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors flex items-center gap-2">
|
||||
<div className="p-1.5 bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 rounded-md group-hover:bg-brand-100 dark:group-hover:bg-brand-500/20 transition-colors">
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
{topic.name}
|
||||
@@ -71,14 +71,14 @@ export default function Introduction({ allLegacy, allRest, setActiveTopic }) {
|
||||
<div className="flex items-center gap-2 mt-1 mb-2">
|
||||
<span className={`text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded ${
|
||||
topic.type === 'legacy'
|
||||
? 'bg-brand-50 text-brand-600 border border-brand-100'
|
||||
: 'bg-indigo-50 text-indigo-600 border border-indigo-100'
|
||||
? 'bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 border border-brand-100 dark:border-brand-500/20'
|
||||
: 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-500/20'
|
||||
}`}>
|
||||
{topic.type === 'legacy' ? 'GraphQL' : 'REST'}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-400">{topic.endpoints.length} endpoint{topic.endpoints.length !== 1 ? 's' : ''}</span>
|
||||
<span className="text-[11px] text-slate-400 dark:text-slate-500">{topic.endpoints.length} endpoint{topic.endpoints.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 leading-relaxed">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{topic.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,7 +87,7 @@ export default function Introduction({ allLegacy, allRest, setActiveTopic }) {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-16 flex items-center gap-4 p-6 bg-slate-900 text-slate-300 rounded-2xl shadow-code">
|
||||
<div className="mt-16 flex items-center gap-4 p-6 bg-slate-900 dark:bg-dark-700 dark:border dark:border-white/10 text-slate-300 rounded-2xl shadow-code">
|
||||
<Code2 className="text-brand-400 shrink-0" size={24} />
|
||||
<p className="text-sm">
|
||||
Explore interactive endpoints for both GraphQL and REST APIs. All requests route through the dev proxy which injects authentication headers securely.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { Search, ChevronRight, ChevronDown, Layers, Terminal } from 'lucide-react'
|
||||
import { Search, ChevronRight, ChevronDown, Layers, Terminal, X } from 'lucide-react'
|
||||
import { getTopicIcon } from '../lib/icons'
|
||||
|
||||
export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActiveTopic, searchQuery, setSearchQuery }) {
|
||||
export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActiveTopic, searchQuery, setSearchQuery, isOpen = false, onClose }) {
|
||||
const [open, setOpen] = useState({ general: true, legacy: true, rest: true })
|
||||
const toggle = (k) => setOpen((s) => ({ ...s, [k]: !s[k] }))
|
||||
|
||||
@@ -10,12 +10,12 @@ export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActi
|
||||
<div>
|
||||
<button
|
||||
onClick={() => toggle(key)}
|
||||
className="w-full flex items-center justify-between px-2 py-1.5 text-slate-800 font-semibold text-sm hover:text-brand-600 transition-colors group"
|
||||
className="w-full flex items-center justify-between px-2 py-1.5 text-slate-800 dark:text-slate-200 font-semibold text-sm hover:text-brand-600 transition-colors group"
|
||||
>
|
||||
<span className="tracking-wide text-xs uppercase text-slate-400 group-hover:text-brand-500 transition-colors">
|
||||
<span className="tracking-wide text-xs uppercase text-slate-400 dark:text-slate-500 group-hover:text-brand-500 transition-colors">
|
||||
{title}
|
||||
</span>
|
||||
{open[key] ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
||||
{open[key] ? <ChevronDown size={14} className="text-slate-400 dark:text-slate-500" /> : <ChevronRight size={14} className="text-slate-400 dark:text-slate-500" />}
|
||||
</button>
|
||||
<div className={`mt-2 space-y-1 overflow-hidden transition-all duration-500 ${open[key] ? 'max-h-[800px] opacity-100' : 'max-h-0 opacity-0'}`}>
|
||||
{topics.map((t) => {
|
||||
@@ -27,13 +27,13 @@ export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActi
|
||||
onClick={() => setActiveTopic(t)}
|
||||
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-all duration-200 group ${
|
||||
isActive
|
||||
? 'text-brand-700 bg-brand-50 shadow-sm font-medium'
|
||||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-100/50'
|
||||
? 'text-brand-700 dark:text-brand-300 bg-brand-50 dark:bg-brand-500/10 shadow-sm font-medium'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-100/50 dark:hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
size={16}
|
||||
className={`${isActive ? 'text-brand-500' : 'text-slate-400 group-hover:text-slate-500'} transition-colors`}
|
||||
className={`${isActive ? 'text-brand-500' : 'text-slate-400 dark:text-slate-500 group-hover:text-slate-500 dark:group-hover:text-slate-400'} transition-colors`}
|
||||
/>
|
||||
{t.name}
|
||||
</button>
|
||||
@@ -44,21 +44,35 @@ export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActi
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-[280px] glass h-screen fixed top-0 left-0 flex flex-col z-20 transition-all">
|
||||
<div
|
||||
className={`w-[280px] glass h-screen fixed top-0 left-0 flex flex-col z-40 transition-transform duration-300 ease-out ${
|
||||
isOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
} md:translate-x-0`}
|
||||
>
|
||||
|
||||
{/* Brand Header */}
|
||||
<div className="p-6 border-b border-slate-100/50">
|
||||
<div className="flex items-center gap-3 text-slate-900 font-bold text-xl mb-6">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-brand-500 to-indigo-600 flex items-center justify-center shadow-glow">
|
||||
<Layers className="text-white w-4 h-4" />
|
||||
<div className="p-6 border-b border-slate-100/50 dark:border-white/5">
|
||||
<div className="flex items-center justify-between gap-3 text-slate-900 dark:text-slate-100 font-bold text-xl mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-brand-500 to-indigo-600 flex items-center justify-center shadow-glow">
|
||||
<Layers className="text-white w-4 h-4" />
|
||||
</div>
|
||||
<span className="tracking-tight">NearleXpress</span>
|
||||
</div>
|
||||
<span className="tracking-tight">NearleXpress</span>
|
||||
{/* Close button — mobile only */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close navigation"
|
||||
className="md:hidden w-8 h-8 -mr-1 flex items-center justify-center rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100/60 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative group">
|
||||
<Search className="w-4 h-4 absolute left-3 top-3 text-slate-400 group-focus-within:text-brand-500 transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
className="w-full pl-9 pr-3 py-2.5 bg-slate-50/50 border border-slate-200/60 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 focus:bg-white transition-all placeholder:text-slate-400"
|
||||
className="w-full pl-9 pr-3 py-2.5 bg-slate-50/50 dark:bg-white/5 border border-slate-200/60 dark:border-white/10 rounded-xl text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 focus:bg-white dark:focus:bg-white/5 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-500"
|
||||
placeholder="Search documentation..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
@@ -73,21 +87,21 @@ export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActi
|
||||
<div>
|
||||
<button
|
||||
onClick={() => toggle('general')}
|
||||
className="w-full flex items-center justify-between px-2 py-1.5 text-slate-800 font-semibold text-sm hover:text-brand-600 transition-colors group"
|
||||
className="w-full flex items-center justify-between px-2 py-1.5 text-slate-800 dark:text-slate-200 font-semibold text-sm hover:text-brand-600 transition-colors group"
|
||||
>
|
||||
<span className="tracking-wide text-xs uppercase text-slate-400 group-hover:text-brand-500 transition-colors">Getting Started</span>
|
||||
{open.general ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
||||
<span className="tracking-wide text-xs uppercase text-slate-400 dark:text-slate-500 group-hover:text-brand-500 transition-colors">Getting Started</span>
|
||||
{open.general ? <ChevronDown size={14} className="text-slate-400 dark:text-slate-500" /> : <ChevronRight size={14} className="text-slate-400 dark:text-slate-500" />}
|
||||
</button>
|
||||
<div className={`mt-2 space-y-1 overflow-hidden transition-all duration-300 ${open.general ? 'max-h-40 opacity-100' : 'max-h-0 opacity-0'}`}>
|
||||
<button
|
||||
onClick={() => setActiveTopic(null)}
|
||||
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-all duration-200 ${
|
||||
activeTopic === null
|
||||
? 'text-brand-700 bg-brand-50 shadow-sm font-medium'
|
||||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-100/50'
|
||||
? 'text-brand-700 dark:text-brand-300 bg-brand-50 dark:bg-brand-500/10 shadow-sm font-medium'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-100/50 dark:hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<Terminal size={16} className={activeTopic === null ? 'text-brand-500' : 'text-slate-400'} />
|
||||
<Terminal size={16} className={activeTopic === null ? 'text-brand-500' : 'text-slate-400 dark:text-slate-500'} />
|
||||
Introduction
|
||||
</button>
|
||||
</div>
|
||||
|
||||
40
src/components/ThemeToggle.jsx
Normal file
40
src/components/ThemeToggle.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Sun, Moon } from 'lucide-react'
|
||||
|
||||
function getInitialTheme() {
|
||||
if (typeof window === 'undefined') return false
|
||||
const stored = localStorage.getItem('theme')
|
||||
if (stored) return stored === 'dark'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [dark, setDark] = useState(getInitialTheme)
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement
|
||||
if (dark) root.classList.add('dark')
|
||||
else root.classList.remove('dark')
|
||||
try { localStorage.setItem('theme', dark ? 'dark' : 'light') } catch {}
|
||||
}, [dark])
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setDark((d) => !d)}
|
||||
aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
className="fixed top-3 right-3 sm:top-4 sm:right-4 z-50 w-10 h-10 sm:w-11 sm:h-11 rounded-full flex items-center justify-center glass text-slate-600 dark:text-slate-200 shadow-lg hover:scale-105 hover:text-brand-500 dark:hover:text-brand-400 active:scale-95 transition-all"
|
||||
>
|
||||
<span className="relative w-[18px] h-[18px]">
|
||||
<Sun
|
||||
size={18}
|
||||
className={`absolute inset-0 transition-all duration-300 ${dark ? 'opacity-100 rotate-0 scale-100' : 'opacity-0 -rotate-90 scale-50'}`}
|
||||
/>
|
||||
<Moon
|
||||
size={18}
|
||||
className={`absolute inset-0 transition-all duration-300 ${dark ? 'opacity-0 rotate-90 scale-50' : 'opacity-100 rotate-0 scale-100'}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -108,24 +108,24 @@ export default function TopicView({ topic, searchQuery }) {
|
||||
return (
|
||||
<div className="pb-24">
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-slate-900 mb-4 tracking-tight">{topic.name}</h1>
|
||||
<p className="text-lg text-slate-600 max-w-3xl leading-relaxed">{topic.description}</p>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-slate-900 dark:text-slate-100 mb-4 tracking-tight">{topic.name}</h1>
|
||||
<p className="text-base sm:text-lg text-slate-600 dark:text-slate-400 max-w-3xl leading-relaxed">{topic.description}</p>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<span className={`inline-flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-widest px-3 py-1 rounded-full border ${
|
||||
topic.type === 'legacy'
|
||||
? 'bg-brand-50 text-brand-600 border-brand-100'
|
||||
: 'bg-indigo-50 text-indigo-600 border-indigo-100'
|
||||
? 'bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 border-brand-100 dark:border-brand-500/20'
|
||||
: 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-100 dark:border-indigo-500/20'
|
||||
}`}>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
|
||||
{topic.type === 'legacy' ? 'GraphQL API' : 'REST API'}
|
||||
</span>
|
||||
<span className="text-sm text-slate-400">{filtered.length} endpoint{filtered.length !== 1 ? 's' : ''}</span>
|
||||
<span className="text-sm text-slate-400 dark:text-slate-500">{filtered.length} endpoint{filtered.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="text-center py-20 text-slate-400 text-sm bg-white rounded-2xl border border-slate-200/60">
|
||||
<div className="text-center py-20 text-slate-400 dark:text-slate-500 text-sm bg-white dark:bg-dark-800 rounded-2xl border border-slate-200/60 dark:border-white/10">
|
||||
No endpoints match "{searchQuery}".
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -8,6 +8,18 @@
|
||||
body {
|
||||
@apply bg-slate-50 text-slate-800 font-sans antialiased selection:bg-brand-500/30 selection:text-brand-900;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply bg-dark-900 text-slate-200;
|
||||
}
|
||||
|
||||
html {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@@ -15,6 +27,10 @@
|
||||
@apply bg-white/70 backdrop-blur-md border border-white/40 shadow-glass;
|
||||
}
|
||||
|
||||
.dark .glass {
|
||||
@apply bg-dark-800/80 border-white/5 shadow-none;
|
||||
}
|
||||
|
||||
.dark-glass {
|
||||
@apply bg-dark-900/90 backdrop-blur-xl border border-white/5;
|
||||
}
|
||||
@@ -40,12 +56,22 @@
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-slate-300;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
@apply bg-dark-600 rounded-full;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-dark-700;
|
||||
}
|
||||
|
||||
.bg-grid-pattern {
|
||||
background-image: linear-gradient(to right, #f1f5f9 1px, transparent 1px),
|
||||
linear-gradient(to bottom, #f1f5f9 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
.dark .bg-grid-pattern {
|
||||
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.animation-delay-2000 { animation-delay: 2s; }
|
||||
.animation-delay-4000 { animation-delay: 4s; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: ['./index.html', './src/**/*.{js,jsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
Reference in New Issue
Block a user