mirror of
https://github.com/quantum5/totp.git
synced 2025-04-24 13:41:58 -04:00
Add advanced option collapsing
This commit is contained in:
parent
4f87a59307
commit
9c6cdf6ed3
10
package-lock.json
generated
10
package-lock.json
generated
|
@ -17,6 +17,7 @@
|
|||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/node": "^20.12.5",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
|
@ -1302,6 +1303,15 @@
|
|||
"@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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/node": "^20.12.5",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
|
|
12
src/ActionLink.tsx
Normal file
12
src/ActionLink.tsx
Normal 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} />;
|
||||
}
|
19
src/App.tsx
19
src/App.tsx
|
@ -1,9 +1,13 @@
|
|||
import React from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import {NumberInput, TextInput} from './Input';
|
||||
import OTPOutput, {HashAlgorithm} from './OTPOutput';
|
||||
import Select from './Select';
|
||||
import Collapsible from './Collapsible';
|
||||
import ActionLink from './ActionLink.tsx';
|
||||
|
||||
function App() {
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
|
||||
const [secret, setSecret] = React.useState('');
|
||||
const [step, setStep] = React.useState(30);
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
|
@ -29,6 +33,14 @@ function App() {
|
|||
return () => clearTimeout(timer);
|
||||
}, [validStep, offset, step]);
|
||||
|
||||
const showAdvanced = React.useCallback(() => {
|
||||
setAdvanced(true);
|
||||
}, []);
|
||||
|
||||
const hideAdvanced = React.useCallback(() => {
|
||||
setAdvanced(false);
|
||||
}, []);
|
||||
|
||||
const onReset = React.useCallback(() => {
|
||||
setStep(30);
|
||||
setDigits(6);
|
||||
|
@ -39,6 +51,10 @@ function App() {
|
|||
<div className="totp-app">
|
||||
<div className="totp-settings">
|
||||
<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}
|
||||
error={!validStep && 'You must enter an integer time step ≥ 1 second'}/>
|
||||
<NumberInput label="Code digits" value={digits} onChange={setDigits} min={1} max={10}
|
||||
|
@ -49,6 +65,7 @@ function App() {
|
|||
sha512: 'SHA-512',
|
||||
}}/>
|
||||
<button type="button" className="btn btn-secondary" onClick={onReset}>Reset</button>
|
||||
</Collapsible>
|
||||
</div>
|
||||
{valid && <OTPOutput secret={secret} offset={offset} algorithm={algorithm} digits={digits}/>}
|
||||
</div>
|
||||
|
|
28
src/Collapsible.tsx
Normal file
28
src/Collapsible.tsx
Normal 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>;
|
||||
}
|
|
@ -35,6 +35,10 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
.totp-settings {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.totp-app {
|
||||
@include make-row();
|
||||
|
@ -51,6 +55,11 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
.totp-action-link {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.totp-input, .totp-select {
|
||||
label {
|
||||
font-weight: $font-weight-bold;
|
||||
|
|
Loading…
Reference in a new issue