11import { useState , useEffect , useRef } from "react" ;
22import atsiomLogo from "./assets/atsiom-logo.png" ;
3+ import Home from "./components/Home.jsx" ;
34import ChmodCalculator from "./components/ChmodCalculator.jsx" ;
45import CrontabCalculator from "./components/CrontabCalculator.jsx" ;
56import CIDRCalculator from "./components/CIDRCalculator.jsx" ;
@@ -16,6 +17,51 @@ import RegexTester from "./components/RegexTester.jsx";
1617import JwtDecoder from "./components/JwtDecoder.jsx" ;
1718import Disclaimer from "./components/Disclaimer.jsx" ;
1819
20+ const SITE_URL = "https://syskit.atsiom.com" ;
21+
22+ const TOOL_META = {
23+ chmod : { title : "chmod calculator — syskit" , desc : "Calculate Unix file permission bits and generate the chmod command. Set owner, group, and other permissions interactively." } ,
24+ cron : { title : "cron expression builder — syskit" , desc : "Build cron expressions visually and preview the human-readable schedule. Supports standard 5-field crontab syntax." } ,
25+ cidr : { title : "CIDR subnet calculator — syskit" , desc : "Subnet calculator — compute usable hosts, network address, broadcast, and full IP range from any CIDR notation." } ,
26+ raid : { title : "RAID calculator — syskit" , desc : "Compute usable storage, fault tolerance, and efficiency for RAID 0, 1, 5, 6, and 10. Plan your storage configuration." } ,
27+ sed : { title : "sed command generator — syskit" , desc : "Generate sed substitution and deletion commands interactively. Build one-liners without memorizing the syntax." } ,
28+ awk : { title : "awk one-liner builder — syskit" , desc : "Build awk one-liners for field extraction and pattern filtering. Generate ready-to-run commands from a visual interface." } ,
29+ ipinfo : { title : "IP geolocation lookup — syskit" , desc : "Geo-locate any IP address with ISP details and a live map. Look up city, country, ASN, and network info instantly." } ,
30+ nslookup : { title : "DNS lookup — syskit" , desc : "Query A, AAAA, MX, TXT, NS, and CNAME records via DNS-over-HTTPS. No installation required — runs in your browser." } ,
31+ epoch : { title : "Unix epoch converter — syskit" , desc : "Convert Unix timestamps to human-readable dates and back. Supports seconds, milliseconds, and custom time zones." } ,
32+ dnsprop : { title : "DNS propagation checker — syskit" , desc : "Check DNS propagation across multiple global resolvers. See if your DNS changes have reached different regions." } ,
33+ urlencode : { title : "URL encoder / decoder — syskit" , desc : "Percent-encode and decode URLs and query strings. Convert special characters for safe use in HTTP requests." } ,
34+ base64 : { title : "Base64 encoder / decoder — syskit" , desc : "Encode plain text to Base64 and decode Base64 strings. Supports Unicode input and output." } ,
35+ regex : { title : "regex tester — syskit" , desc : "Test regular expressions with live match highlighting. Supports global, case-insensitive, multiline, and dotAll flags." } ,
36+ jwt : { title : "JWT decoder — syskit" , desc : "Inspect JWT header and payload claims without verifying the signature. Decode tokens and view expiry, issuer, and custom claims." } ,
37+ disclaimer :{ title : "Disclaimer — syskit" , desc : "Terms of use and disclaimer for syskit developer tools." } ,
38+ } ;
39+
40+ const HOME_META = {
41+ title : "syskit — developer tools for the browser" ,
42+ desc : "Free browser-based tools for developers and sysadmins. chmod calculator, cron builder, CIDR subnet calculator, RAID planner, regex tester, JWT decoder, Base64, URL encode, DNS lookup, and more. No sign-up, no tracking." ,
43+ } ;
44+
45+ function updatePageMeta ( toolId ) {
46+ const meta = toolId ? TOOL_META [ toolId ] : null ;
47+ const { title, desc } = meta ?? HOME_META ;
48+ const path = toolId ? `/${ toolId } ` : "/" ;
49+ const url = SITE_URL + path ;
50+
51+ document . title = title ;
52+
53+ const setMeta = ( sel , content ) => { const el = document . querySelector ( sel ) ; if ( el ) el . setAttribute ( "content" , content ) ; } ;
54+ const setId = ( id , val , attr = "content" ) => { const el = document . getElementById ( id ) ; if ( el ) el . setAttribute ( attr , val ) ; } ;
55+
56+ setMeta ( 'meta[name="description"]' , desc ) ;
57+ setId ( "canonical" , url , "href" ) ;
58+ setId ( "og-url" , url ) ;
59+ setId ( "og-title" , title ) ;
60+ setId ( "og-desc" , desc ) ;
61+ setId ( "tw-title" , title ) ;
62+ setId ( "tw-desc" , desc ) ;
63+ }
64+
1965const TOOLS = [
2066 { id : "chmod" , label : "chmod" , glyph : "rwx" , Component : ChmodCalculator , badge : "permissions" } ,
2167 { id : "cron" , label : "crontab" , glyph : "*/5" , Component : CrontabCalculator , badge : "scheduler" } ,
@@ -36,10 +82,9 @@ const TOOLS = [
3682
3783function getToolFromPath ( ) {
3884 const segments = window . location . pathname . split ( "/" ) . filter ( Boolean ) ;
85+ if ( ! segments . length ) return null ; // home
3986 const id = segments [ segments . length - 1 ] ?? "" ;
40- const fromPath = TOOLS . find ( ( t ) => t . id === id ) ?. id ;
41- if ( fromPath ) return fromPath ;
42- return localStorage . getItem ( "syskit-last-tool" ) ?? "chmod" ;
87+ return TOOLS . find ( ( t ) => t . id === id ) ?. id ?? null ;
4388}
4489
4590const THEME_OPTIONS = [
@@ -127,6 +172,7 @@ function ThemeToggle({ theme, onChange }) {
127172export default function App ( ) {
128173 const [ activeTool , setActiveTool ] = useState ( getToolFromPath ) ;
129174 const [ theme , setTheme ] = useState ( ( ) => localStorage . getItem ( "syskit-theme" ) || "system" ) ;
175+ const [ sidebarOpen , setSidebarOpen ] = useState ( false ) ;
130176
131177 const resolveTheme = ( t ) =>
132178 t === "system"
@@ -151,12 +197,16 @@ export default function App() {
151197 } , [ theme ] ) ;
152198
153199 useEffect ( ( ) => {
154- localStorage . setItem ( "syskit-last-tool" , activeTool ) ;
155- const segments = window . location . pathname . split ( "/" ) . filter ( Boolean ) ;
156- const current = segments [ segments . length - 1 ] ?? "" ;
157- if ( current !== activeTool ) {
158- const base = segments . slice ( 0 , - 1 ) . join ( "/" ) ;
159- history . pushState ( { } , "" , ( base ? "/" + base : "" ) + "/" + activeTool ) ;
200+ if ( activeTool === null ) {
201+ if ( window . location . pathname !== "/" ) history . pushState ( { } , "" , "/" ) ;
202+ } else {
203+ localStorage . setItem ( "syskit-last-tool" , activeTool ) ;
204+ const segments = window . location . pathname . split ( "/" ) . filter ( Boolean ) ;
205+ const current = segments [ segments . length - 1 ] ?? "" ;
206+ if ( current !== activeTool ) {
207+ const base = segments . slice ( 0 , - 1 ) . join ( "/" ) ;
208+ history . pushState ( { } , "" , ( base ? "/" + base : "" ) + "/" + activeTool ) ;
209+ }
160210 }
161211 } , [ activeTool ] ) ;
162212
@@ -166,19 +216,28 @@ export default function App() {
166216 return ( ) => window . removeEventListener ( "popstate" , onPop ) ;
167217 } , [ ] ) ;
168218
219+ useEffect ( ( ) => { updatePageMeta ( activeTool ) ; } , [ activeTool ] ) ;
220+
169221
170222
171223 const scrollRef = useRef ( null ) ;
172224 useEffect ( ( ) => { if ( scrollRef . current ) scrollRef . current . scrollTop = 0 ; } , [ activeTool ] ) ;
173225
174226 const activeMeta = TOOLS . find ( ( t ) => t . id === activeTool ) ;
175- const ActiveComponent = activeMeta ?. Component || ChmodCalculator ;
227+ const ActiveComponent = activeMeta ?. Component ?? null ;
228+
229+ const selectTool = ( id ) => { setActiveTool ( id ) ; setSidebarOpen ( false ) ; } ;
176230
177231 return (
178232 < div style = { { display : "flex" , height : "100vh" , overflow : "hidden" } } >
179233
234+ { /* ── SIDEBAR OVERLAY (mobile) ── */ }
235+ { sidebarOpen && (
236+ < div className = "sidebar-overlay" onClick = { ( ) => setSidebarOpen ( false ) } />
237+ ) }
238+
180239 { /* ── SIDEBAR ── */ }
181- < aside style = { {
240+ < aside className = { `app-sidebar ${ sidebarOpen ? " open" : "" } ` } style = { {
182241 width : "var(--sidebar-w)" , flexShrink : 0 ,
183242 background : "var(--surface)" , borderRight : "1px solid var(--border)" ,
184243 display : "flex" , flexDirection : "column" ,
@@ -187,18 +246,31 @@ export default function App() {
187246
188247 { /* Brand */ }
189248 < div style = { { height : "var(--header-h)" , display : "flex" , alignItems : "center" , padding : "0 1.25rem" , borderBottom : "1px solid var(--border)" , flexShrink : 0 } } >
190- < span style = { { fontFamily : "var(--font-mono)" , fontSize : "var(--lg)" , fontWeight : 400 , color : "var(--green)" , letterSpacing : "0.04em" } } > syskit< span style = { { color : "var(--text-faint)" } } > :</ span > </ span >
249+ < button onClick = { ( ) => selectTool ( null ) } style = { { background : "none" , border : "none" , padding : 0 , cursor : "pointer" } } >
250+ < span style = { { fontFamily : "var(--font-mono)" , fontSize : "var(--lg)" , fontWeight : 400 , color : "var(--green)" , letterSpacing : "0.04em" } } >
251+ syskit< span style = { { color : "var(--text-faint)" } } > :</ span >
252+ </ span >
253+ </ button >
191254 </ div >
192255
193256 { /* Nav items */ }
194257 < div style = { { flex : 1 , padding : "0.75rem 0.5rem" , overflowY : "auto" } } >
258+ { /* Home link */ }
259+ < button
260+ onClick = { ( ) => selectTool ( null ) }
261+ style = { { width : "100%" , display : "flex" , alignItems : "center" , gap : 10 , padding : "9px 10px" , border : `1px solid ${ activeTool === null ? "var(--green-dim)" : "transparent" } ` , background : activeTool === null ? "var(--green-bg)" : "transparent" , borderRadius : 10 , cursor : "pointer" , textAlign : "left" , transition : "all 0.13s" , marginBottom : 6 } }
262+ >
263+ < span style = { { fontFamily : "var(--font-mono)" , fontSize : "var(--xs)" , fontWeight : 400 , color : activeTool === null ? "var(--green)" : "var(--text-faint)" , background : activeTool === null ? "rgba(46,204,113,0.12)" : "var(--surface-2)" , border : `1px solid ${ activeTool === null ? "var(--green-dim)" : "var(--border)" } ` , borderRadius : 8 , padding : "2px 6px" , flexShrink : 0 , minWidth : 40 , textAlign : "center" , transition : "all 0.13s" } } > ~/</ span >
264+ < span style = { { fontSize : "var(--md)" , fontWeight : 400 , color : activeTool === null ? "var(--green)" : "var(--text-muted)" , transition : "color 0.13s" } } > home</ span >
265+ </ button >
266+
195267 < div style = { { fontFamily : "var(--font-mono)" , fontSize : "var(--xs)" , fontWeight : 400 , letterSpacing : "0.14em" , textTransform : "uppercase" , color : "var(--text-faint)" , padding : "0.5rem 0.75rem 0.5rem" , marginBottom : 4 } } > Tools</ div >
196268 { TOOLS . map ( ( tool ) => {
197269 const isActive = activeTool === tool . id ;
198270 return (
199271 < button
200272 key = { tool . id }
201- onClick = { ( ) => setActiveTool ( tool . id ) }
273+ onClick = { ( ) => selectTool ( tool . id ) }
202274 style = { { width : "100%" , display : "flex" , alignItems : "center" , gap : 10 , padding : "9px 10px" , border : `1px solid ${ isActive ? "var(--green-dim)" : "transparent" } ` , background : isActive ? "var(--green-bg)" : "transparent" , borderRadius : 10 , cursor : "pointer" , textAlign : "left" , transition : "all 0.13s" , marginBottom : 2 } }
203275 >
204276 < span style = { { fontFamily : "var(--font-mono)" , fontSize : "var(--xs)" , fontWeight : 400 , color : isActive ? "var(--green)" : "var(--text-faint)" , background : isActive ? "rgba(46,204,113,0.12)" : "var(--surface-2)" , border : `1px solid ${ isActive ? "var(--green-dim)" : "var(--border)" } ` , borderRadius : 8 , padding : "2px 6px" , flexShrink : 0 , minWidth : 40 , textAlign : "center" , transition : "all 0.13s" } } >
@@ -228,21 +300,47 @@ export default function App() {
228300 < div style = { { flex : 1 , display : "flex" , flexDirection : "column" , minWidth : 0 , height : "100vh" , overflow : "hidden" } } >
229301
230302 { /* Header */ }
231- < header style = { { height : "var(--header-h)" , flexShrink : 0 , background : "var(--surface)" , borderBottom : "1px solid var(--border)" , display : "flex" , alignItems : "center" , padding : "0 2.5rem" , gap : 10 } } >
232- < span style = { { fontSize : "var(--md)" , fontWeight : 600 , color : "var(--text)" } } >
233- { activeMeta ?. label }
234- </ span >
235- < span style = { { fontFamily : "var(--font-mono)" , fontSize : "var(--xs)" , color : "var(--text-faint)" , background : "var(--surface-2)" , border : "1px solid var(--border)" , borderRadius : 8 , padding : "2px 9px" } } >
236- { activeMeta ?. badge }
237- </ span >
303+ < header className = "app-header" style = { { height : "var(--header-h)" , flexShrink : 0 , background : "var(--surface)" , borderBottom : "1px solid var(--border)" , display : "flex" , alignItems : "center" , padding : "0 2.5rem" , gap : 10 } } >
304+
305+ { /* LEFT — hamburger + brand (mobile only, CSS shows these) */ }
306+ < button
307+ className = "hamburger-btn"
308+ onClick = { ( ) => setSidebarOpen ( ( v ) => ! v ) }
309+ style = { { display : "none" , alignItems : "center" , justifyContent : "center" , width : 34 , height : 34 , background : "var(--surface-2)" , border : "1px solid var(--border)" , borderRadius : 8 , flexShrink : 0 } }
310+ >
311+ < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2.5" strokeLinecap = "round" >
312+ < line x1 = "3" y1 = "6" x2 = "21" y2 = "6" />
313+ < line x1 = "3" y1 = "12" x2 = "21" y2 = "12" />
314+ < line x1 = "3" y1 = "18" x2 = "21" y2 = "18" />
315+ </ svg >
316+ </ button >
317+ < button className = "mobile-brand" onClick = { ( ) => selectTool ( null ) }
318+ style = { { display : "none" , background : "none" , border : "none" , padding : 0 , fontFamily : "var(--font-mono)" , fontSize : "var(--lg)" , fontWeight : 400 , color : "var(--green)" , letterSpacing : "0.04em" , cursor : "pointer" } } >
319+ syskit< span style = { { color : "var(--text-faint)" } } > :</ span >
320+ </ button >
321+
322+ { /* CENTER — tool name + badge, desktop only, only when a tool is active */ }
323+ { activeTool !== null && activeMeta && (
324+ < span className = "header-tool-info" >
325+ < span style = { { fontSize : "var(--md)" , fontWeight : 600 , color : "var(--text)" } } > { activeMeta . label } </ span >
326+ < span style = { { fontFamily : "var(--font-mono)" , fontSize : "var(--xs)" , color : "var(--text-faint)" , background : "var(--surface-2)" , border : "1px solid var(--border)" , borderRadius : 8 , padding : "2px 9px" } } >
327+ { activeMeta . badge }
328+ </ span >
329+ </ span >
330+ ) }
331+
332+ { /* RIGHT — spacer + theme toggle */ }
238333 < div style = { { flex : 1 } } />
239334 < ThemeToggle theme = { theme } onChange = { setTheme } />
240335 </ header >
241336
242337 { /* Scrollable content */ }
243- < div ref = { scrollRef } style = { { flex : 1 , overflowY : "auto" , padding : "2rem 2.5rem 0" } } >
338+ < div ref = { scrollRef } className = "app-content" style = { { flex : 1 , overflowY : "auto" , padding : "2rem 2.5rem 0" } } >
244339 < div style = { { width : "100%" , maxWidth : 960 , margin : "0 auto" , paddingBottom : "5rem" } } >
245- < ActiveComponent key = { activeTool } />
340+ { activeTool === null
341+ ? < Home key = "home" tools = { TOOLS } onSelect = { selectTool } />
342+ : ActiveComponent ? < ActiveComponent key = { activeTool } /> : null
343+ }
246344 </ div >
247345 </ div >
248346 </ div >
0 commit comments