From e2017fdd3e05d289c814d5e0eb39493ff9ae62c8 Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Sun, 10 Nov 2024 16:07:25 +0700 Subject: [PATCH] feat: improve tweet embedding using react-tweet --- components/NotionPage.tsx | 17 ++++-- lib/get-tweets.ts | 62 +++++++++++++++++++++ lib/notion.ts | 3 ++ lib/types.ts | 4 ++ next.config.js | 46 ++++------------ package.json | 10 ++-- pnpm-lock.yaml | 110 +++++++++++++++++++++++--------------- 7 files changed, 165 insertions(+), 87 deletions(-) create mode 100644 lib/get-tweets.ts diff --git a/components/NotionPage.tsx b/components/NotionPage.tsx index 47a655dd9a..629d3ce248 100644 --- a/components/NotionPage.tsx +++ b/components/NotionPage.tsx @@ -7,8 +7,12 @@ import { type PageBlock } from 'notion-types' import { formatDate, getBlockTitle, getPageProperty } from 'notion-utils' import * as React from 'react' import BodyClassName from 'react-body-classname' -import { type NotionComponents, NotionRenderer } from 'react-notion-x' -import TweetEmbed from 'react-tweet-embed' +import { + type NotionComponents, + NotionRenderer, + useNotionContext +} from 'react-notion-x' +import { EmbeddedTweet, TweetNotFound, TweetSkeleton } from 'react-tweet' import { useSearchParam } from 'react-use' import type * as types from '@/lib/types' @@ -97,7 +101,14 @@ const Modal = dynamic( ) function Tweet({ id }: { id: string }) { - return + const { recordMap } = useNotionContext() + const tweet = (recordMap as types.ExtendedTweetRecordMap)?.tweets?.[id] + + return ( + }> + {tweet ? : } + + ) } const propertyLastEditedTimeValue = ( diff --git a/lib/get-tweets.ts b/lib/get-tweets.ts new file mode 100644 index 0000000000..9618266e3e --- /dev/null +++ b/lib/get-tweets.ts @@ -0,0 +1,62 @@ +import { type ExtendedRecordMap } from 'notion-types' +import { getPageTweetIds } from 'notion-utils' +import pMap from 'p-map' +import pMemoize from 'p-memoize' +import { getTweet as getTweetData } from 'react-tweet/api' + +import type { ExtendedTweetRecordMap } from './types' +import { db } from './db' + +export async function getTweetsMap( + recordMap: ExtendedRecordMap +): Promise { + const tweetIds = getPageTweetIds(recordMap) + + const tweetsMap = Object.fromEntries( + await pMap( + tweetIds, + async (tweetId: string) => { + return [tweetId, await getTweet(tweetId)] + }, + { + concurrency: 8 + } + ) + ) + + ;(recordMap as ExtendedTweetRecordMap).tweets = tweetsMap +} + +async function getTweetImpl(tweetId: string): Promise { + if (!tweetId) return null + + const cacheKey = `tweet:${tweetId}` + + try { + try { + const cachedTweet = await db.get(cacheKey) + if (cachedTweet) { + return cachedTweet + } + } catch (err) { + // ignore redis errors + console.warn(`redis error get "${cacheKey}"`, err.message) + } + + const tweetData = await getTweetData(tweetId) + + try { + await db.set(cacheKey, tweetData) + } catch (err) { + // ignore redis errors + console.warn(`redis error set "${cacheKey}"`, err.message) + } + + return tweetData + } catch (err: any) { + console.warn('failed to get tweet', tweetId, err.message) + return null + } +} + +export const getTweet = pMemoize(getTweetImpl) diff --git a/lib/notion.ts b/lib/notion.ts index d0723556df..35f40001cb 100644 --- a/lib/notion.ts +++ b/lib/notion.ts @@ -12,6 +12,7 @@ import { navigationLinks, navigationStyle } from './config' +import { getTweetsMap } from './get-tweets' import { notion } from './notion-api' import { getPreviewImageMap } from './preview-images' @@ -64,6 +65,8 @@ export async function getPage(pageId: string): Promise { ;(recordMap as any).preview_images = previewImageMap } + await getTweetsMap(recordMap) + return recordMap } diff --git a/lib/types.ts b/lib/types.ts index 2972167f86..e1d38d2bec 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -18,6 +18,10 @@ export interface PageProps { error?: PageError } +export interface ExtendedTweetRecordMap extends ExtendedRecordMap { + tweets: Record +} + export interface Params extends ParsedUrlQuery { pageId: string } diff --git a/next.config.js b/next.config.js index 9faccb9f23..33fa61993f 100644 --- a/next.config.js +++ b/next.config.js @@ -10,41 +10,12 @@ export default withBundleAnalyzer({ staticPageGenerationTimeout: 300, images: { remotePatterns: [ - { - protocol: 'https', - hostname: 'www.notion.so', - pathname: '**' - }, - { - protocol: 'https', - hostname: 'notion.so', - pathname: '**' - }, - { - protocol: 'https', - hostname: 'images.unsplash.com', - pathname: '**' - }, - { - protocol: 'https', - hostname: 'pbs.twimg.com', - pathname: '**' - }, - { - protocol: 'https', - hostname: 'abs.twimg.com', - pathname: '**' - }, - { - protocol: 'https', - hostname: 's3.us-west-2.amazonaws.com', - pathname: '**' - }, - { - protocol: 'https', - hostname: 'transitivebullsh.it', - pathname: '**' - } + { protocol: 'https', hostname: 'www.notion.so' }, + { protocol: 'https', hostname: 'notion.so' }, + { protocol: 'https', hostname: 'images.unsplash.com' }, + { protocol: 'https', hostname: 'abs.twimg.com' }, + { protocol: 'https', hostname: 'pbs.twimg.com' }, + { protocol: 'https', hostname: 's3.us-west-2.amazonaws.com' } ], formats: ['image/avif', 'image/webp'], dangerouslyAllowSVG: true, @@ -62,5 +33,8 @@ export default withBundleAnalyzer({ 'node_modules/react-dom' ) return config - } + }, + + // See https://react-tweet.vercel.app/next#troubleshooting + transpilePackages: ['react-tweet'] }) diff --git a/package.json b/package.json index e05d3d0388..452b457dfb 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,9 @@ "ky": "^1.7.2", "lqip-modern": "^2.2.1", "next": "^15.0.3", - "notion-client": "^7.1.1", - "notion-types": "^7.1.1", - "notion-utils": "^7.1.1", + "notion-client": "^7.1.3", + "notion-types": "^7.1.3", + "notion-utils": "^7.1.3", "p-map": "^7.0.2", "p-memoize": "^7.1.1", "posthog-js": "^1.181.0", @@ -50,8 +50,8 @@ "react": "^18.2.0", "react-body-classname": "^1.3.1", "react-dom": "^18.2.0", - "react-notion-x": "^7.2.1", - "react-tweet-embed": "^2.0.0", + "react-notion-x": "^7.2.3", + "react-tweet": "^3.2.1", "react-use": "^17.4.2", "rss": "^1.2.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41495f524a..be02629a53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,14 +39,14 @@ importers: specifier: ^15.0.3 version: 15.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) notion-client: - specifier: ^7.1.1 - version: 7.1.1 + specifier: ^7.1.3 + version: 7.1.3 notion-types: - specifier: ^7.1.1 - version: 7.1.1 + specifier: ^7.1.3 + version: 7.1.3 notion-utils: - specifier: ^7.1.1 - version: 7.1.1 + specifier: ^7.1.3 + version: 7.1.3 p-map: specifier: ^7.0.2 version: 7.0.2 @@ -69,11 +69,11 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) react-notion-x: - specifier: ^7.2.1 - version: 7.2.1(@babel/runtime@7.26.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-tweet-embed: - specifier: ^2.0.0 - version: 2.0.0(react@18.3.1) + specifier: ^7.2.3 + version: 7.2.3(@babel/runtime@7.26.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-tweet: + specifier: ^3.2.1 + version: 3.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-use: specifier: ^17.4.2 version: 17.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1627,16 +1627,16 @@ packages: resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} engines: {node: '>=14.16'} - notion-client@7.1.1: - resolution: {integrity: sha512-mT/yEOIlbQzIAMsuIoWMX3XQrkoN07NMtSilDYIQQShxYJDdXMYfLuSoOgalEwFmwwsDnETHJqfrdkfe6sF9Rw==} + notion-client@7.1.3: + resolution: {integrity: sha512-84K4h/pD8fSIth5cKF0qUcHTqGdzzQ6x6hMVErZzbIFcXlOmJvHROd+no5epcMnw1juuIdeDbodN/wT8+U53OA==} engines: {node: '>=18'} - notion-types@7.1.1: - resolution: {integrity: sha512-wsj/mwTi0hZjldvfVoKgtrXrOaBRgEsXic2n1kh6S1Aj7snx9Xo8sA8KtHqmXqKhJ8BFEsavh0Dv3TlyMv8aWg==} + notion-types@7.1.3: + resolution: {integrity: sha512-kUcMa5SXpzNxmE9PdrSjiP1lmba8ue7+KdMjcmq8QO/wOmdmXCKhr1FHXNqP5Wdxmm2i9sDq87x0S1pA/vJ4kg==} engines: {node: '>=18'} - notion-utils@7.1.1: - resolution: {integrity: sha512-Hks/sipBA7aDZ3TS90CRKuKvCsvs/cW+MXzZPFh/Q1uTjhEt9GynSUl8o11k+Nm4No3+deQJY/hsuv90gyCFjg==} + notion-utils@7.1.3: + resolution: {integrity: sha512-52dReJMdEBt6O4f/Y9FZqhRNBoDY73p0i1XSqpunaTzxIQW61St6a+8bhZ0HiFPpXvxPUw7HCBfNAqn0CrmF2A==} engines: {node: '>=18'} npm-normalize-package-bin@4.0.0: @@ -1883,8 +1883,8 @@ packages: react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 - react-notion-x@7.2.1: - resolution: {integrity: sha512-wy/MOp/+pOL/qKUcY3azMvTn/y6AkKqutFboOKn0OUouPe2OaOjUk/VIGFV081UL7+nwCKbWecHgRZ9MuwwHng==} + react-notion-x@7.2.3: + resolution: {integrity: sha512-eTRkypql15+Uej00MvkG8cWhbt5PDFmj4Zfvyr7582GPblefHFDmtTZOxMiVJhSU7Kv8LZEuGXsYQtFE1cQPqQ==} engines: {node: '>=18'} peerDependencies: react: '>=18' @@ -1905,10 +1905,11 @@ packages: peerDependencies: react: ^16.3.0 || ^17.0.0 || ^18.0.0 - react-tweet-embed@2.0.0: - resolution: {integrity: sha512-g2kfPjSRTOKeJtaQF5EMuSTmp/q8I0qdDs/pZ2qLXZjCWExDT/JgjxSlyM65NyNzsz8072PDpvlO/sIXwwVpdQ==} + react-tweet@3.2.1: + resolution: {integrity: sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA==} peerDependencies: - react: '>=17' + react: '>= 18.0.0' + react-dom: '>= 18.0.0' react-universal-interface@0.6.2: resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} @@ -2209,6 +2210,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swr@2.2.5: + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -2320,6 +2326,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2465,8 +2476,8 @@ snapshots: '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) eslint-config-prettier: 9.1.0(eslint@8.57.1) - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jest: 28.8.3(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) eslint-plugin-jest-dom: 5.4.0(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) @@ -2980,8 +2991,7 @@ snapshots: client-only@0.0.1: {} - clsx@2.1.1: - optional: true + clsx@2.1.1: {} cluster-key-slot@1.1.2: {} @@ -3247,37 +3257,37 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -3288,7 +3298,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -4072,21 +4082,21 @@ snapshots: normalize-url@8.0.1: {} - notion-client@7.1.1: + notion-client@7.1.3: dependencies: ky: 1.7.2 - notion-types: 7.1.1 - notion-utils: 7.1.1 + notion-types: 7.1.3 + notion-utils: 7.1.3 p-map: 7.0.2 - notion-types@7.1.1: {} + notion-types@7.1.3: {} - notion-utils@7.1.1: + notion-utils@7.1.3: dependencies: is-url-superb: 6.1.0 mem: 10.0.0 normalize-url: 8.0.1 - notion-types: 7.1.1 + notion-types: 7.1.3 p-queue: 8.0.1 npm-normalize-package-bin@4.0.0: {} @@ -4333,13 +4343,13 @@ snapshots: react-lifecycles-compat: 3.0.4 warning: 4.0.3 - react-notion-x@7.2.1(@babel/runtime@7.26.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-notion-x@7.2.3(@babel/runtime@7.26.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@fisch0920/medium-zoom': 1.0.7 '@matejmazur/react-katex': 3.1.3(katex@0.16.11)(react@18.3.1) katex: 0.16.11 - notion-types: 7.1.1 - notion-utils: 7.1.1 + notion-types: 7.1.3 + notion-utils: 7.1.3 prismjs: 1.29.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -4379,9 +4389,13 @@ snapshots: dependencies: react: 18.3.1 - react-tweet-embed@2.0.0(react@18.3.1): + react-tweet@3.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: + '@swc/helpers': 0.5.13 + clsx: 2.1.1 react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + swr: 2.2.5(react@18.3.1) react-universal-interface@0.6.2(react@18.3.1)(tslib@2.8.1): dependencies: @@ -4748,6 +4762,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swr@2.2.5(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) + tapable@2.2.1: {} tar@6.2.1: @@ -4865,6 +4885,10 @@ snapshots: '@use-it/event-listener': 0.1.7(react@18.3.1) react: 18.3.1 + use-sync-external-store@1.2.2(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: optional: true