updated for the mobile view and dark theme toggle

This commit is contained in:
2026-06-10 11:17:20 +05:30
parent 042a7155b6
commit 6133a4e339
8 changed files with 174 additions and 79 deletions

10
package-lock.json generated
View File

@@ -72,7 +72,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",
@@ -1378,7 +1377,6 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1420,7 +1418,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1643,7 +1640,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -1966,7 +1962,6 @@
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
@@ -2530,7 +2525,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -2923,7 +2917,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -2971,7 +2964,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -3161,7 +3153,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3642,7 +3633,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View File

@@ -1,7 +1,9 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import { Menu, Layers } from 'lucide-react';
import Sidebar from './components/Sidebar';
import EndpointViewer from './components/EndpointViewer';
import Introduction from './components/Introduction';
import ThemeToggle from './components/ThemeToggle';
import { graphqlTopics, GRAPHQL_BASE_URL } from './data/apiData';
import { restTopics, REST_BASE_URL } from './data/restTopics';
@@ -27,31 +29,79 @@ function filterTopics(topics, q) {
function App() {
const [activeTopic, setActiveTopic] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [sidebarOpen, setSidebarOpen] = useState(false);
const [dark, setDark] = useState(() => {
if (typeof window === 'undefined') return false;
const saved = localStorage.getItem('theme');
if (saved) return saved === 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});
useEffect(() => {
const root = document.documentElement;
root.classList.toggle('dark', dark);
localStorage.setItem('theme', dark ? 'dark' : 'light');
}, [dark]);
const q = searchQuery.trim().toLowerCase();
const visibleGraphql = useMemo(() => filterTopics(allGraphql, q), [q]);
const visibleRest = useMemo(() => filterTopics(allRest, q), [q]);
// On mobile, picking a topic should close the drawer
const selectTopic = (topic) => {
setActiveTopic(topic);
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 duration-300">
{/* 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 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 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 filter blur-3xl opacity-20 dark:opacity-10 animate-blob animation-delay-4000"></div>
{/* Mobile top bar */}
<header className="lg:hidden fixed top-0 left-0 right-0 h-16 z-30 glass flex items-center justify-between px-4">
<div 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-3.5 h-3.5" />
</div>
<span className="tracking-tight">NearleDaily</span>
</div>
<button
onClick={() => setSidebarOpen(true)}
aria-label="Open navigation menu"
className="w-10 h-10 -mr-1 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={22} />
</button>
</header>
{/* Mobile drawer backdrop */}
{sidebarOpen && (
<div
onClick={() => setSidebarOpen(false)}
className="lg:hidden fixed inset-0 z-30 bg-slate-900/40 backdrop-blur-sm"
/>
)}
<Sidebar
graphqlTopics={visibleGraphql}
restTopics={visibleRest}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveTopic={selectTopic}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
open={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/>
<main className="ml-[280px] flex-1 min-h-screen relative z-10">
<ThemeToggle dark={dark} setDark={setDark} />
<main className="lg:ml-[280px] flex-1 min-h-screen relative z-10 pt-16 lg: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">
<EndpointViewer topic={activeTopic} />
</div>
) : (
@@ -59,7 +109,7 @@ function App() {
<Introduction
allGraphql={allGraphql}
allRest={allRest}
setActiveTopic={setActiveTopic}
setActiveTopic={selectTopic}
/>
</div>
)}

View File

@@ -132,8 +132,8 @@ function EndpointRow({ endpoint, topic }) {
const urlPath = endpoint.url.split('?')[0];
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-8 lg:gap-16">
{/* Left Column: Description & Parameters */}
<div className="xl:col-span-5 space-y-8">
@@ -142,36 +142,36 @@ function EndpointRow({ endpoint, topic }) {
<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-all">
{endpoint.name}
</h3>
</div>
<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>
<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">{topic.baseUrl}</div>
<div className="font-semibold text-slate-800 break-all">{urlPath}</div>
<div className="text-xs text-slate-400 dark:text-slate-500 truncate">{topic.baseUrl}</div>
<div className="font-semibold text-slate-800 dark:text-slate-200 break-all">{urlPath}</div>
</div>
</div>
{/* Query params (GET endpoints) */}
{endpoint.method === 'GET' && Object.keys(params).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-slate-600"></span> Query Parameters
</h4>
<div className="space-y-4">
{Object.entries(params).map(([key, val]) => (
<div key={key} 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">
{key}
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-mono">string</span>
<span className="text-[10px] text-slate-400 dark:text-slate-500 uppercase tracking-wider font-mono">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-white/5 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"
value={val}
placeholder={`Enter ${key}...`}
onChange={e => {
@@ -196,7 +196,7 @@ function EndpointRow({ endpoint, topic }) {
{/* Right Column: Code Panel */}
<div className="xl:col-span-7">
<div className="dark-glass rounded-2xl overflow-hidden shadow-code flex flex-col h-full min-h-[450px] transform transition-transform duration-500 group-hover/row:-translate-y-1">
<div className="dark-glass rounded-2xl overflow-hidden shadow-code flex flex-col h-full min-h-[360px] sm:min-h-[450px] transform transition-transform duration-500 group-hover/row:-translate-y-1">
{/* macOS window dots */}
<div className="h-10 bg-white/5 border-b border-white/5 flex items-center px-4 gap-2 shrink-0">
@@ -339,18 +339,18 @@ export default function EndpointViewer({ topic }) {
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 break-words">{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 === 'graphql'
? '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 === 'graphql' ? 'GraphQL' : 'REST API'}
</span>
<span className="text-sm text-slate-400">{topic.endpoints.length} endpoint{topic.endpoints.length !== 1 ? 's' : ''}</span>
<span className="text-sm text-slate-400 dark:text-slate-500">{topic.endpoints.length} endpoint{topic.endpoints.length !== 1 ? 's' : ''}</span>
</div>
</div>

View File

@@ -22,32 +22,32 @@ export default function Introduction({ allGraphql, allRest, setActiveTopic }) {
const allTopics = [...allGraphql, ...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" />
v2.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">NearleDaily 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, lightning-fast platform designed for logistics, order management,
and multi-tenant POS administration available through both Hasura GraphQL and REST APIs.
</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</div>
<code className="font-mono text-sm text-slate-700">{GRAPHQL_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</div>
<code className="font-mono text-sm text-slate-700 dark:text-slate-300 break-all">{GRAPHQL_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>
@@ -59,14 +59,14 @@ export default function Introduction({ allGraphql, 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 hover:border-brand-200 dark:hover:border-brand-500/30 transition-all duration-300 cursor-pointer overflow-hidden"
style={{ animationDelay: `${index * 80}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}
@@ -75,14 +75,14 @@ export default function Introduction({ allGraphql, 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 border ${
topic.type === 'graphql'
? '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'
}`}>
{topic.type === 'graphql' ? '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>
@@ -91,7 +91,7 @@ export default function Introduction({ allGraphql, 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 text-slate-300 rounded-2xl shadow-code border border-transparent dark:border-white/5">
<Code2 className="text-brand-400 shrink-0" size={24} />
<p className="text-sm">
Explore real-time interactive endpoints. GraphQL requests are authenticated with the admin secret. REST endpoints support CORS and can be called directly from the browser.

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Search, ChevronRight, ChevronDown, Layers, Terminal, Users, ShoppingCart, Home, Box, Link2, Truck, UserCircle, Bike, FileText, CreditCard, Wrench } from 'lucide-react';
import { Search, ChevronRight, ChevronDown, Layers, Terminal, Users, ShoppingCart, Home, Box, Link2, Truck, UserCircle, Bike, FileText, CreditCard, Wrench, X } from 'lucide-react';
const topicIcons = {
users: Users,
@@ -16,7 +16,7 @@ const topicIcons = {
mobile: Box,
};
export default function Sidebar({ graphqlTopics, restTopics, activeTopic, setActiveTopic, searchQuery, setSearchQuery }) {
export default function Sidebar({ graphqlTopics, restTopics, activeTopic, setActiveTopic, searchQuery, setSearchQuery, open: drawerOpen = false, onClose }) {
const [open, setOpen] = useState({ general: true, graphql: true, rest: true });
const toggle = (key) => setOpen(prev => ({ ...prev, [key]: !prev[key] }));
@@ -24,10 +24,10 @@ export default function Sidebar({ graphqlTopics, restTopics, activeTopic, setAct
<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 dark:hover:text-brand-400 transition-colors group"
>
<span className="tracking-wide text-xs uppercase text-slate-400 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" />}
<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 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(topic => {
@@ -39,11 +39,11 @@ export default function Sidebar({ graphqlTopics, restTopics, activeTopic, setAct
onClick={() => setActiveTopic(topic)}
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`} />
<Icon size={16} 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`} />
{topic.name}
</button>
);
@@ -53,21 +53,31 @@ export default function Sidebar({ graphqlTopics, restTopics, activeTopic, setAct
);
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 lg:translate-x-0 ${drawerOpen ? 'translate-x-0' : '-translate-x-full'}`}>
{/* 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 mb-6">
<div className="flex items-center gap-3 text-slate-900 dark:text-slate-100 font-bold text-xl">
<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">NearleDaily</span>
</div>
<span className="tracking-tight">NearleDaily</span>
{/* Close button (mobile drawer only) */}
<button
onClick={onClose}
aria-label="Close navigation menu"
className="lg: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/10 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder="Search documentation..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
@@ -82,21 +92,21 @@ export default function Sidebar({ graphqlTopics, restTopics, activeTopic, setAct
<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 dark:hover:text-brand-400 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
? '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 ? 'text-brand-500' : 'text-slate-400'} />
<Terminal size={16} className={!activeTopic ? 'text-brand-500' : 'text-slate-400 dark:text-slate-500'} />
Introduction
</button>
</div>

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Sun, Moon } from 'lucide-react';
export default function ThemeToggle({ dark, setDark }) {
return (
<button
onClick={() => setDark(d => !d)}
aria-label={dark ? 'Switch to light theme' : 'Switch to dark theme'}
title={dark ? 'Light mode' : 'Dark mode'}
className="fixed top-3.5 right-16 lg:top-4 lg:right-4 z-50 w-11 h-11 rounded-full flex items-center justify-center
glass shadow-glass border border-slate-200/60 dark:border-white/10
text-slate-600 dark:text-slate-300 hover:text-brand-500 dark:hover:text-brand-400
hover:shadow-glow hover:-translate-y-0.5 active:translate-y-0
transition-all duration-300"
>
{dark
? <Sun size={18} className="transition-transform duration-300" />
: <Moon size={18} className="transition-transform duration-300" />}
</button>
);
}

View File

@@ -8,12 +8,18 @@
body {
@apply bg-slate-50 text-slate-800 font-sans antialiased selection:bg-brand-500/30 selection:text-brand-900;
}
html.dark body {
@apply bg-dark-900 text-slate-300 selection:bg-brand-500/30 selection:text-brand-100;
}
}
@layer utilities {
.glass {
@apply bg-white/70 backdrop-blur-md border border-white/40 shadow-glass;
}
html.dark .glass {
@apply bg-dark-800/80 border-white/5;
}
.dark-glass {
@apply bg-dark-900/90 backdrop-blur-xl border border-white/5;
@@ -40,9 +46,26 @@
@apply bg-slate-700 hover:bg-slate-600;
}
html.dark ::-webkit-scrollbar-thumb {
@apply bg-dark-600 hover:bg-slate-600;
}
/* Background Pattern */
.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;
}
html.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);
}
/* Hide scrollbar utility (used in sidebar / panes) */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}

View File

@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",