Dynamic loading optimizations (#1925)
* dynamic: React QR Scanner, React Datepicker; placeholder for syntax highlighting * Loading placeholders, prevent layout shifting
This commit is contained in:
parent
31b58baf51
commit
31532ff830
@ -20,7 +20,6 @@ import TextareaAutosize from 'react-textarea-autosize'
|
|||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
import textAreaCaret from 'textarea-caret'
|
import textAreaCaret from 'textarea-caret'
|
||||||
import ReactDatePicker from 'react-datepicker'
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css'
|
import 'react-datepicker/dist/react-datepicker.css'
|
||||||
import useDebounceCallback, { debounce } from './use-debounce-callback'
|
import useDebounceCallback, { debounce } from './use-debounce-callback'
|
||||||
import { FileUpload } from './file-upload'
|
import { FileUpload } from './file-upload'
|
||||||
@ -38,9 +37,10 @@ import QrIcon from '@/svgs/qr-code-line.svg'
|
|||||||
import QrScanIcon from '@/svgs/qr-scan-line.svg'
|
import QrScanIcon from '@/svgs/qr-scan-line.svg'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import { QRCodeSVG } from 'qrcode.react'
|
import { QRCodeSVG } from 'qrcode.react'
|
||||||
import { Scanner } from '@yudiel/react-qr-scanner'
|
import dynamic from 'next/dynamic'
|
||||||
import { qrImageSettings } from './qr'
|
import { qrImageSettings } from './qr'
|
||||||
import { useIsClient } from './use-client'
|
import { useIsClient } from './use-client'
|
||||||
|
import PageLoading from './page-loading'
|
||||||
|
|
||||||
export class SessionRequiredError extends Error {
|
export class SessionRequiredError extends Error {
|
||||||
constructor () {
|
constructor () {
|
||||||
@ -971,6 +971,19 @@ export function Select ({ label, items, info, groupClassName, onChange, noForm,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DatePickerSkeleton () {
|
||||||
|
return (
|
||||||
|
<div className='react-datepicker-wrapper'>
|
||||||
|
<input className='form-control clouds fade-out p-0 px-2 mb-0' />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReactDatePicker = dynamic(() => import('react-datepicker').then(mod => mod.default), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <DatePickerSkeleton />
|
||||||
|
})
|
||||||
|
|
||||||
export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to, className, ...props }) {
|
export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to, className, ...props }) {
|
||||||
const formik = noForm ? null : useFormikContext()
|
const formik = noForm ? null : useFormikContext()
|
||||||
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
|
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
|
||||||
@ -1038,19 +1051,23 @@ export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactDatePicker
|
<>
|
||||||
className={`form-control text-center ${className}`}
|
{ReactDatePicker && (
|
||||||
selectsRange
|
<ReactDatePicker
|
||||||
maxDate={new Date()}
|
className={`form-control text-center ${className}`}
|
||||||
minDate={new Date('2021-05-01')}
|
selectsRange
|
||||||
{...props}
|
maxDate={new Date()}
|
||||||
selected={new Date(innerFrom)}
|
minDate={new Date('2021-05-01')}
|
||||||
startDate={new Date(innerFrom)}
|
{...props}
|
||||||
endDate={innerTo ? new Date(innerTo) : undefined}
|
selected={new Date(innerFrom)}
|
||||||
dateFormat={dateFormat}
|
startDate={new Date(innerFrom)}
|
||||||
onChangeRaw={onChangeRawHandler}
|
endDate={innerTo ? new Date(innerTo) : undefined}
|
||||||
onChange={innerOnChange}
|
dateFormat={dateFormat}
|
||||||
/>
|
onChangeRaw={onChangeRawHandler}
|
||||||
|
onChange={innerOnChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1070,19 +1087,27 @@ export function DateTimeInput ({ label, groupClassName, name, ...props }) {
|
|||||||
|
|
||||||
function DateTimePicker ({ name, className, ...props }) {
|
function DateTimePicker ({ name, className, ...props }) {
|
||||||
const [field, , helpers] = useField({ ...props, name })
|
const [field, , helpers] = useField({ ...props, name })
|
||||||
|
const ReactDatePicker = dynamic(() => import('react-datepicker').then(mod => mod.default), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <span>loading date picker</span>
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<ReactDatePicker
|
<>
|
||||||
{...field}
|
{ReactDatePicker && (
|
||||||
{...props}
|
<ReactDatePicker
|
||||||
showTimeSelect
|
{...field}
|
||||||
dateFormat='Pp'
|
{...props}
|
||||||
className={`form-control ${className}`}
|
showTimeSelect
|
||||||
selected={(field.value && new Date(field.value)) || null}
|
dateFormat='Pp'
|
||||||
value={(field.value && new Date(field.value)) || null}
|
className={`form-control ${className}`}
|
||||||
onChange={(val) => {
|
selected={(field.value && new Date(field.value)) || null}
|
||||||
helpers.setValue(val)
|
value={(field.value && new Date(field.value)) || null}
|
||||||
}}
|
onChange={(val) => {
|
||||||
/>
|
helpers.setValue(val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1149,6 +1174,10 @@ function QrPassword ({ value }) {
|
|||||||
function PasswordScanner ({ onScan, text }) {
|
function PasswordScanner ({ onScan, text }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <PageLoading />
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup.Text
|
<InputGroup.Text
|
||||||
@ -1158,26 +1187,28 @@ function PasswordScanner ({ onScan, text }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{text && <h5 className='line-height-md mb-4 text-center'>{text}</h5>}
|
{text && <h5 className='line-height-md mb-4 text-center'>{text}</h5>}
|
||||||
<Scanner
|
{Scanner && (
|
||||||
formats={['qr_code']}
|
<Scanner
|
||||||
onScan={([{ rawValue: result }]) => {
|
formats={['qr_code']}
|
||||||
onScan(result)
|
onScan={([{ rawValue: result }]) => {
|
||||||
onClose()
|
onScan(result)
|
||||||
}}
|
onClose()
|
||||||
styles={{
|
}}
|
||||||
video: {
|
styles={{
|
||||||
aspectRatio: '1 / 1'
|
video: {
|
||||||
}
|
aspectRatio: '1 / 1'
|
||||||
}}
|
}
|
||||||
onError={(error) => {
|
}}
|
||||||
if (error instanceof DOMException) {
|
onError={(error) => {
|
||||||
console.log(error)
|
if (error instanceof DOMException) {
|
||||||
} else {
|
console.log(error)
|
||||||
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
|
} else {
|
||||||
}
|
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
|
||||||
onClose()
|
}
|
||||||
}}
|
onClose()
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -238,6 +238,17 @@ function Table ({ node, ...props }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prevent layout shifting when the code block is loading
|
||||||
|
function CodeSkeleton ({ className, children, ...props }) {
|
||||||
|
return (
|
||||||
|
<div className='rounded' style={{ padding: '0.5em' }}>
|
||||||
|
<code className={`${className}`} {...props}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Code ({ node, inline, className, children, style, ...props }) {
|
function Code ({ node, inline, className, children, style, ...props }) {
|
||||||
const [ReactSyntaxHighlighter, setReactSyntaxHighlighter] = useState(null)
|
const [ReactSyntaxHighlighter, setReactSyntaxHighlighter] = useState(null)
|
||||||
const [syntaxTheme, setSyntaxTheme] = useState(null)
|
const [syntaxTheme, setSyntaxTheme] = useState(null)
|
||||||
@ -245,7 +256,10 @@ function Code ({ node, inline, className, children, style, ...props }) {
|
|||||||
|
|
||||||
const loadHighlighter = useCallback(() =>
|
const loadHighlighter = useCallback(() =>
|
||||||
Promise.all([
|
Promise.all([
|
||||||
dynamic(() => import('react-syntax-highlighter').then(mod => mod.LightAsync), { ssr: false }),
|
dynamic(() => import('react-syntax-highlighter').then(mod => mod.LightAsync), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <CodeSkeleton className={className} {...props}>{children}</CodeSkeleton>
|
||||||
|
}),
|
||||||
import('react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark').then(mod => mod.default)
|
import('react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark').then(mod => mod.default)
|
||||||
]), []
|
]), []
|
||||||
)
|
)
|
||||||
@ -260,7 +274,7 @@ function Code ({ node, inline, className, children, style, ...props }) {
|
|||||||
}
|
}
|
||||||
}, [inline])
|
}, [inline])
|
||||||
|
|
||||||
if (inline || !ReactSyntaxHighlighter || !syntaxTheme) {
|
if (inline || !ReactSyntaxHighlighter) { // inline code doesn't have a border radius
|
||||||
return (
|
return (
|
||||||
<code className={className} {...props}>
|
<code className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
@ -269,9 +283,13 @@ function Code ({ node, inline, className, children, style, ...props }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactSyntaxHighlighter style={syntaxTheme} language={language} PreTag='div' customStyle={{ borderRadius: '0.3rem' }} {...props}>
|
<>
|
||||||
{children}
|
{ReactSyntaxHighlighter && syntaxTheme && (
|
||||||
</ReactSyntaxHighlighter>
|
<ReactSyntaxHighlighter style={syntaxTheme} language={language} PreTag='div' customStyle={{ borderRadius: '0.3rem' }} {...props}>
|
||||||
|
{children}
|
||||||
|
</ReactSyntaxHighlighter>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ import { lnAddrSchema, withdrawlSchema } from '@/lib/validate'
|
|||||||
import { useShowModal } from '@/components/modal'
|
import { useShowModal } from '@/components/modal'
|
||||||
import { useField } from 'formik'
|
import { useField } from 'formik'
|
||||||
import { useToast } from '@/components/toast'
|
import { useToast } from '@/components/toast'
|
||||||
import { Scanner } from '@yudiel/react-qr-scanner'
|
|
||||||
import { decode } from 'bolt11'
|
import { decode } from 'bolt11'
|
||||||
import CameraIcon from '@/svgs/camera-line.svg'
|
import CameraIcon from '@/svgs/camera-line.svg'
|
||||||
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||||
@ -24,6 +23,8 @@ import useDebounceCallback from '@/components/use-debounce-callback'
|
|||||||
import { lnAddrOptions } from '@/lib/lnurl'
|
import { lnAddrOptions } from '@/lib/lnurl'
|
||||||
import AccordianItem from '@/components/accordian-item'
|
import AccordianItem from '@/components/accordian-item'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
|
import PageLoading from '@/components/page-loading'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||||
|
|
||||||
@ -153,39 +154,47 @@ function InvoiceScanner ({ fieldName }) {
|
|||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const [,, helpers] = useField(fieldName)
|
const [,, helpers] = useField(fieldName)
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <PageLoading />
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup.Text
|
<InputGroup.Text
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showModal(onClose => {
|
showModal(onClose => {
|
||||||
return (
|
return (
|
||||||
<Scanner
|
<>
|
||||||
formats={['qr_code']}
|
{Scanner && (
|
||||||
onScan={([{ rawValue: result }]) => {
|
<Scanner
|
||||||
result = result.toLowerCase()
|
formats={['qr_code']}
|
||||||
if (result.split('lightning=')[1]) {
|
onScan={([{ rawValue: result }]) => {
|
||||||
helpers.setValue(result.split('lightning=')[1].split(/[&?]/)[0])
|
result = result.toLowerCase()
|
||||||
} else if (decode(result.replace(/^lightning:/, ''))) {
|
if (result.split('lightning=')[1]) {
|
||||||
helpers.setValue(result.replace(/^lightning:/, ''))
|
helpers.setValue(result.split('lightning=')[1].split(/[&?]/)[0])
|
||||||
} else {
|
} else if (decode(result.replace(/^lightning:/, ''))) {
|
||||||
throw new Error('Not a proper lightning payment request')
|
helpers.setValue(result.replace(/^lightning:/, ''))
|
||||||
}
|
} else {
|
||||||
onClose()
|
throw new Error('Not a proper lightning payment request')
|
||||||
}}
|
}
|
||||||
styles={{
|
onClose()
|
||||||
video: {
|
}}
|
||||||
aspectRatio: '1 / 1'
|
styles={{
|
||||||
}
|
video: {
|
||||||
}}
|
aspectRatio: '1 / 1'
|
||||||
onError={(error) => {
|
}
|
||||||
if (error instanceof DOMException) {
|
}}
|
||||||
console.log(error)
|
onError={(error) => {
|
||||||
} else {
|
if (error instanceof DOMException) {
|
||||||
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
|
console.log(error)
|
||||||
}
|
} else {
|
||||||
onClose()
|
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
|
||||||
}}
|
}
|
||||||
/>
|
onClose()
|
||||||
|
}}
|
||||||
|
/>)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user