From 3afaff6d2fab336726e316aee1a511936340afb1 Mon Sep 17 00:00:00 2001 From: Kyle Holmberg Date: Sun, 9 Nov 2025 22:17:08 +0700 Subject: [PATCH 01/11] remove unused deps --- package.json | 7 --- pnpm-lock.yaml | 125 ------------------------------------------------- 2 files changed, 132 deletions(-) diff --git a/package.json b/package.json index 99999499e..2142b0818 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "vitest": "cross-env vitest --config ./vitest.config.mts" }, "dependencies": { - "@innocuous/components": "^2.1.1", "@next/bundle-analyzer": "^12.3.1", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-tabs": "1.1.13", @@ -46,12 +45,10 @@ "airtable": "^0.12.2", "axios": "^1.12.2", "cva": "^1.0.0-beta.4", - "date-fns": "^2.29.3", "fast-xml-parser": "^3.21.1", "fingerprintjs2": "^2.1.4", "fontfaceobserver": "^2.3.0", "formik": "^2.4.6", - "history": "^5.3.0", "intersection-observer": "^0.12.2", "js-cookie": "^3.0.1", "jwt-decode": "^3.1.2", @@ -61,20 +58,16 @@ "next": "^12.3.1", "next-cookies": "^2.0.3", "next-sitemap": "^4.2.3", - "nuka-carousel": "^5.2.0", "object-hash": "^3.0.0", "path": "^0.12.7", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-facebook-login": "^4.1.1", - "react-google-login": "^5.2.2", "react-on-screen": "^2.1.1", "react-player": "^2.16.0", "react-scroll": "^1.9.3", "react-scroll-up-button": "^1.6.4", "react-select": "^4.0.2", - "react-youtube": "9.0.3", "tailwind-merge": "^3.3.1", "yup": "^1.7.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 133be6dae..023293cda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@innocuous/components': - specifier: ^2.1.1 - version: 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@next/bundle-analyzer': specifier: ^12.3.1 version: 12.3.7 @@ -41,9 +38,6 @@ importers: cva: specifier: ^1.0.0-beta.4 version: 1.0.0-beta.4(typescript@5.9.3) - date-fns: - specifier: ^2.29.3 - version: 2.30.0 fast-xml-parser: specifier: ^3.21.1 version: 3.21.1 @@ -56,9 +50,6 @@ importers: formik: specifier: ^2.4.6 version: 2.4.8(@types/react@18.3.26)(react@18.3.1) - history: - specifier: ^5.3.0 - version: 5.3.0 intersection-observer: specifier: ^0.12.2 version: 0.12.2 @@ -86,9 +77,6 @@ importers: next-sitemap: specifier: ^4.2.3 version: 4.2.3(next@12.3.7(@babel/core@7.28.5)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) - nuka-carousel: - specifier: ^5.2.0 - version: 5.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) object-hash: specifier: ^3.0.0 version: 3.0.0 @@ -104,12 +92,6 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) - react-facebook-login: - specifier: ^4.1.1 - version: 4.1.1(react@18.3.1) - react-google-login: - specifier: ^5.2.2 - version: 5.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-on-screen: specifier: ^2.1.1 version: 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -125,9 +107,6 @@ importers: react-select: specifier: ^4.0.2 version: 4.3.1(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-youtube: - specifier: 9.0.3 - version: 9.0.3(react@18.3.1) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -1461,18 +1440,6 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead - '@innocuous/components@2.1.1': - resolution: {integrity: sha512-yU4a5kYCerTJOGDWdVsaLL3TvbDf9ZTitQBbTO8DS4fq6sjcHzhQcIyJ5Ox0EgNRtk1RujXHC5OOsMN3uUXYuA==} - peerDependencies: - react: '>=16.8.6' - react-dom: '>=16.8.6' - - '@innocuous/hooks@2.1.1': - resolution: {integrity: sha512-aEmassaakqeAsI0FXeScMNexZTppDRxIMSOxD12aVFLBxCIdXjSMwBd1tQZJpFYTtUbTjh6x76GPUTfyI8WsqQ==} - peerDependencies: - react: '>=16.8.6' - react-dom: '>=16.8.6' - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -4234,10 +4201,6 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} - debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -5268,9 +5231,6 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - history@5.3.0: - resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==} - hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} @@ -6324,13 +6284,6 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nuka-carousel@5.6.0: - resolution: {integrity: sha512-pELQHcOTJ5yJvs7wtXD5J5pqBMHDfdI667V9jaEhJVr9VQKMwq87xXv5P1cXiNamJuT3g2zw30ic6Y4eII4ofA==} - engines: {node: '>=12.0.0'} - peerDependencies: - react: ^15.3.0 || ^16.0.0 || ^17.0.1 || ^18.0.0 - react-dom: ^15.3.0 || ^16.0.0 || ^17.0.1 || ^18.0.0 - nwsapi@2.2.22: resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} @@ -6958,24 +6911,12 @@ packages: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 - react-facebook-login@4.1.1: - resolution: {integrity: sha512-COnHEHlYGTKipz4963safFAK9PaNTcCiXfPXMS/yxo8El+/AJL5ye8kMJf23lKSSGGPgqFQuInskIHVqGqTvSw==} - peerDependencies: - react: ^16.0.0 - react-fast-compare@2.0.4: resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-google-login@5.2.2: - resolution: {integrity: sha512-JUngfvaSMcOuV0lFff7+SzJ2qviuNMQdqlsDJkUM145xkGPVIfqWXq9Ui+2Dr6jdJWH5KYdynz9+4CzKjI5u6g==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - peerDependencies: - react: ^16 || ^17 - react-dom: ^16 || ^17 - react-input-autosize@3.0.0: resolution: {integrity: sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==} peerDependencies: @@ -7085,12 +7026,6 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react-youtube@9.0.3: - resolution: {integrity: sha512-NfLSlFRTGz5sanbt23Yfw+2TOhWZFMM8qBESb4qs5VxMcykVC1BsQhPcptdAKCFK5YOa0le/y8+lp/obrSZUjQ==} - engines: {node: '>= 14.x'} - peerDependencies: - react: '>=0.14.1' - react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -7458,9 +7393,6 @@ packages: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} - sister@3.0.2: - resolution: {integrity: sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -8563,9 +8495,6 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - youtube-player@5.5.2: - resolution: {integrity: sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==} - yup@1.7.1: resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} @@ -9708,17 +9637,6 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@innocuous/components@2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@innocuous/hooks': 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - - '@innocuous/hooks@2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -13230,10 +13148,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - date-fns@2.30.0: - dependencies: - '@babel/runtime': 7.28.4 - debug@2.6.9: dependencies: ms: 2.0.0 @@ -14547,10 +14461,6 @@ snapshots: he@1.2.0: {} - history@5.3.0: - dependencies: - '@babel/runtime': 7.28.4 - hmac-drbg@1.0.1: dependencies: hash.js: 1.1.7 @@ -15642,11 +15552,6 @@ snapshots: dependencies: boolbase: 1.0.0 - nuka-carousel@5.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - nwsapi@2.2.22: {} nypm@0.5.4: @@ -16324,21 +16229,10 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-is: 18.1.0 - react-facebook-login@4.1.1(react@18.3.1): - dependencies: - react: 18.3.1 - react-fast-compare@2.0.4: {} react-fast-compare@3.2.2: {} - react-google-login@5.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@types/react': 18.3.26 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-input-autosize@3.0.0(react@18.3.1): dependencies: prop-types: 15.8.1 @@ -16462,15 +16356,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-youtube@9.0.3(react@18.3.1): - dependencies: - fast-deep-equal: 3.1.3 - prop-types: 15.8.1 - react: 18.3.1 - youtube-player: 5.5.2 - transitivePeerDependencies: - - supports-color - react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -16934,8 +16819,6 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 - sister@3.0.2: {} - sisteransi@1.0.5: {} slash@3.0.0: {} @@ -18156,14 +18039,6 @@ snapshots: yocto-queue@1.2.1: {} - youtube-player@5.5.2: - dependencies: - debug: 2.6.9 - load-script: 1.0.0 - sister: 3.0.2 - transitivePeerDependencies: - - supports-color - yup@1.7.1: dependencies: property-expr: 2.0.6 From 7521392ef8fbc0475586a913962e3d7946140a7e Mon Sep 17 00:00:00 2001 From: Kyle Holmberg Date: Mon, 10 Nov 2025 00:30:46 +0700 Subject: [PATCH 02/11] replace react-scroll-up-button --- .../ScrollToTopButton/ScrollToTopButton.tsx | 36 ++++++++ .../__tests__/ScrollToTopButton.test.tsx | 83 +++++++++++++++++++ .../ScrollToTopButton.test.tsx.snap | 21 +++++ pages/_app.tsx | 4 +- 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 components/ScrollToTopButton/ScrollToTopButton.tsx create mode 100644 components/ScrollToTopButton/__tests__/ScrollToTopButton.test.tsx create mode 100644 components/ScrollToTopButton/__tests__/__snapshots__/ScrollToTopButton.test.tsx.snap diff --git a/components/ScrollToTopButton/ScrollToTopButton.tsx b/components/ScrollToTopButton/ScrollToTopButton.tsx new file mode 100644 index 000000000..f784b9b18 --- /dev/null +++ b/components/ScrollToTopButton/ScrollToTopButton.tsx @@ -0,0 +1,36 @@ +import { cx } from 'common/utils/cva'; +import { useEffect, useState } from 'react'; + +export function ScrollToTopButton() { + const [isInView, setIsInView] = useState(false); + + useEffect(() => { + const handleScroll = () => setIsInView(window.scrollY >= 750); + window.addEventListener('scroll', handleScroll, { passive: true }); + handleScroll(); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + return ( + + ); +} diff --git a/components/ScrollToTopButton/__tests__/ScrollToTopButton.test.tsx b/components/ScrollToTopButton/__tests__/ScrollToTopButton.test.tsx new file mode 100644 index 000000000..6b2ebd028 --- /dev/null +++ b/components/ScrollToTopButton/__tests__/ScrollToTopButton.test.tsx @@ -0,0 +1,83 @@ +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { vi } from 'vitest'; +import createSnapshotTest from 'test-utils/createSnapshotTest'; + +import { ScrollToTopButton } from '../ScrollToTopButton'; + +describe('ScrollToTopButton', () => { + beforeEach(() => { + vi.clearAllMocks(); + window.scrollY = 0; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should render with required props', () => { + createSnapshotTest(); + }); + + it('should not be visible when scroll position is less than 750px', () => { + const { container } = render(); + const button = container.querySelector('button'); + + expect(button).toHaveClass('opacity-0'); + expect(button).toHaveClass('-right-5'); + }); + + it('should be visible when scroll position is 750px', async () => { + const { container } = render(); + + Object.defineProperty(window, 'scrollY', { value: 750, writable: true }); + fireEvent.scroll(window); + + await waitFor(() => { + const button = container.querySelector('button'); + expect(button).toHaveClass('opacity-70'); + expect(button).toHaveClass('right-6'); + }); + }); + + it('should hide when scrolling back above 750px threshold', async () => { + const { container } = render(); + + Object.defineProperty(window, 'scrollY', { value: 1000, writable: true }); + fireEvent.scroll(window); + + await waitFor(() => { + const button = container.querySelector('button'); + expect(button).toHaveClass('opacity-70'); + }); + + Object.defineProperty(window, 'scrollY', { value: 500, writable: true }); + fireEvent.scroll(window); + + await waitFor(() => { + const button = container.querySelector('button'); + expect(button).toHaveClass('opacity-0'); + expect(button).toHaveClass('-right-5'); + }); + }); + + it('should scroll to top when button is clicked', async () => { + const scrollToSpy = vi.fn(); + window.scrollTo = scrollToSpy; + + Object.defineProperty(window, 'scrollY', { value: 1000, writable: true }); + + const { container } = render(); + fireEvent.scroll(window); + + await waitFor(() => { + const button = container.querySelector('button'); + expect(button).toHaveClass('opacity-70'); + }); + + const button = container.querySelector('button'); + fireEvent.click(button!); + + expect(scrollToSpy).toHaveBeenCalledTimes(1); + expect(scrollToSpy).toHaveBeenCalledWith({ top: 0, behavior: 'auto' }); + }); +}); diff --git a/components/ScrollToTopButton/__tests__/__snapshots__/ScrollToTopButton.test.tsx.snap b/components/ScrollToTopButton/__tests__/__snapshots__/ScrollToTopButton.test.tsx.snap new file mode 100644 index 000000000..bcc8e47e5 --- /dev/null +++ b/components/ScrollToTopButton/__tests__/__snapshots__/ScrollToTopButton.test.tsx.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ScrollToTopButton > should render with required props 1`] = ` + +`; diff --git a/pages/_app.tsx b/pages/_app.tsx index 5c56ee8ae..a8cd7ac09 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -10,13 +10,13 @@ import FontFaceObserver from 'fontfaceobserver'; import hash from 'object-hash'; import LogRocket from 'logrocket'; import setupLogRocketReact from 'logrocket-react'; -import ScrollUpButton from 'react-scroll-up-button'; import { gtag } from 'common/utils/thirdParty/gtag'; import Nav from 'components/Nav/Nav'; import Footer from 'components/Footer/Footer'; import 'common/styles/globals.css'; import type { AppProps } from 'next/app'; import type NextErrorComponent from 'next/error'; +import { ScrollToTopButton } from 'components/ScrollToTopButton/ScrollToTopButton'; const isProduction = process.env.NODE_ENV === 'production'; @@ -39,7 +39,7 @@ function Layout({ children }: PropsWithChildren) {