Add advanced option collapsing

This commit is contained in:
Quantum 2024-04-07 01:45:31 -04:00
parent 4f87a59307
commit 9c6cdf6ed3
6 changed files with 88 additions and 11 deletions

10
package-lock.json generated
View file

@ -17,6 +17,7 @@
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^5.2.10",
"@types/node": "^20.12.5", "@types/node": "^20.12.5",
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
@ -1302,6 +1303,15 @@
"@babel/types": "^7.20.7" "@babel/types": "^7.20.7"
} }
}, },
"node_modules/@types/bootstrap": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
"integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.9.2"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",

View file

@ -19,6 +19,7 @@
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^5.2.10",
"@types/node": "^20.12.5", "@types/node": "^20.12.5",
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",

12
src/ActionLink.tsx Normal file
View file

@ -0,0 +1,12 @@
import React from 'react';
type HTMLAnchorProps = Omit<React.HTMLProps<HTMLAnchorElement>, 'href' | 'onClick'>;
export default function ActionLink({onClick, className, ...props}: HTMLAnchorProps & { onClick: () => void }) {
const handleClick = React.useCallback((e: React.SyntheticEvent) => {
e.preventDefault();
onClick();
}, [onClick]);
return <a className={`totp-action-link ${className}`} onClick={handleClick} {...props} />;
}

View file

@ -1,9 +1,13 @@
import React from 'react'; import React, {useState} from 'react';
import {NumberInput, TextInput} from './Input'; import {NumberInput, TextInput} from './Input';
import OTPOutput, {HashAlgorithm} from './OTPOutput'; import OTPOutput, {HashAlgorithm} from './OTPOutput';
import Select from './Select'; import Select from './Select';
import Collapsible from './Collapsible';
import ActionLink from './ActionLink.tsx';
function App() { function App() {
const [advanced, setAdvanced] = useState(false);
const [secret, setSecret] = React.useState(''); const [secret, setSecret] = React.useState('');
const [step, setStep] = React.useState(30); const [step, setStep] = React.useState(30);
const [offset, setOffset] = React.useState(0); const [offset, setOffset] = React.useState(0);
@ -29,6 +33,14 @@ function App() {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [validStep, offset, step]); }, [validStep, offset, step]);
const showAdvanced = React.useCallback(() => {
setAdvanced(true);
}, []);
const hideAdvanced = React.useCallback(() => {
setAdvanced(false);
}, []);
const onReset = React.useCallback(() => { const onReset = React.useCallback(() => {
setStep(30); setStep(30);
setDigits(6); setDigits(6);
@ -39,6 +51,10 @@ function App() {
<div className="totp-app"> <div className="totp-app">
<div className="totp-settings"> <div className="totp-settings">
<TextInput label="Secret key" value={secret} onChange={setSecret}/> <TextInput label="Secret key" value={secret} onChange={setSecret}/>
{advanced ?
<ActionLink onClick={hideAdvanced}>Hide advanced options</ActionLink> :
<ActionLink onClick={showAdvanced}>Show advanced options</ActionLink>}
<Collapsible show={advanced}>
<NumberInput label="Time step" value={step} onChange={setStep} min={1} <NumberInput label="Time step" value={step} onChange={setStep} min={1}
error={!validStep && 'You must enter an integer time step ≥ 1 second'}/> error={!validStep && 'You must enter an integer time step ≥ 1 second'}/>
<NumberInput label="Code digits" value={digits} onChange={setDigits} min={1} max={10} <NumberInput label="Code digits" value={digits} onChange={setDigits} min={1} max={10}
@ -49,6 +65,7 @@ function App() {
sha512: 'SHA-512', sha512: 'SHA-512',
}}/> }}/>
<button type="button" className="btn btn-secondary" onClick={onReset}>Reset</button> <button type="button" className="btn btn-secondary" onClick={onReset}>Reset</button>
</Collapsible>
</div> </div>
{valid && <OTPOutput secret={secret} offset={offset} algorithm={algorithm} digits={digits}/>} {valid && <OTPOutput secret={secret} offset={offset} algorithm={algorithm} digits={digits}/>}
</div> </div>

28
src/Collapsible.tsx Normal file
View file

@ -0,0 +1,28 @@
import React from 'react';
import {Collapse} from 'bootstrap';
import classNames from 'classnames';
export interface CollapsibleHandle {
show: () => void;
hide: () => void;
toggle: () => void;
}
export default function Collapsible({children, show}: { children: React.ReactNode; show: boolean }) {
const collapse = React.useRef<Collapse>();
const onLoad = React.useCallback((element: HTMLDivElement) => {
collapse.current = new Collapse(element, {toggle: show});
}, []);
React.useEffect(() => {
if (show)
collapse.current?.show();
else
collapse.current?.hide();
}, [show]);
return <div ref={onLoad} className={classNames('collapse', {show})}>
{children}
</div>;
}

View file

@ -35,6 +35,10 @@ nav {
} }
} }
.totp-settings {
margin-bottom: 0.5em;
}
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
.totp-app { .totp-app {
@include make-row(); @include make-row();
@ -51,6 +55,11 @@ nav {
} }
} }
.totp-action-link {
cursor: pointer;
user-select: none;
}
.totp-input, .totp-select { .totp-input, .totp-select {
label { label {
font-weight: $font-weight-bold; font-weight: $font-weight-bold;