From b73ca2e5136939ce44828b0f6bd855bca3941034 Mon Sep 17 00:00:00 2001 From: BenoitBellegarde Date: Sat, 23 Nov 2024 13:06:59 +0100 Subject: [PATCH 1/3] feat: Add font size mangement for tab page + improve browser resize detection --- src/components/TabPanel.tsx | 127 ++++++++++++++++++++------------- src/contexts/AppContext.tsx | 23 +++++- src/hooks/useBackgroundTabs.ts | 17 +++++ src/hooks/useDebounce.ts | 22 ------ src/hooks/useTabs.ts | 16 ++++- src/hooks/useWindowSize.ts | 33 +++++++++ src/pages/tab/[...slug].tsx | 42 +++++++++-- src/theme.ts | 1 - 8 files changed, 199 insertions(+), 82 deletions(-) create mode 100644 src/hooks/useBackgroundTabs.ts delete mode 100644 src/hooks/useDebounce.ts create mode 100644 src/hooks/useWindowSize.ts diff --git a/src/components/TabPanel.tsx b/src/components/TabPanel.tsx index bcc6465..7744dbe 100644 --- a/src/components/TabPanel.tsx +++ b/src/components/TabPanel.tsx @@ -8,35 +8,33 @@ import { Menu, MenuButton, MenuItem, + MenuItemOption, MenuList, + MenuOptionGroup, Skeleton, Text, Tooltip, useBreakpointValue, useColorModeValue, + useToast, } from '@chakra-ui/react' import HTMLReactParser from 'html-react-parser' import { GiGuitarHead } from 'react-icons/gi' import { RiHeartFill, RiHeartLine } from 'react-icons/ri' +import { MdFontDownload } from 'react-icons/md' import { FaCircleArrowDown } from 'react-icons/fa6' import { GiMusicalScore } from 'react-icons/gi' import { GiCrowbar } from 'react-icons/gi' import Difficulty from './Difficulty' import ChordDiagram from './ChordDiagram' import { Tab, UGChordCollection } from '../types/tabs' -import { - MouseEventHandler, - useEffect, - useLayoutEffect, - useRef, - useState, -} from 'react' +import { MouseEventHandler, useEffect, useRef, useState } from 'react' import { useRouter } from 'next/router' -import useDebounce from '../hooks/useDebounce' import { FaPlayCircle } from 'react-icons/fa' import ChordTransposer from './ChordTransposer' import BackingtrackPlayer from './BackingtrackPlayer' import Autoscroller from './Autoscroller' +import useAppStateContext from '../hooks/useAppStateContext' interface TabPanelProps { selectedTab: Tab @@ -55,17 +53,14 @@ export default function TabPanel({ refetchTab, }: TabPanelProps) { const router = useRouter() - const widthBrowser = useDebounce( - typeof document !== 'undefined' ? document.documentElement.clientWidth : 0, - 500, - ) + const { tabFontSize, setTabFontSize } = useAppStateContext() + const [chordsDiagrams, setChordsDiagrams] = useState( selectedTabContent?.chordsDiagrams, ) const [showAutoscroll, setShowAutoscroll] = useState(false) const [showBackingTrack, setShowBackingTrack] = useState(false) - const firstUpdate = useRef(true) const flexSongNameDirection = useBreakpointValue({ base: @@ -76,29 +71,16 @@ export default function TabPanel({ sm: 'row', }) const borderLightColor = useColorModeValue('gray.200', 'gray.700') - const paddingThirdRow = useBreakpointValue({ base: 2, sm: 1 }) - const widthThirdRow = useBreakpointValue({ base: '100%', sm: 'initial' }) + const widthThirdRow = useBreakpointValue({ base: '100%', md: 'initial' }) + const marginTopThirdRow = useBreakpointValue({ base: 0, md: 2 }) + const paddingTopThirdRow = useBreakpointValue({ base: 1, md: 0 }) + + const fontSizeValues = [50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150] useEffect(() => { setChordsDiagrams(selectedTabContent?.chordsDiagrams) }, [selectedTabContent]) - // Refetch tab when resizing browser or changing orientation to get the updated responsive tab from UG - if (typeof document !== 'undefined') { - // Hook executed only in browser to prevent NextJS SSR to return a warning - // eslint-disable-next-line react-hooks/rules-of-hooks - useLayoutEffect(() => { - if (firstUpdate.current) { - firstUpdate.current = false - return - } - if (!selectedTabContent) return - - refetchTab() - // Disabling this effect on the first load of the tab to prevent triggering the toast only because of scrollbar appearing/disappearing - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [widthBrowser, refetchTab]) - } return ( <> )} - + - Difficulty + Key {' '} - + + {selectedTabContent?.tonality} {' '} - Tuning + Capo {' '} - - {selectedTabContent?.tuning.join(' ')} + + {selectedTabContent?.capo} {' '} - Key + Difficulty {' '} - - {selectedTabContent?.tonality} + {' '} - Capo + Tuning {' '} - - {selectedTabContent?.capo} + + {selectedTabContent?.tuning.join(' ')} {' '} + {chordsDiagrams && selectedTabContent?.type === 'Chords' && ( )} - + + + } + leftIcon={} + > + Font size + + + { + setTabFontSize(parseInt(selectedSize)) + }} + > + {fontSizeValues.map((size) => ( + + {size}% + + ))} + + + @@ -336,7 +359,11 @@ export default function TabPanel({ justifyContent="center" > - + {selectedTabContent && HTMLReactParser(selectedTabContent?.htmlTab)} diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index 813b8dd..12315b8 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -7,7 +7,8 @@ import { Dispatch, } from 'react' import { TAB_SOURCES } from '../constants' -import useDebounce from '../hooks/useDebounce' +import useBackgroundTabs from '../hooks/useBackgroundTabs' +import useWindowSize from '../hooks/useWindowSize' import useLocalStorage from '../hooks/useLocalStorage' import useTabs from '../hooks/useTabs' import useTabsList from '../hooks/useTabsList' @@ -26,8 +27,13 @@ interface AppState { setFavorites: Dispatch> selectedTab: Tab setSelectedTab: Dispatch> + tabFontSize: number + setTabFontSize: Dispatch> isLoadingTab: boolean + widthBrowser: number selectedTabContent: Tab + isLoadingTabBackground: boolean + selectedTabContentBackground: Tab handleClickFavorite: MouseEventHandler favoriteActive: boolean setFavoriteActive: Dispatch> @@ -56,13 +62,20 @@ export function AppStateProvider({ children }) { rating: 0, type: 'Tab', }) + const [tabFontSize, setTabFontSize] = useState(100) + const [widthBrowser, heightBrowser] = useWindowSize() const toast = useToast() const { isLoading: isLoadingTab, data: selectedTabContent, refetch: refetchTab, - } = useTabs(selectedTab.url) + } = useTabs(selectedTab.url, tabFontSize, widthBrowser) + + const { + isLoading: isLoadingTabBackground, + data: selectedTabContentBackground, + } = useBackgroundTabs(selectedTab.url, tabFontSize, widthBrowser) const { isLoading: isLoadingTabList, @@ -93,6 +106,7 @@ export function AppStateProvider({ children }) { ? 'Song added to your favorites' : 'Song removed from your favorites', status: isAdded ? 'success' : 'info', + position: 'top-right', duration: 2000, }) } @@ -112,8 +126,13 @@ export function AppStateProvider({ children }) { setFavorites, selectedTab, setSelectedTab, + tabFontSize, + setTabFontSize, isLoadingTab, selectedTabContent, + isLoadingTabBackground, + widthBrowser, + selectedTabContentBackground, handleClickFavorite, favoriteActive, setFavoriteActive, diff --git a/src/hooks/useBackgroundTabs.ts b/src/hooks/useBackgroundTabs.ts new file mode 100644 index 0000000..0051efe --- /dev/null +++ b/src/hooks/useBackgroundTabs.ts @@ -0,0 +1,17 @@ +import { useQuery } from 'react-query' +import { getDatasTab } from './useTabs' + +export default function useBackgroundTabs( + url: string, + fontSize: number = 100, + widthBrowser: number, +) { + return useQuery( + ['getBackgroundTab', fontSize, widthBrowser], + async ({ signal }) => getDatasTab(url, fontSize, widthBrowser, signal), + { + enabled: url.length > 0, + cacheTime: 0, + }, + ) +} diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts deleted file mode 100644 index 9256389..0000000 --- a/src/hooks/useDebounce.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useState, useEffect } from 'react' - -export default function useDebounce(value: T, delay: number): T { - // State and setters for debounced value - const [debouncedValue, setDebouncedValue] = useState(value) - useEffect( - () => { - // Update debounced value after delay - const handler = setTimeout(() => { - setDebouncedValue(value) - }, delay) - // Cancel the timeout if value changes (also on delay change or unmount) - // This is how we prevent debounced value from updating ifs value is changed ... - // .. within the delay period. Timeout gets cleared and restarted. - return () => { - clearTimeout(handler) - } - }, - [value, delay], // Only re-call effect if value or delay changes - ) - return debouncedValue -} diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index cc0d9f2..696c72f 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -2,19 +2,29 @@ import { useQuery } from 'react-query' import { ApiResponseTab, Tab } from '../types/tabs' export const getDatasTab = async ( url: string, + fontSize: number, + widthBrowser: number, signal: AbortSignal, ): Promise => { const response = await fetch( - `/api/tab?q=${url}&width=${document.documentElement.clientWidth}&height=${document.documentElement.clientHeight}`, + `/api/tab?q=${url}&width=${Math.ceil( + widthBrowser * (1 - (fontSize - 100) / 100), + )}&height=${Math.ceil( + document.documentElement.clientHeight * (1 - (fontSize - 100) / 100), + )}`, { signal }, ) const parsedResponse: ApiResponseTab = await response.json() return parsedResponse.tab } -export default function useTabs(url: string) { +export default function useTabs( + url: string, + fontSize: number = 100, + widthBrowser: number, +) { return useQuery( ['getTab', url], - async ({ signal }) => getDatasTab(url, signal), + async ({ signal }) => getDatasTab(url, fontSize, widthBrowser, signal), { enabled: url.length > 0, }, diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts new file mode 100644 index 0000000..9434ec1 --- /dev/null +++ b/src/hooks/useWindowSize.ts @@ -0,0 +1,33 @@ +import { useState, useLayoutEffect } from 'react' + +export default function useWindowSize(): number[] { + const [size, setSize] = useState([ + typeof document !== 'undefined' ? window.innerWidth : 0, + typeof document !== 'undefined' ? window.innerHeight : 0, + ]) + const [debouncedSize, setDebouncedSize] = useState(size) + + useLayoutEffect(() => { + let timeoutId: NodeJS.Timeout + + function updateSize() { + clearTimeout(timeoutId) + const newSize = [window.innerWidth, window.innerHeight] + timeoutId = setTimeout(() => { + setDebouncedSize(newSize) + }, 500) + } + + window.addEventListener('resize', updateSize) + + // Initial update + updateSize() + + return () => { + clearTimeout(timeoutId) + window.removeEventListener('resize', updateSize) + } + }, []) + + return debouncedSize +} diff --git a/src/pages/tab/[...slug].tsx b/src/pages/tab/[...slug].tsx index 5a4c2d3..21e8f95 100644 --- a/src/pages/tab/[...slug].tsx +++ b/src/pages/tab/[...slug].tsx @@ -1,10 +1,10 @@ -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import TabPanel from '../../components/TabPanel' import Head from 'next/head' import useAppStateContext from '../../hooks/useAppStateContext' import { Tab } from '../../types/tabs' import { useRouter } from 'next/router' -import { Fade } from '@chakra-ui/react' +import { Fade, useToast } from '@chakra-ui/react' export default function TabPage(): JSX.Element { const router = useRouter() @@ -20,12 +20,19 @@ export default function TabPage(): JSX.Element { selectedTabContent, handleClickFavorite, refetchTab, + isLoadingTabBackground, + selectedTabContentBackground, } = useAppStateContext() + const toast = useToast() + const title = selectedTabContent ? `${selectedTabContent.name} by ${selectedTabContent.artist} - Ultimate Tab` : 'Tab - Ultimate Tab' + const [updatedResponsiveTab, setUpdatedResponsiveTab] = + useState(selectedTabContent) + useEffect(() => { if (slug) { setSelectedTab((prevState) => ({ @@ -35,6 +42,33 @@ export default function TabPage(): JSX.Element { } }, [slug, setSelectedTab]) + useEffect(() => { + if (!isLoadingTab) { + if (!isLoadingTabBackground) { + if (typeof selectedTabContentBackground !== 'undefined') { + setUpdatedResponsiveTab(selectedTabContentBackground) + } + } else { + toast.closeAll() + toast({ + description: 'Adapting tab for your browser...🛠️', + status: 'info', + position: 'top-right', + duration: 3000, + }) + } + } + }, [ + selectedTabContentBackground, + isLoadingTabBackground, + toast, + isLoadingTab, + ]) + + useEffect(() => { + setUpdatedResponsiveTab(selectedTabContent) + }, [selectedTabContent]) + return ( <> @@ -45,9 +79,9 @@ export default function TabPage(): JSX.Element { in={true} > el.url === selectedTab.url) !== 'undefined' diff --git a/src/theme.ts b/src/theme.ts index 71d077c..620a9e0 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -17,7 +17,6 @@ export const extendedTheme = extendTheme({ }, 'pre, code': { fontFamily: `'Poppins Mono', monospace !important`, - fontSize: 'md !important', }, '.PlayerRSWP': { bgColor: props.colorMode === 'dark' && '#1a202c !important', From 0d69eeae3212df7c39eb59355270184f4b05dc26 Mon Sep 17 00:00:00 2001 From: BenoitBellegarde Date: Sat, 23 Nov 2024 13:34:30 +0100 Subject: [PATCH 2/3] fix: Debug autoscroller play button inconsistency on mobile devices --- src/components/Autoscroller.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/Autoscroller.tsx b/src/components/Autoscroller.tsx index 97eba40..3a8d800 100644 --- a/src/components/Autoscroller.tsx +++ b/src/components/Autoscroller.tsx @@ -36,12 +36,21 @@ export default function Autoscroller({ const requestAnimationFrameRef = useRef(0) const manualScrollintervalRef = useRef(null) + if (typeof document !== 'undefined') { + var isTouch = 'ontouchstart' in document.documentElement + } + const formatSpeed = (scrollSpeed: number) => { const base100 = 200000 const percent = base100 / scrollSpeed return Math.round(percent * 100) } + const handlePlayButton = () => { + setIsScrolling((isScrolling) => !isScrolling) + setIsEnabled((isEnabled) => !isEnabled) + } + const resetRequestAnimationFrame = () => { cancelAnimationFrame(requestAnimationFrameRef.current) requestAnimationFrameRef.current = null @@ -157,10 +166,8 @@ export default function Autoscroller({ color: 'white', }} isActive={isEnabled} - onClick={() => { - setIsScrolling((isScrolling) => !isScrolling) - setIsEnabled((isEnabled) => !isEnabled) - }} + onTouchStart={handlePlayButton} + onMouseDown={!isTouch ? handlePlayButton : undefined} size={'sm'} boxShadow="md" fontWeight={'normal'} From 937af9e74c5636b0cf0f807d8f6fea0086dce9eb Mon Sep 17 00:00:00 2001 From: BenoitBellegarde Date: Sat, 23 Nov 2024 14:27:44 +0100 Subject: [PATCH 3/3] feat: Add github links + version --- README.md | 1 + src/components/Footer.tsx | 48 +++++++++++++++++++++++++++++++++++++++ src/components/Layout.tsx | 2 ++ src/pages/index.tsx | 17 ++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/components/Footer.tsx diff --git a/README.md b/README.md index 19757bf..7f5294a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A web application that delivers an enhanced, ads-free and fast responsive interf - Chords visualizer with official diagrams from Ultimate Guitar. - Chords transposer. - Autoscroll tab. +- Font size management - Backing track player (using YouTube API). - Add tabs to favorites without the need for an account (stored in local storage). diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..64820d7 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,48 @@ +import { Text, Stack, Divider, Link, IconButton, Flex } from '@chakra-ui/react' +import { FaGithub } from 'react-icons/fa' + +export default function Nav({}: {}): JSX.Element { + const version: string = process.env.NEXT_PUBLIC_UT_VERSION + return ( +
+ + + + + Built by{' '} + + Benoit Bellegarde + + + + + + ver {version} + + } + rounded="md" + /> + + +
+ ) +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index c5db309..790511c 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -2,6 +2,7 @@ import { Container, Flex } from '@chakra-ui/react' import { useRouter } from 'next/router' import { ReactNode, useRef } from 'react' import Backdrop from './Backdrop' +import Footer from './Footer' import Nav from './Nav' interface LayoutProps { @@ -20,6 +21,7 @@ export default function Layout({ children }: LayoutProps): JSX.Element { {children} +
diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e82fcb6..aac5064 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -9,6 +9,7 @@ import { Fade, } from '@chakra-ui/react' import Head from 'next/head' +import { FaGithub } from 'react-icons/fa' import { useRouter } from 'next/router' export default function Home(): JSX.Element { @@ -84,6 +85,22 @@ export default function Home(): JSX.Element { > Get started +