diff --git a/package-lock.json b/package-lock.json index 3da0c20..9426422 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 20baddb..6f36743 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ActionLink.tsx b/src/ActionLink.tsx new file mode 100644 index 0000000..98dc021 --- /dev/null +++ b/src/ActionLink.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +type HTMLAnchorProps = Omit, 'href' | 'onClick'>; + +export default function ActionLink({onClick, className, ...props}: HTMLAnchorProps & { onClick: () => void }) { + const handleClick = React.useCallback((e: React.SyntheticEvent) => { + e.preventDefault(); + onClick(); + }, [onClick]); + + return ; +} diff --git a/src/App.tsx b/src/App.tsx index 1d8285a..d3cc023 100644 --- a/src/App.tsx +++ b/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,16 +51,21 @@ function App() {
- - - + +
{valid && }
diff --git a/src/Collapsible.tsx b/src/Collapsible.tsx new file mode 100644 index 0000000..ad5cef5 --- /dev/null +++ b/src/Collapsible.tsx @@ -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(); + + 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
+ {children} +
; +} diff --git a/src/index.scss b/src/index.scss index 308947c..72b04e5 100644 --- a/src/index.scss +++ b/src/index.scss @@ -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;