/ src / app.tsx
app.tsx
  1  import { createRoot } from 'react-dom/client'
  2  import { useState, useEffect } from 'react'
  3  import { BrowserRouter, Routes, Route, Link, useLocation, useSearchParams } from 'react-router-dom'
  4  import { ErrorBoundary, getErrorMessage } from 'react-error-boundary'
  5  
  6  import { LS_RADIXES, AppContext, getCharsLS, sanitizeInput, serializeRadixes, unserializeRadixes } from './common.ts'
  7  import { type Radix, createRadixes, createRadix, num2str, str2num, allowedCharaters } from './radixes.ts'
  8  
  9  import Header from './components/header.tsx'
 10  import Show from './components/show.tsx'
 11  import Add from './components/add.tsx'
 12  import Multiply from './components/multiply.tsx'
 13  import Convert from './components/convert.tsx'
 14  
 15  import './app.css'
 16  
 17  
 18  export type UpdateRadixes = (radixes: Radix[]) => void
 19  export type UpdateValue = (value: bigint, radix?: Radix) => void
 20  
 21  const BIG_INT_0 = 0n
 22  
 23  createRoot(document.querySelector('#root')!).render(
 24  	<BrowserRouter basename={import.meta.env.BASE_URL}>
 25  		<App/>
 26  	</BrowserRouter>
 27  )
 28  
 29  function App() {
 30  	const [ error, setError ] = useState<unknown>()
 31  	const updateError = (error: unknown) => { setError(error); setTimeout(() => { setError(undefined) }, 30_000) }
 32  	const { radixes, enabledRadixes, updateRadixes, value, updateValue } = useStore(updateError)
 33  	const { pathname, search } = useLocation()
 34  
 35  	useEffect(() => {
 36  		const keyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') setError(undefined) }
 37  		document.addEventListener('keydown', keyDown)
 38  		return () => { document.removeEventListener('keydown', keyDown) }
 39  	}, [])
 40  
 41  	return (
 42  		<ErrorBoundary onError={updateError} FallbackComponent={ErrorToast}>
 43  			<AppContext value={{ updateError }}>
 44  				{ getErrorMessage(error) && <ErrorToast error={error}/> }
 45  				<Header radixes={radixes} updateRadixes={updateRadixes}/>
 46  				<nav className="tabs tabs-bordered justify-center mb-4 z-10">
 47  					<Link className={`tab ${pathname === '/' ? 'tab-active' : ''}`} to={`/${search}`}>Show</Link>
 48  					<Link className={`tab ${pathname.includes('add') ? 'tab-active' : ''}`} to={`add${search}`}>Add</Link>
 49  					<Link className={`tab ${pathname.includes('multiply') ? 'tab-active' : ''}`} to={`multiply${search}`}>Multiply</Link>
 50  					<Link className={`tab ${pathname.includes('convert') ? 'tab-active' : ''}`} to={`convert${search}`}>Convert</Link>
 51  				</nav>
 52  				<Routes>
 53  					<Route path="/" element={<Show radixes={enabledRadixes}/>}/>
 54  					<Route path="add" element={<Add radixes={enabledRadixes}/>}/>
 55  					<Route path="multiply" element={<Multiply radixes={enabledRadixes}/>}/>
 56  					<Route path="convert" element={<Convert radixes={enabledRadixes} value={value} updateValue={updateValue}/>}/>
 57  				</Routes>
 58  			</AppContext>
 59  		</ErrorBoundary>
 60  	)
 61  }
 62  
 63  function ErrorToast({ error }: { error: unknown }) {
 64  	return (
 65  		<div className="toast toast-top toast-center z-50">
 66  			<div className="alert alert-error">
 67  				<pre>{ getErrorMessage(error) ?? 'Unknown error' }</pre>
 68  			</div>
 69  		</div>
 70  	)
 71  }
 72  
 73  function useStore(updateError: (error: unknown) => void) {
 74  	const [ radixes, setRadixes ] = useState(getRadixesLS(updateError) ?? createRadixes(getCharsLS()))
 75  	const [ enabledRadixes, setEnabledRadixes ] = useState(radixes.filter(r => r.enabled))
 76  	const [ searchParams, setSearchParams ] = useSearchParams()
 77  	const [ radix, setRadix ] = useState(createRadix(10))
 78  	const [ value, setValue ] = useState(BIG_INT_0)
 79  
 80  	const updateRadixes: UpdateRadixes = radixes => {
 81  		setRadixes(radixes)
 82  
 83  		const enabledRadixes = radixes.filter(v => v.enabled)
 84  		setEnabledRadixes(enabledRadixes)
 85  
 86  		searchParams.delete('r')
 87  		enabledRadixes.forEach(r => { searchParams.append('r', r.name) })
 88  		setSearchParams(searchParams)
 89  
 90  		localStorage.setItem(LS_RADIXES, serializeRadixes(radixes))
 91  	}
 92  
 93  	const updateValue: UpdateValue = (value, r = radix) => {
 94  		if (value === BIG_INT_0) {
 95  			searchParams.delete('radix')
 96  			searchParams.delete('value')
 97  		} else {
 98  			if (r.name === '10') {
 99  				searchParams.delete('radix')
100  			} else {
101  				searchParams.set('radix', r.name)
102  			}
103  			searchParams.set('value', num2str(value, r))
104  		}
105  		setValue(value)
106  		setRadix(r)
107  		setSearchParams(searchParams)
108  	}
109  
110  	useEffect(() => {
111  		if (searchParams.has('clear-settings')) localStorage.clear()
112  
113  		if (searchParams.has('r')) {
114  			const searchRadixes = searchParams.getAll('r')
115  			radixes.forEach(r => { r.enabled = searchRadixes.includes(r.name) })
116  			updateRadixes(radixes)
117  			setEnabledRadixes(radixes.filter(r => r.enabled))
118  		} else {
119  			enabledRadixes.forEach(r => { searchParams.append('r', r.name) })
120  			setSearchParams(searchParams)
121  		}
122  
123  		let r: Radix | undefined = radix
124  
125  		const sRadix = searchParams.get('radix')
126  		if (sRadix) {
127  			r = radixes.find(r => r.name === sRadix)
128  			if (r == undefined) {
129  				updateError(new Error(`Unknown radix "${sRadix}" in the URL`))
130  				return
131  			}
132  			setRadix(r)
133  		}
134  
135  		const sValue = searchParams.get('value')
136  		if (sValue) {
137  			const [ value, rest ] = sanitizeInput(sValue, r)
138  			setValue(str2num(value, r))
139  			if (rest) updateError(new Error(`Non-Base characters "${rest}" for radix "${r.name}" has been filtered out. ${allowedCharaters(r)}`))
140  		}
141  	}, [])
142  
143  	return { radixes, enabledRadixes, updateRadixes, value, updateValue }
144  }
145  
146  function getRadixesLS(updateError: (error: unknown) => void): Radix[] | undefined {
147  	const item = localStorage.getItem(LS_RADIXES)
148  	if (item == undefined) return
149  
150  	try {
151  		return unserializeRadixes(item)
152  	} catch (error) {
153  		updateError(error)
154  		localStorage.removeItem(LS_RADIXES)
155  	}
156  }