Dynamic loading optimizations (#1925)

* dynamic: React QR Scanner, React Datepicker; placeholder for syntax highlighting

* Loading placeholders, prevent layout shifting
This commit is contained in:
soxa 2025-02-24 22:06:40 +01:00 committed by GitHub
parent 31b58baf51
commit 31532ff830
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 138 additions and 80 deletions

View File

@ -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>
) )
}) })

View File

@ -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>
)}
</>
) )
} }

View File

@ -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()
}}
/>)}
</>
) )
}) })
}} }}