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 { 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>
|
||||
)
|
||||
})
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}}
|
||||
/>)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user