From 31532ff83031d3626084371f0a716fa9e609f2d4 Mon Sep 17 00:00:00 2001 From: soxa <6390896+Soxasora@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:06:40 +0100 Subject: [PATCH] Dynamic loading optimizations (#1925) * dynamic: React QR Scanner, React Datepicker; placeholder for syntax highlighting * Loading placeholders, prevent layout shifting --- components/form.js | 125 ++++++++++++++++++++++++++++----------------- components/text.js | 28 ++++++++-- pages/withdraw.js | 65 +++++++++++++---------- 3 files changed, 138 insertions(+), 80 deletions(-) diff --git a/components/form.js b/components/form.js index 5b5b12d2..c429ab7c 100644 --- a/components/form.js +++ b/components/form.js @@ -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 ( +
+ +
+ ) +} + +const ReactDatePicker = dynamic(() => import('react-datepicker').then(mod => mod.default), { + ssr: false, + loading: () => +}) + 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 && ( + + )} + ) } @@ -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: () => loading date picker + }) return ( - { - helpers.setValue(val) - }} - /> + <> + {ReactDatePicker && ( + { + 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: () => + }) return ( {text &&
{text}
} - { - 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 && ( + { + 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() + }} + /> + )} ) }) diff --git a/components/text.js b/components/text.js index 4e62041e..effca8a8 100644 --- a/components/text.js +++ b/components/text.js @@ -238,6 +238,17 @@ function Table ({ node, ...props }) { ) } +// prevent layout shifting when the code block is loading +function CodeSkeleton ({ className, children, ...props }) { + return ( +
+ + {children} + +
+ ) +} + 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: () => {children} + }), 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 ( {children} @@ -269,9 +283,13 @@ function Code ({ node, inline, className, children, style, ...props }) { } return ( - - {children} - + <> + {ReactSyntaxHighlighter && syntaxTheme && ( + + {children} + + )} + ) } diff --git a/pages/withdraw.js b/pages/withdraw.js index 1a39386e..1e198d21 100644 --- a/pages/withdraw.js +++ b/pages/withdraw.js @@ -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: () => + }) + return ( { showModal(onClose => { return ( - { - 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 && ( + { + 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() + }} + />)} + ) }) }}