From c7576b43ac2171e399cafbf4d7905ebb7ee8be7a Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 20 Jan 2025 13:11:27 -0600 Subject: [PATCH 1/3] Introduce loading status to thumbnails --- src/components/LinkWithStatus.tsx | 15 ++++++++++++--- src/photo/PhotoMedium.tsx | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/components/LinkWithStatus.tsx b/src/components/LinkWithStatus.tsx index 5655454d..6b00bf2c 100644 --- a/src/components/LinkWithStatus.tsx +++ b/src/components/LinkWithStatus.tsx @@ -17,10 +17,15 @@ const FLICKER_THRESHOLD = 400; // Clear loading status after long duration const MAX_LOADING_DURATION = 15_000; -export type LinkWithStatusProps = ComponentProps & { +export type LinkWithStatusProps = Omit< + ComponentProps, 'children' +> & { loadingElement?: ReactNode loadingClassName?: string contentClassName?: string + children: ReactNode | ((props: { + isLoading: boolean + }) => ReactNode) } export default function LinkWithStatus({ @@ -44,6 +49,8 @@ export default function LinkWithStatus({ const stopLoadingTimeout = useRef(undefined); const maxLoadingTimeout = useRef(undefined); + const isControlled = typeof children === 'function'; + const clearTimeouts = useCallback(() => { [startLoadingTimeout, stopLoadingTimeout, maxLoadingTimeout] .forEach(timeout => { @@ -114,11 +121,13 @@ export default function LinkWithStatus({ contentClassName, loadingElement ? isLoading ? 'opacity-0' : 'opacity-100' - : loadingClassName + : (loadingClassName || isControlled) ? 'opacity-100' : isLoading ? 'opacity-50' : 'opacity-100', )}> - {children} + {typeof children === 'function' + ? children({ isLoading }) + : children} {isLoading && loadingElement && - + ); }; From 8518bd216cd291bb5471d5fb9767554ffb67f511 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 20 Jan 2025 13:33:12 -0600 Subject: [PATCH 2/3] Add spinner to loading photo thumbs --- src/photo/PhotoMedium.tsx | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/photo/PhotoMedium.tsx b/src/photo/PhotoMedium.tsx index 085bba98..cd6feb0a 100644 --- a/src/photo/PhotoMedium.tsx +++ b/src/photo/PhotoMedium.tsx @@ -13,6 +13,7 @@ import { SHOULD_PREFETCH_ALL_LINKS } from '@/site/config'; import { useRef } from 'react'; import useOnVisible from '@/utility/useOnVisible'; import LinkWithStatus from '@/components/LinkWithStatus'; +import Spinner from '@/components/Spinner'; export default function PhotoMedium({ photo, @@ -48,16 +49,27 @@ export default function PhotoMedium({ )} prefetch={prefetch} > - + {({ isLoading }) => +
+ {isLoading && +
+ +
} + +
} ); }; From 271aeb0bb41ad85dae5ba82d8739ce41bff9f040 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 20 Jan 2025 14:02:03 -0600 Subject: [PATCH 3/3] Refine photo thumb loading animation --- eslint.config.mjs | 1 + next.config.ts | 1 - src/photo/PhotoMedium.tsx | 3 ++- tailwind.config.js | 6 ++++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9a536467..47a47718 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ const eslintConfig = [ rules: { '@next/next/no-img-element': 'off', '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-require-imports': 'off', 'no-unused-expressions': ['warn'], '@typescript-eslint/no-unused-vars': [ 'warn', { diff --git a/next.config.ts b/next.config.ts index 62a91b49..c41d7549 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ import type { NextConfig } from 'next'; import { RemotePattern } from 'next/dist/shared/lib/image-config'; diff --git a/src/photo/PhotoMedium.tsx b/src/photo/PhotoMedium.tsx index cd6feb0a..e67e744d 100644 --- a/src/photo/PhotoMedium.tsx +++ b/src/photo/PhotoMedium.tsx @@ -54,7 +54,8 @@ export default function PhotoMedium({ {isLoading &&
diff --git a/tailwind.config.js b/tailwind.config.js index f86a24d3..a728d01d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -28,12 +28,18 @@ module.exports = { animation: { 'rotate-pulse': 'rotate-pulse 0.75s linear infinite normal both running', + 'fade-in': + 'fade-in 0.5s linear', 'hover-drift': 'hover-drift 8s linear infinite', 'hover-wobble': 'hover-wobble 6s linear infinite normal both running', }, keyframes: { + 'fade-in': { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, 'rotate-pulse': { '0%': { transform: 'rotate(0deg) scale(1)' }, '50%': { transform: 'rotate(180deg) scale(0.8)' },