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 { numWithUnits } from '@/lib/format'
import textAreaCaret from 'textarea-caret'
import ReactDatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'
import useDebounceCallback, { debounce } from './use-debounce-callback'
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 { useShowModal } from './modal'
import { QRCodeSVG } from 'qrcode.react'
import { Scanner } from '@yudiel/react-qr-scanner'
import dynamic from 'next/dynamic'
import { qrImageSettings } from './qr'
import { useIsClient } from './use-client'
import PageLoading from './page-loading'
export class SessionRequiredError extends Error {
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 }) {
const formik = noForm ? null : useFormikContext()
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
@ -1038,19 +1051,23 @@ export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to
}
return (
<ReactDatePicker
className={`form-control text-center ${className}`}
selectsRange
maxDate={new Date()}
minDate={new Date('2021-05-01')}
{...props}
selected={new Date(innerFrom)}
startDate={new Date(innerFrom)}
endDate={innerTo ? new Date(innerTo) : undefined}
dateFormat={dateFormat}
onChangeRaw={onChangeRawHandler}
onChange={innerOnChange}
/>
<>
{ReactDatePicker && (
<ReactDatePicker
className={`form-control text-center ${className}`}
selectsRange
maxDate={new Date()}
minDate={new Date('2021-05-01')}
{...props}
selected={new Date(innerFrom)}
startDate={new Date(innerFrom)}
endDate={innerTo ? new Date(innerTo) : undefined}
dateFormat={dateFormat}
onChangeRaw={onChangeRawHandler}
onChange={innerOnChange}
/>
)}
</>
)
}
@ -1070,19 +1087,27 @@ export function DateTimeInput ({ label, groupClassName, name, ...props }) {
function DateTimePicker ({ name, className, ...props }) {
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 (
<ReactDatePicker
{...field}
{...props}
showTimeSelect
dateFormat='Pp'
className={`form-control ${className}`}
selected={(field.value && new Date(field.value)) || null}
value={(field.value && new Date(field.value)) || null}
onChange={(val) => {
helpers.setValue(val)
}}
/>
<>
{ReactDatePicker && (
<ReactDatePicker
{...field}
{...props}
showTimeSelect
dateFormat='Pp'
className={`form-control ${className}`}
selected={(field.value && new Date(field.value)) || null}
value={(field.value && new Date(field.value)) || null}
onChange={(val) => {
helpers.setValue(val)
}}
/>
)}
</>
)
}
@ -1149,6 +1174,10 @@ function QrPassword ({ value }) {
function PasswordScanner ({ onScan, text }) {
const showModal = useShowModal()
const toaster = useToast()
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), {
ssr: false,
loading: () => <PageLoading />
})
return (
<InputGroup.Text
@ -1158,26 +1187,28 @@ function PasswordScanner ({ onScan, text }) {
return (
<div>
{text && <h5 className='line-height-md mb-4 text-center'>{text}</h5>}
<Scanner
formats={['qr_code']}
onScan={([{ rawValue: result }]) => {
onScan(result)
onClose()
}}
styles={{
video: {
aspectRatio: '1 / 1'
}
}}
onError={(error) => {
if (error instanceof DOMException) {
console.log(error)
} else {
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
}
onClose()
}}
/>
{Scanner && (
<Scanner
formats={['qr_code']}
onScan={([{ rawValue: result }]) => {
onScan(result)
onClose()
}}
styles={{
video: {
aspectRatio: '1 / 1'
}
}}
onError={(error) => {
if (error instanceof DOMException) {
console.log(error)
} else {
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
}
onClose()
}}
/>
)}
</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 }) {
const [ReactSyntaxHighlighter, setReactSyntaxHighlighter] = useState(null)
const [syntaxTheme, setSyntaxTheme] = useState(null)
@ -245,7 +256,10 @@ function Code ({ node, inline, className, children, style, ...props }) {
const loadHighlighter = useCallback(() =>
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)
]), []
)
@ -260,7 +274,7 @@ function Code ({ node, inline, className, children, style, ...props }) {
}
}, [inline])
if (inline || !ReactSyntaxHighlighter || !syntaxTheme) {
if (inline || !ReactSyntaxHighlighter) { // inline code doesn't have a border radius
return (
<code className={className} {...props}>
{children}
@ -269,9 +283,13 @@ function Code ({ node, inline, className, children, style, ...props }) {
}
return (
<ReactSyntaxHighlighter style={syntaxTheme} language={language} PreTag='div' customStyle={{ borderRadius: '0.3rem' }} {...props}>
{children}
</ReactSyntaxHighlighter>
<>
{ReactSyntaxHighlighter && syntaxTheme && (
<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 { useField } from 'formik'
import { useToast } from '@/components/toast'
import { Scanner } from '@yudiel/react-qr-scanner'
import { decode } from 'bolt11'
import CameraIcon from '@/svgs/camera-line.svg'
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 AccordianItem from '@/components/accordian-item'
import { numWithUnits } from '@/lib/format'
import PageLoading from '@/components/page-loading'
import dynamic from 'next/dynamic'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -153,39 +154,47 @@ function InvoiceScanner ({ fieldName }) {
const showModal = useShowModal()
const [,, helpers] = useField(fieldName)
const toaster = useToast()
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), {
ssr: false,
loading: () => <PageLoading />
})
return (
<InputGroup.Text
style={{ cursor: 'pointer' }}
onClick={() => {
showModal(onClose => {
return (
<Scanner
formats={['qr_code']}
onScan={([{ rawValue: result }]) => {
result = result.toLowerCase()
if (result.split('lightning=')[1]) {
helpers.setValue(result.split('lightning=')[1].split(/[&?]/)[0])
} else if (decode(result.replace(/^lightning:/, ''))) {
helpers.setValue(result.replace(/^lightning:/, ''))
} else {
throw new Error('Not a proper lightning payment request')
}
onClose()
}}
styles={{
video: {
aspectRatio: '1 / 1'
}
}}
onError={(error) => {
if (error instanceof DOMException) {
console.log(error)
} else {
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
}
onClose()
}}
/>
<>
{Scanner && (
<Scanner
formats={['qr_code']}
onScan={([{ rawValue: result }]) => {
result = result.toLowerCase()
if (result.split('lightning=')[1]) {
helpers.setValue(result.split('lightning=')[1].split(/[&?]/)[0])
} else if (decode(result.replace(/^lightning:/, ''))) {
helpers.setValue(result.replace(/^lightning:/, ''))
} else {
throw new Error('Not a proper lightning payment request')
}
onClose()
}}
styles={{
video: {
aspectRatio: '1 / 1'
}
}}
onError={(error) => {
if (error instanceof DOMException) {
console.log(error)
} else {
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
}
onClose()
}}
/>)}
</>
)
})
}}