diff --git a/.eslintrc.js b/.eslintrc.js index e37cfd6..3e135b5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,10 +1,10 @@ - -module.exports = { - extends: ["next", "next/core-web-vitals"], - rules: { - "@next/next/no-img-element": 0, // TODO: Remove when Image component is updated - "react/display-name": 0, - "no-unused-vars": 1, - "react/no-unknown-property": 0, - }, -}; + +module.exports = { + extends: ["next", "next/core-web-vitals"], + rules: { + "@next/next/no-img-element": 0, // TODO: Remove when Image component is updated + "react/display-name": 0, + "no-unused-vars": 1, + "react/no-unknown-property": 0, + }, +}; diff --git a/pages/_error.js b/app/_error.js similarity index 100% rename from pages/_error.js rename to app/_error.js diff --git a/pages/error.js b/app/error/page.js similarity index 100% rename from pages/error.js rename to app/error/page.js diff --git a/pages/genre/index.js b/app/genre/page.js similarity index 93% rename from pages/genre/index.js rename to app/genre/page.js index a57201f..1239dee 100644 --- a/pages/genre/index.js +++ b/app/genre/page.js @@ -1,7 +1,7 @@ +'use client' import { useEffect, useState } from 'react'; -import Head from 'next/head'; -import Router, { useRouter } from 'next/router'; +import { useRouter } from 'next/router'; import { useDispatch, useSelector } from 'react-redux'; import { animateScroll as scroll } from 'react-scroll'; @@ -84,9 +84,7 @@ const Genre = () => { return ( <PageWrapper> <PaddingWrapper> - <Head> - <title>{`${general.selectedMenuItemName} Movies`}</title> - </Head> + <Metadata title={`${general.selectedMenuItemName} Movies`} /> <Header title={general.selectedMenuItemName} subtitle='movies' /> @@ -101,4 +99,4 @@ const Genre = () => { ); }; -export default Genre; +export default Genre; \ No newline at end of file diff --git a/app/layout.jsx b/app/layout.jsx new file mode 100644 index 0000000..eaee83a --- /dev/null +++ b/app/layout.jsx @@ -0,0 +1,16 @@ +import { Inter } from "next/font/google"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ children }) { + return ( + <html lang="en"> + <body className={inter.className}>{children}</body> + </html> + ); +} diff --git a/pages/list/add-or-edit/index.js b/app/list/add-or-edit/page.js similarity index 98% rename from pages/list/add-or-edit/index.js rename to app/list/add-or-edit/page.js index f0db959..ee6b37d 100644 --- a/pages/list/add-or-edit/index.js +++ b/app/list/add-or-edit/page.js @@ -1,8 +1,9 @@ +'use client' import { useState, useEffect } from 'react'; import Router, { useRouter } from 'next/router'; -import Head from 'next/head'; +import { Metadata } from 'next/head'; import Header from 'parts/Header'; import NotFound from 'parts/NotFound'; @@ -171,9 +172,9 @@ const AddOrEdit = ({ if (editStatus === STATUSES.RESOLVED) { return ( <> - <Head> + <Metadata> <title>{listId ? listName : 'Create New List: Step1'}</title> - </Head> + </Metadata> <PageWrapper> <PaddingWrapper> <Header @@ -218,4 +219,4 @@ const AddOrEdit = ({ } }; -export default withAuth(AddOrEdit); +export default withAuth(AddOrEdit); \ No newline at end of file diff --git a/pages/list/add-or-remove-items/index.js b/app/list/add-or-remove-items/page.js similarity index 98% rename from pages/list/add-or-remove-items/index.js rename to app/list/add-or-remove-items/page.js index 158fde0..8e6d03f 100644 --- a/pages/list/add-or-remove-items/index.js +++ b/app/list/add-or-remove-items/page.js @@ -1,8 +1,9 @@ +'use client' import { useState, useEffect } from 'react'; import Router, { useRouter } from 'next/router'; -import Head from 'next/head'; +import { Metadata } from 'next/head'; import Header from 'parts/Header'; import NotFound from 'parts/NotFound'; @@ -204,9 +205,9 @@ const AddOrRemoveItems = ({ if (status === STATUSES.RESOLVED) { return ( <> - <Head> + <Metadata> <title>{movies.name}</title> - </Head> + </Metadata> <PageWrapper> <PaddingWrapper> <Header @@ -256,4 +257,4 @@ const AddOrRemoveItems = ({ } }; -export default withAuth(AddOrRemoveItems); +export default withAuth(AddOrRemoveItems); \ No newline at end of file diff --git a/pages/list/choose-image/index.js b/app/list/choose-image/page.js similarity index 97% rename from pages/list/choose-image/index.js rename to app/list/choose-image/page.js index 95f52e1..17d4d83 100644 --- a/pages/list/choose-image/index.js +++ b/app/list/choose-image/page.js @@ -1,3 +1,4 @@ +'use client' import { @@ -6,7 +7,7 @@ import { useCallback } from 'react'; import { useRouter } from 'next/router'; -import Head from 'next/head'; +import { Metadata } from 'next/head'; import { animateScroll as scroll } from 'react-scroll'; import Header from 'parts/Header'; @@ -23,7 +24,7 @@ import withAuth from 'utils/hocs/withAuth'; import QUERY_PARAMS from 'utils/constants/query-params'; import STATUSES from 'utils/constants/statuses'; import { TMDB_API_NEW_VERSION, TMDB_IMAGE_BASE_URL } from 'config/tmdb'; -import tmdbAPI from 'services/tmdbAPI'; +import tmdbAPI from 'services/tmdb'; const BACKDROP_STATUSES = { SELECTED: 'SELECTED', @@ -135,9 +136,9 @@ const ChooseImage = ({ if (status === STATUSES.RESOLVED) { return ( <> - <Head> + <Metadata> <title>{movies.name}</title> - </Head> + </Metadata> <PageWrapper> <PaddingWrapper> <Header @@ -190,4 +191,4 @@ const ChooseImage = ({ } }; -export default withAuth(withTheme(ChooseImage)); +export default withAuth(withTheme(ChooseImage)); \ No newline at end of file diff --git a/pages/list/index.js b/app/list/page.js similarity index 95% rename from pages/list/index.js rename to app/list/page.js index b709463..5735823 100644 --- a/pages/list/index.js +++ b/app/list/page.js @@ -1,6 +1,6 @@ +'use client' import { useEffect, useState } from 'react'; -import Head from 'next/head'; import { useRouter } from 'next/router'; import { animateScroll as scroll } from 'react-scroll'; @@ -82,9 +82,7 @@ const List = () => { if (status === STATUSES.RESOLVED) { return ( <> - <Head> - <title>{movies.name ?? 'List'}</title> - </Head> + <Metadata title={movies.name ?? 'List'} /> <PageWrapper> <PaddingWrapper> <Header @@ -111,4 +109,4 @@ const List = () => { } }; -export default List; +export default List; \ No newline at end of file diff --git a/pages/list/remove/index.js b/app/list/remove/page.js similarity index 97% rename from pages/list/remove/index.js rename to app/list/remove/page.js index 048d604..2d5e0d1 100644 --- a/pages/list/remove/index.js +++ b/app/list/remove/page.js @@ -1,7 +1,8 @@ +'use client' import { useState, useEffect } from 'react'; import Router, { useRouter } from 'next/router'; -import Head from 'next/head'; +import { Metadata } from 'next/head'; import Header from 'parts/Header'; import NotFound from 'parts/NotFound'; @@ -114,9 +115,9 @@ const Remove = ({ if (status === STATUSES.RESOLVED) { return ( <> - <Head> + <Metadata> <title>Delete List</title> - </Head> + </Metadata> <PageWrapper> <PaddingWrapper> <Header @@ -156,4 +157,4 @@ const Remove = ({ } }; -export default withAuth(Remove); +export default withAuth(Remove); \ No newline at end of file diff --git a/pages/movie/index.js b/app/movie/page.js similarity index 96% rename from pages/movie/index.js rename to app/movie/page.js index c011123..53b6fa1 100644 --- a/pages/movie/index.js +++ b/app/movie/page.js @@ -1,8 +1,9 @@ +'use client' import { useEffect } from 'react'; import Router, { useRouter } from 'next/router'; -import Head from 'next/head'; +import { Metadata } from 'next/head'; import { useDispatch, useSelector } from 'react-redux'; import { animateScroll as scroll } from 'react-scroll'; @@ -76,9 +77,9 @@ const Movie = () => { return ( <PageWrapper> - <Head> + <Metadata> <title>{`${movie.title} - Movie Library`}</title> - </Head> + </Metadata> <MovieSummary baseUrl={baseUrl} movie={movie} /> @@ -89,4 +90,4 @@ const Movie = () => { ); }; -export default Movie; +export default Movie; \ No newline at end of file diff --git a/pages/my-lists/index.js b/app/my-lists/page.js similarity index 95% rename from pages/my-lists/index.js rename to app/my-lists/page.js index 02c9cb8..65a1205 100644 --- a/pages/my-lists/index.js +++ b/app/my-lists/page.js @@ -1,7 +1,7 @@ +'use client' import { useEffect, useState } from 'react'; -import Head from 'next/head'; import { useRouter } from 'next/router'; import { animateScroll as scroll } from 'react-scroll'; @@ -81,9 +81,7 @@ const MyLists = ({ if (status === STATUSES.RESOLVED) { return ( <> - <Head> - <title>My Lists</title> - </Head> + <Metadata title="My Lists" /> <PageWrapper> <PaddingWrapper> <Header @@ -99,4 +97,4 @@ const MyLists = ({ } }; -export default withAuth(MyLists); +export default withAuth(MyLists); \ No newline at end of file diff --git a/pages/404.js b/app/not-found.js similarity index 100% rename from pages/404.js rename to app/not-found.js diff --git a/app/not-found.jsx b/app/not-found.jsx new file mode 100644 index 0000000..ed413d2 --- /dev/null +++ b/app/not-found.jsx @@ -0,0 +1,11 @@ +import Link from 'next/link'; + +export default function NotFound() { + return ( + <div> + <h2>Not Found</h2> + <p>Could not find requested resource</p> + <Link href="/">Return Home</Link> + </div> + ); +} diff --git a/pages/index.js b/app/page.js similarity index 89% rename from pages/index.js rename to app/page.js index c5af8c1..0f4f771 100644 --- a/pages/index.js +++ b/app/page.js @@ -1,3 +1,4 @@ +'use client' /** * TODO: * https://nextjs.org/blog/next-9#automatic-partial-static-export RE: https://github.com/vercel/next.js/discussions/10874 @@ -7,8 +8,7 @@ */ import { useEffect } from 'react'; -import Head from 'next/head'; -import Router, { useRouter } from 'next/router'; +import { useRouter } from 'next/router'; import { useDispatch, useSelector } from 'react-redux'; import { animateScroll as scroll } from 'react-scroll'; @@ -99,16 +99,7 @@ const Home = () => { return ( <> - <Head> - {/* MEMO: inspired by https://addyosmani.com/blog/preload-hero-images/ */} - <link - rel='preload' - as='fetch' - // TODO: page is hardcoded - href={`${TMDB_API_BASE_URL}/${TMDB_API_VERSION}/movie/popular?api_key=${TMDB_API_KEY}&page=1`} - crossOrigin='true' /> - <title>{`${general.selectedMenuItemName} Movies`}</title> - </Head> + <Metadata title={`${general.selectedMenuItemName} Movies`} /> {movies.loading ? ( <Loader /> ) : ( diff --git a/pages/person/index.js b/app/person/page.js similarity index 96% rename from pages/person/index.js rename to app/person/page.js index 38c8bc2..47f3cc2 100644 --- a/pages/person/index.js +++ b/app/person/page.js @@ -1,8 +1,9 @@ +'use client' import { useState, useEffect } from 'react'; import Router, { useRouter } from 'next/router'; -import Head from 'next/head'; +import { Metadata } from 'next/head'; import { useDispatch, useSelector } from 'react-redux'; import { animateScroll as scroll } from 'react-scroll'; @@ -85,9 +86,9 @@ const Person = () => { return ( <PageWrapper> - <Head> + <Metadata> <title>{`${person.name} - Person Library`}</title> - </Head> + </Metadata> <PersonSummary baseUrl={baseUrl} person={person} /> @@ -100,4 +101,4 @@ const Person = () => { ); }; -export default Person; +export default Person; \ No newline at end of file diff --git a/pages/search/index.js b/app/search/page.js similarity index 92% rename from pages/search/index.js rename to app/search/page.js index 0a3e598..12e7006 100644 --- a/pages/search/index.js +++ b/app/search/page.js @@ -1,8 +1,8 @@ +'use client' import { useEffect } from 'react'; -import Head from 'next/head'; -import Router, { useRouter } from 'next/router'; +import { useRouter } from 'next/router'; import { useDispatch, useSelector } from 'react-redux'; import { animateScroll as scroll } from 'react-scroll'; @@ -79,9 +79,7 @@ const Search = () => { return ( <PageWrapper> <PaddingWrapper> - <Head> - <title>{`${searchTerm} - Search Results`}</title> - </Head> + <Metadata title={`${searchTerm} - Search Results`} /> <Header title={searchTerm} subtitle='search results' /> @@ -94,4 +92,4 @@ const Search = () => { } }; -export default Search; +export default Search; \ No newline at end of file diff --git a/components/AnchorButton/index.js b/components/AnchorButton/index.js index 39cb57d..0b9cf7c 100644 --- a/components/AnchorButton/index.js +++ b/components/AnchorButton/index.js @@ -1,5 +1,7 @@ +import React from "react"; + import Button from 'components/UI/Button'; const AnchorButton = React.forwardRef(({ diff --git a/components/LinkButton/index.js b/components/LinkButton/index.js index a6aa5db..d068855 100644 --- a/components/LinkButton/index.js +++ b/components/LinkButton/index.js @@ -11,13 +11,10 @@ const LinkButton = ({ buttonProps = {} }) => ( <> - <Link - passHref - as={as} - href={href}> - <a {...anchorProps}> - <Button {...buttonProps} /> - </a> + <Link passHref as={as} href={href} {...anchorProps}> + + <Button {...buttonProps} /> + </Link> </> ); diff --git a/components/Logo/index.js b/components/Logo/index.js index 7af38db..a333fa0 100644 --- a/components/Logo/index.js +++ b/components/Logo/index.js @@ -16,7 +16,8 @@ const Logo = () => ( [QUERY_PARAMS.CATEGORY]: STATIC_MOVIE_CATEGORIES[0].name, [QUERY_PARAMS.PAGE]: 1 } - }}> + }} + legacyBehavior> <a> <picture> <source srcSet={LOGO_IMAGE_PATH} media='(min-width: 80em)' /> diff --git a/components/Menu/MenuItemLink/index.js b/components/Menu/MenuItemLink/index.js index a895a44..64c1449 100644 --- a/components/Menu/MenuItemLink/index.js +++ b/components/Menu/MenuItemLink/index.js @@ -1,4 +1,5 @@ +import React from "react"; import Link from 'next/link'; import withTheme from 'utils/hocs/withTheme'; @@ -12,10 +13,7 @@ const MenuItemLink = React.forwardRef(({ ...rest }, ref) => ( <> - <Link - href={href} - as={as} - passHref> + <Link href={href} as={as} passHref legacyBehavior> <a ref={ref} {...rest}> diff --git a/components/MovieSummary/MovieInfo/TheCastSection/Cast/PersonLink/index.js b/components/MovieSummary/MovieInfo/TheCastSection/Cast/PersonLink/index.js index d3bfb0f..92f20dd 100644 --- a/components/MovieSummary/MovieInfo/TheCastSection/Cast/PersonLink/index.js +++ b/components/MovieSummary/MovieInfo/TheCastSection/Cast/PersonLink/index.js @@ -18,7 +18,8 @@ const PersonLink = ({ [QUERY_PARAMS.ID]: person.id, [QUERY_PARAMS.PAGE]: 1 } - }}> + }} + legacyBehavior> <a> <Profile src={`${baseUrl}w${W185H278.WIDTH}${person.profile_path}`} alt={person.name} /> </a> diff --git a/components/MovieSummary/MovieInfo/TheGenresSection/GenreLink/index.js b/components/MovieSummary/MovieInfo/TheGenresSection/GenreLink/index.js index 50c1fbe..5e43683 100644 --- a/components/MovieSummary/MovieInfo/TheGenresSection/GenreLink/index.js +++ b/components/MovieSummary/MovieInfo/TheGenresSection/GenreLink/index.js @@ -19,7 +19,8 @@ const GenreLink = ({ [QUERY_PARAMS.NAME]: genre.name, [QUERY_PARAMS.PAGE]: 1 } - }}> + }} + legacyBehavior> <a> <DotCircleIcon fill='currentColor' diff --git a/components/MyHead/index.js b/components/MyHead/index.js index 39555b6..3c12001 100644 --- a/components/MyHead/index.js +++ b/components/MyHead/index.js @@ -1,22 +1,35 @@ - -import Head from 'next/head'; +import { NextSeo } from 'next-seo'; import { LOGO_IMAGE_PATH, DARK_TMDB_IMAGE_PATH, LIGHT_TMDB_IMAGE_PATH } from 'utils/constants/image-paths'; const MyHead = ({ children }) => ( - <Head> - <link rel='preconnect' href='https://image.tmdb.org' /> - <link rel='preconnect' href='https://api.themoviedb.org' /> - {/* <link rel='preconnect' href='https://www.google-analytics.com' /> */} - {/* <link rel='preconnect' href='https://content-autofill.googleapis.com' /> */} - <link rel='preload' href={LOGO_IMAGE_PATH} as='image' media='(min-width: 80em)' /> - <link rel='preload' href={DARK_TMDB_IMAGE_PATH} as='image' media='(prefers-color-scheme: dark) and (min-width: 80em)' /> - <link rel='preload' href={LIGHT_TMDB_IMAGE_PATH} as='image' media='(prefers-color-scheme: light) and (min-width: 80em)' /> - {/* MEMO: inspired by https://web.dev/optimize-lcp/#establish-third-party-connections-early */} - {/* <link rel='dns-prefetch' href='https://image.tmdb.org' /> - <link rel='dns-prefetch' href='https://api.themoviedb.org' /> */} + <> + <NextSeo + openGraph={{ + images: [ + { + url: LOGO_IMAGE_PATH, + width: 1200, + height: 630, + alt: 'Logo', + }, + { + url: DARK_TMDB_IMAGE_PATH, + width: 1200, + height: 630, + alt: 'Dark Image', + }, + { + url: LIGHT_TMDB_IMAGE_PATH, + width: 1200, + height: 630, + alt: 'Light Image', + }, + ], + }} + /> {children} - </Head> + </> ); -export default MyHead; +export default MyHead; \ No newline at end of file diff --git a/components/PosterLink/index.js b/components/PosterLink/index.js index 73291b2..23cbbd9 100644 --- a/components/PosterLink/index.js +++ b/components/PosterLink/index.js @@ -1,4 +1,5 @@ +import React from "react"; import Link from 'next/link'; import withTheme from 'utils/hocs/withTheme'; @@ -11,10 +12,7 @@ const PosterLink = React.forwardRef(({ ...rest }, ref) => ( <> - <Link - passHref - as={as} - href={href}> + <Link passHref as={as} href={href} legacyBehavior> <a ref={ref} {...rest}> diff --git a/components/UI/TheSelectSearch/index.js b/components/UI/TheSelectSearch/index.js index 51d1496..3b8ec0b 100644 --- a/components/UI/TheSelectSearch/index.js +++ b/components/UI/TheSelectSearch/index.js @@ -1,5 +1,7 @@ +import React from "react"; + import SelectSearch from 'react-select-search/dist/cjs'; import clsx from 'clsx'; diff --git a/containers/DarkModeToggle/index.js b/containers/DarkModeToggle/index.js index d374d71..dd20167 100644 --- a/containers/DarkModeToggle/index.js +++ b/containers/DarkModeToggle/index.js @@ -1,7 +1,4 @@ - - -// MEMO: inspired by https://web.dev/prefers-color-scheme/#the-lessdark-mode-togglegreater-custom-element -import Head from 'next/head'; +import { Metadata } from 'components/Metadata'; import clsx from 'clsx'; import useDarkMode from 'use-dark-mode'; @@ -21,83 +18,16 @@ const DarkModeToggle = ({ return ( <> - <Head> - {darkMode.value ? ( - <> - {/* TODO: block for now for toggle experience of the favicon depending on the light/dark mode */} - {/* <link - rel='icon' - href='/dark-favicon.ico' /> */} - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link - rel='apple-touch-icon' - sizes='180x180' - href='/dark-apple-touch-icon.png' /> - <link - rel='icon' - type='image/png' - sizes='32x32' - href='/dark-favicon-32x32.png' /> - <link - rel='icon' - type='image/png' - sizes='16x16' - href='/dark-favicon-16x16.png' /> - <link - rel='manifest' - href='/dark-manifest.webmanifest' /> - <link - rel='mask-icon' - href='/dark-safari-pinned-tab.svg' - color='#5bbad5' /> - <meta - name='msapplication-TileColor' - content='#da532c' /> - {/* TODO: hardcoded */} - <meta name='theme-color' content='#fafafa' /> - <meta - name='msapplication-config' - content='/dark-browserconfig.xml' /> - </> - ) : ( - <> - {/* TODO: block for now for toggle experience of the favicon depending on the light/dark mode */} - {/* <link - rel='icon' - href='/light-favicon.ico' /> */} - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link - rel='apple-touch-icon' - sizes='180x180' - href='/light-apple-touch-icon.png' /> - <link - rel='icon' - type='image/png' - sizes='32x32' - href='/light-favicon-32x32.png' /> - <link - rel='icon' - type='image/png' - sizes='16x16' - href='/light-favicon-16x16.png' /> - <link - rel='manifest' - href='/light-manifest.webmanifest' /> - <link - rel='mask-icon' - href='/light-safari-pinned-tab.svg' - color='#5bbad5' /> - <meta - name='msapplication-TileColor' - content='#da532c' /> - {/* TODO: hardcoded */} - <meta name='theme-color' content='#303030' /> - <meta - name='msapplication-config' - content='/light-browserconfig.xml' /> - </> - )} - </Head> + <Metadata + title={darkMode.value ? 'Dark Mode' : 'Light Mode'} + description={darkMode.value ? 'Switch to light mode' : 'Switch to dark mode'} + favicon={darkMode.value ? '/dark-favicon.ico' : '/light-favicon.ico'} + appleTouchIcon={darkMode.value ? '/dark-apple-touch-icon.png' : '/light-apple-touch-icon.png'} + manifest={darkMode.value ? '/dark-manifest.webmanifest' : '/light-manifest.webmanifest'} + maskIcon={darkMode.value ? '/dark-safari-pinned-tab.svg' : '/light-safari-pinned-tab.svg'} + msapplicationConfig={darkMode.value ? '/dark-browserconfig.xml' : '/light-browserconfig.xml'} + themeColor={darkMode.value ? '#fafafa' : '#303030'} + /> <div className={clsx('dark-mode-toggle', className)}> <button type='button' @@ -149,4 +79,4 @@ const DarkModeToggle = ({ ); }; -export default withTheme(DarkModeToggle); +export default withTheme(DarkModeToggle); \ No newline at end of file diff --git a/containers/ListActions/index.js b/containers/ListActions/index.js index bc14ec2..a1ed1d0 100644 --- a/containers/ListActions/index.js +++ b/containers/ListActions/index.js @@ -63,49 +63,47 @@ const ListActions = ({ } ]; - return ( - <> - <Modal - opened={shareModalOpened} - onClose={closeShareModalHandler} - title={`Share ${listName}`} - body={ - <TextInput - id='share-link' - label='URL' - defaultValue={ - `${typeof location !== 'undefined' ? location.origin : ''}/list?${[QUERY_PARAMS.ID]}=${listId}&${[QUERY_PARAMS.PAGE]}=${page}` - } - readOnly /> - } /> - <Navbar> - {listActions.map(listAction => ( - <NavbarItem - key={listAction.title} - invisible={listAction.invisible}> - {listAction.href ? ( - <Link - href={listAction.href}> - <a> - <TextButton style={{padding: 0}}> - {listAction.title} - </TextButton> - </a> - </Link> - ) : ( - <a> - <TextButton - style={{padding: 0}} - onClick={listAction.onClick}> - {listAction.title} - </TextButton> - </a> - )} - </NavbarItem> - ))} - </Navbar> - </> - ); + return <> + <Modal + opened={shareModalOpened} + onClose={closeShareModalHandler} + title={`Share ${listName}`} + body={ + <TextInput + id='share-link' + label='URL' + defaultValue={ + `${typeof location !== 'undefined' ? location.origin : ''}/list?${[QUERY_PARAMS.ID]}=${listId}&${[QUERY_PARAMS.PAGE]}=${page}` + } + readOnly /> + } /> + <Navbar> + {listActions.map(listAction => ( + <NavbarItem + key={listAction.title} + invisible={listAction.invisible}> + {listAction.href ? ( + (<Link + href={listAction.href}> + + <TextButton style={{padding: 0}}> + {listAction.title} + </TextButton> + + </Link>) + ) : ( + <a> + <TextButton + style={{padding: 0}} + onClick={listAction.onClick}> + {listAction.title} + </TextButton> + </a> + )} + </NavbarItem> + ))} + </Navbar> + </>; }; export default ListActions; diff --git a/containers/ListNavigation/index.js b/containers/ListNavigation/index.js index db9e4e5..0fe6170 100644 --- a/containers/ListNavigation/index.js +++ b/containers/ListNavigation/index.js @@ -93,7 +93,7 @@ const ListNavigation = ({ listId }) => { selected={pathname === listLink.href.pathname}> <Link href={listLink.href}> - <a>{listLink.title}</a> + {listLink.title} </Link> </NavbarItem> ))} diff --git a/containers/SearchBar/Form/index.js b/containers/SearchBar/Form/index.js index 630bc73..bff7b4c 100644 --- a/containers/SearchBar/Form/index.js +++ b/containers/SearchBar/Form/index.js @@ -1,5 +1,7 @@ +import React from "react"; + const Form = React.forwardRef(({ opened, theme, diff --git a/containers/SearchBar/Input/index.js b/containers/SearchBar/Input/index.js index 9666d16..f05f84a 100644 --- a/containers/SearchBar/Input/index.js +++ b/containers/SearchBar/Input/index.js @@ -1,4 +1,5 @@ +import React from "react"; const Input = React.forwardRef(({ opened, theme, diff --git a/containers/TheUser/index.js b/containers/TheUser/index.js index 86f4ac5..4d20173 100644 --- a/containers/TheUser/index.js +++ b/containers/TheUser/index.js @@ -26,54 +26,52 @@ const TheUser = ({ isAuthenticated } = useAuth(); - return ( - <> - {isAuthenticated ? ( - <DropdownMenu - align='right' - DropElement={() => ( - <AccountCircleIconButton - aria-label='User Profile' - color={COLOR_TYPES.SECONDARY} - className={className} - style={style} /> - )}> - <DropdownMenuItem> - <Link href={{ - pathname: LINKS.ADD_OR_EDIT_LIST.HREF - }}> - <a>Create New List</a> - </Link> - </DropdownMenuItem> - <DropdownMenuItem> - <Link href={{ - pathname: LINKS.MY_LISTS.HREF, - query: { - [QUERY_PARAMS.PAGE]: 1 - } - }}> - <a>My Lists</a> - </Link> - </DropdownMenuItem> - <DropdownMenuItem> - <TextButton - style={{padding: 0}} - onClick={logout}> - Logout - </TextButton> - </DropdownMenuItem> - </DropdownMenu> - ) : ( - <ExitToAppIconButton - aria-label='Log In' - color={COLOR_TYPES.SECONDARY} - className={className} - style={style} - loading={isPending} - onClick={login} /> - )} - </> - ); + return <> + {isAuthenticated ? ( + <DropdownMenu + align='right' + DropElement={() => ( + <AccountCircleIconButton + aria-label='User Profile' + color={COLOR_TYPES.SECONDARY} + className={className} + style={style} /> + )}> + <DropdownMenuItem> + <Link href={{ + pathname: LINKS.ADD_OR_EDIT_LIST.HREF + }}> + Create New List + </Link> + </DropdownMenuItem> + <DropdownMenuItem> + <Link href={{ + pathname: LINKS.MY_LISTS.HREF, + query: { + [QUERY_PARAMS.PAGE]: 1 + } + }}> + My Lists + </Link> + </DropdownMenuItem> + <DropdownMenuItem> + <TextButton + style={{padding: 0}} + onClick={logout}> + Logout + </TextButton> + </DropdownMenuItem> + </DropdownMenu> + ) : ( + <ExitToAppIconButton + aria-label='Log In' + color={COLOR_TYPES.SECONDARY} + className={className} + style={style} + loading={isPending} + onClick={login} /> + )} + </>; }; export default TheUser; diff --git a/jsconfig.json b/jsconfig.json index 0ccb2da..b639b0f 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,14 +1,5 @@ - -{ - "compilerOptions": { - // This must be specified if "paths" is set - "baseUrl": ".", - // Relative to "baseUrl" - "paths": { - "*": [ - "*", - "/*" - ] - } - } -} +{ + "compilerOptions": { + "baseUrl": "." + } +} \ No newline at end of file diff --git a/package.json b/package.json index 3704a5c..db09453 100644 --- a/package.json +++ b/package.json @@ -15,30 +15,30 @@ "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build" }, "dependencies": { - "@artsy/fresnel": "^1.2.0", - "@loadable/component": "^5.13.1", - "clsx": "^1.1.1", - "next": "^12.2.5", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-glider": "^2.0.1", - "react-lazyload": "^3.0.0", - "react-modal-video": "^1.2.3", - "react-redux": "^7.2.0", - "react-scroll": "^1.8.1", - "react-select-search": "^2.0.4", - "redaxios": "^0.3.0", - "redux": "^4.0.5", - "redux-thunk": "^2.3.0", + "@artsy/fresnel": "^6.1.0", + "@loadable/component": "^5.15.3", + "clsx": "^2.0.0", + "next": "^13.4.19", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-glider": "^4.0.2", + "react-lazyload": "^3.2.0", + "react-modal-video": "^2.0.1", + "react-redux": "^8.1.2", + "react-scroll": "^1.8.9", + "react-select-search": "^4.1.6", + "redaxios": "^0.5.1", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", "use-dark-mode": "^2.3.1" }, "devDependencies": { - "@next/bundle-analyzer": "^9.5.3", - "@svgr/webpack": "^5.4.0", + "@next/bundle-analyzer": "^13.4.19", + "@svgr/webpack": "^8.1.0", "@zeit/next-source-maps": "0.0.3", - "cross-env": "^7.0.2", - "eslint": "^7.4.0", - "eslint-config-next": "^11.0.1", + "cross-env": "^7.0.3", + "eslint": "^8.48.0", + "eslint-config-next": "^13.4.19", "redux-devtools-extension": "^2.13.8", "redux-logger": "^3.0.6" } diff --git a/pages/_app.js b/pages/_app.js deleted file mode 100644 index ecb3e66..0000000 --- a/pages/_app.js +++ /dev/null @@ -1,32 +0,0 @@ - - -import { Provider } from 'react-redux'; -import globalStyles from 'styles/global'; - -import { useStore } from 'store'; -import ThemeProvider from 'utils/hocs/ThemeProvider'; -import Layout from 'parts/Layout'; -import { AuthProvider } from 'utils/hocs/AuthProvider'; - -const MyApp = ({ Component, pageProps }) => { - const store = useStore(pageProps.initialReduxState); - - return ( - <> - <Provider store={store}> - <ThemeProvider> - <AuthProvider> - <Layout> - <Component {...pageProps} /> - </Layout> - </AuthProvider> - </ThemeProvider> - </Provider> - <style jsx global> - {globalStyles} - </style> - </> - ); -}; - -export default MyApp; diff --git a/pages/_document.js b/pages/_document.js deleted file mode 100644 index 67b33bf..0000000 --- a/pages/_document.js +++ /dev/null @@ -1,151 +0,0 @@ - - -import Document, { Html, Head, Main, NextScript } from 'next/document'; - -import Script from 'utils/hocs/Script'; -import CLASS_NAMES from 'utils/constants/class-names'; -import { mediaStyles } from 'utils/helpers/media'; - -class MyDocument extends Document { - static async getInitialProps(ctx) { - const initialProps = await Document.getInitialProps(ctx); - return { ...initialProps }; - } - - render() { - return ( - <Html lang='en'> - <Head> - <meta charSet='utf-8' /> - <meta name='supported-color-schemes' content='dark light' /> - <meta name='color-scheme' content='dark light' /> - {/** - * MEMO: inspired by https://web.dev/prefers-color-scheme/#activating-dark-mode-in-the-operating-system. - * for now we take the approach https://github.com/GoogleChromeLabs/dark-mode-toggle#-using-a-css-class-that-you-toggle - * as it's not supported in SSR environment. - */} - {/* <Script> - {() => { - // If `prefers-color-scheme` is not supported, fall back to light mode. - // In this case, light.css will be downloaded with `highest` priority. - if (window.matchMedia('(prefers-color-scheme: dark)').media === 'not all') { - document.documentElement.style.display = 'none'; - document.head.insertAdjacentHTML( - 'beforeend', - '<link rel="stylesheet" href="/assets/css/light.css" onload="document.documentElement.style.display = \'\'">' - ); - } - }} - </Script> */} - {/* MEMO: inspired by https://web.dev/prefers-color-scheme/#loading-strategy */} - {/* - Conditionally either load the light or the dark stylesheet. The matching file - will be downloaded with `highest`, the non-matching file with `lowest` - priority. If the browser doesn't support `prefers-color-scheme`, the media - query is unknown and the files are downloaded with `lowest` priority (but - above I already force `highest` priority for my default light experience). - */} - {/* <link - rel='stylesheet' - href='/assets/css/dark.css' - media='(prefers-color-scheme: dark)' /> - <link - rel='stylesheet' - href='/assets/css/light.css' - media='(prefers-color-scheme: light)' /> */} - - {/* Primary Meta Tags */} - <meta name='title' content='Next.js Movies' /> - <meta name='description' content='The Movies App is a non-trivial demo application built on top of the TMDB (The Movie Database) API' /> - - {/* Open Graph / Facebook */} - <meta property='og:type' content='website' /> - <meta property='og:url' content='https://movies.zaps.dev' /> - <meta property='og:title' content='Next.js Movies' /> - <meta property='og:description' content='The Movies App is a non-trivial demo application built on top of the TMDB (The Movie Database) API' /> - <meta property='og:image' content='https://movies.zaps.dev/movies-meta-image.jpg' /> - <meta property='og:image:width' content='1200' /> - <meta property='og:image:height' content='628' /> - - {/* Twitter */} - <meta property='twitter:card' content='summary_large_image' /> - <meta property='twitter:url' content='https://movies.zaps.dev' /> - <meta property='twitter:title' content='Next.js Movies' /> - <meta property='twitter:description' content='The Movies App is a non-trivial demo application built on top of the TMDB (The Movie Database) API' /> - <meta property='twitter:image' content='https://movies.zaps.dev/movies-meta-image.jpg' /> - - <style - type='text/css' - dangerouslySetInnerHTML={{__html: mediaStyles}} /> - {/* Adds an event listener to capture uncaught errors. */} - <script - dangerouslySetInnerHTML={{ - __html: ` - addEventListener('error', window.__e=function f(e){f.q=f.q||[];f.q.push(e)}); - ` - }} /> - {/* <script async src="https://www.google-analytics.com/analytics.js"></script> */} - </Head> - <body className={CLASS_NAMES.LIGHT}> - {/* MEMO: inspired by https://github.com/donavon/use-dark-mode#that-flash */} - <Script> - {() => { - // Insert this script in your index.html right after the <body> tag. - // This will help to prevent a flash if dark mode is the default. - - (function() { - // Change these if you use something different in your hook. - var storageKey = 'darkMode'; - var classNameDark = 'dark'; - var classNameLight = 'light'; - - function setClassOnDocumentBody(darkMode) { - document.body.classList.add(darkMode ? classNameDark : classNameLight); - document.body.classList.remove(darkMode ? classNameLight : classNameDark); - } - - var preferDarkQuery = '(prefers-color-scheme: dark)'; - var mql = window.matchMedia(preferDarkQuery); - var supportsColorSchemeQuery = mql.media === preferDarkQuery; - var localStorageTheme = null; - try { - localStorageTheme = localStorage.getItem(storageKey); - } catch (err) {} - var localStorageExists = localStorageTheme !== null; - if (localStorageExists) { - localStorageTheme = JSON.parse(localStorageTheme); - } - - // Determine the source of truth - if (localStorageExists) { - // source of truth from localStorage - setClassOnDocumentBody(localStorageTheme); - } else if (supportsColorSchemeQuery) { - // source of truth from system - setClassOnDocumentBody(mql.matches); - localStorage.setItem(storageKey, mql.matches); - } else { - // source of truth from document.body - var isDarkMode = document.body.classList.contains(classNameDark); - localStorage.setItem(storageKey, JSON.stringify(isDarkMode)); - } - })(); - }} - </Script> - <Main /> - <NextScript /> - {/* TODO: transpile */} - <script - type='module' - dangerouslySetInnerHTML={{ - __html: ` - import('/analytics/base.min.js').then(analytics => analytics.init()); - ` - }} /> - </body> - </Html> - ); - } -} - -export default MyDocument; diff --git a/parts/ErrorBox/index.js b/parts/ErrorBox/index.js index d83a4bf..a0072c2 100644 --- a/parts/ErrorBox/index.js +++ b/parts/ErrorBox/index.js @@ -1,7 +1,4 @@ - - import { useEffect } from 'react'; -import Head from 'next/head'; import { useDispatch, useSelector } from 'react-redux'; import { animateScroll as scroll } from 'react-scroll'; import clsx from 'clsx'; @@ -83,9 +80,7 @@ const ErrorBox = ({ return ( <> <PageWrapper className='error-box'> - <Head> - <title>Oooops!</title> - </Head> + <Metadata title="Oooops!" /> <div className='title-section'> <Title theme={theme}> {statusCode @@ -137,4 +132,4 @@ const ErrorBox = ({ ); }; -export default withTheme(ErrorBox); +export default withTheme(ErrorBox); \ No newline at end of file diff --git a/public/analytics/base.min.js b/public/analytics/base.min.js index 224c1b3..d77b9d1 100644 --- a/public/analytics/base.min.js +++ b/public/analytics/base.min.js @@ -1 +1 @@ -const TRACKING_ID="UA-172478909-4",TRACKING_VERSION="1",NULL_VALUE="(not set)",dimensions={TRACKING_VERSION:"dimension1",CLIENT_ID:"dimension2",WINDOW_ID:"dimension3",HIT_ID:"dimension4",HIT_TIME:"dimension5",HIT_TYPE:"dimension6",HIT_SOURCE:"dimension7",VISIBILITY_STATE:"dimension8"},metrics={RESPONSE_END_TIME:"metric1",DOM_LOAD_TIME:"metric2",WINDOW_LOAD_TIME:"metric3"};export const init=()=>{window.ga=window.ga||((...e)=>(ga.q=ga.q||[]).push(e)),createTracker(),trackErrors(),trackCustomDimensions(),sendInitialPageview(),sendNavigationTimingMetrics()};export const trackError=(e={},n={})=>{ga("send","event",Object.assign({eventCategory:"Error",eventAction:e.name||"(no error name)",eventLabel:`${e.message}\n${e.stack||"(no stack trace)"}`,nonInteraction:!0},n))};const createTracker=()=>{ga("create",TRACKING_ID,"auto"),ga("set","transport","beacon")},trackErrors=()=>{const e=window.__e&&window.__e.q||[],n=e=>{const n=e.error||{message:`${e.message} (${e.lineno}:${e.colno})`};trackError(n,{eventCategory:"Uncaught Error"})};for(let t of e)n(t);window.addEventListener("error",n)},trackCustomDimensions=()=>{Object.keys(dimensions).forEach(e=>{ga("set",dimensions[e],NULL_VALUE)}),ga(e=>{e.set({[dimensions.TRACKING_VERSION]:"1",[dimensions.CLIENT_ID]:e.get("clientId"),[dimensions.WINDOW_ID]:uuid()})}),ga(e=>{const n=e.get("buildHitTask");e.set("buildHitTask",e=>{const t=e.get("queueTime")||0;e.set(dimensions.HIT_TIME,String(new Date-t),!0),e.set(dimensions.HIT_ID,uuid(),!0),e.set(dimensions.HIT_TYPE,e.get("hitType"),!0),e.set(dimensions.VISIBILITY_STATE,document.visibilityState,!0),n(e)})})},sendInitialPageview=()=>{ga("send","pageview",{[dimensions.HIT_SOURCE]:"pageload"})},sendNavigationTimingMetrics=()=>{if(!window.performance||!window.performance.timing)return;if("complete"!=document.readyState)return void window.addEventListener("load",sendNavigationTimingMetrics);const e=performance.timing,n=e.navigationStart,t=Math.round(e.responseEnd-n),i=Math.round(e.domContentLoadedEventStart-n),o=Math.round(e.loadEventStart-n);((...e)=>e.every(e=>e>0&&e<6e6))(t,i,o)&&ga("send","event",{eventCategory:"Navigation Timing",eventAction:"track",eventLabel:NULL_VALUE,nonInteraction:!0,[metrics.RESPONSE_END_TIME]:t,[metrics.DOM_LOAD_TIME]:i,[metrics.WINDOW_LOAD_TIME]:o})},uuid=function e(n){return n?(n^16*Math.random()>>n/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,e)}; \ No newline at end of file +const TRACKING_ID="UA-172478909-4",TRACKING_VERSION="1",NULL_VALUE="(not set)",dimensions={TRACKING_VERSION:"dimension1",CLIENT_ID:"dimension2",WINDOW_ID:"dimension3",HIT_ID:"dimension4",HIT_TIME:"dimension5",HIT_TYPE:"dimension6",HIT_SOURCE:"dimension7",VISIBILITY_STATE:"dimension8"},metrics={RESPONSE_END_TIME:"metric1",DOM_LOAD_TIME:"metric2",WINDOW_LOAD_TIME:"metric3"};export const init=()=>{window.ga=window.ga||((...e)=>(ga.q=ga.q||[]).push(e)),createTracker(),trackErrors(),trackCustomDimensions(),sendInitialPageview(),sendNavigationTimingMetrics()};export const trackError=(e={},n={})=>{ga("send","event",Object.assign({eventCategory:"Error",eventAction:e.name||"(no error name)",eventLabel:`${e.message}\n${e.stack||"(no stack trace)"}`,nonInteraction:!0},n))};const createTracker=()=>{ga("create",TRACKING_ID,"auto"),ga("set","transport","beacon")},trackErrors=()=>{const e=window.__e&&window.__e.q||[],n=e=>{const n=e.error||{message:`${e.message} (${e.lineno}:${e.colno})`};trackError(n,{eventCategory:"Uncaught Error"})};for(let t of e)n(t);window.addEventListener("error",n)},trackCustomDimensions=()=>{Object.keys(dimensions).forEach(e=>{ga("set",dimensions[e],NULL_VALUE)}),ga(e=>{e.set({[dimensions.TRACKING_VERSION]:"1",[dimensions.CLIENT_ID]:e.get("clientId"),[dimensions.WINDOW_ID]:uuid()})}),ga(e=>{const n=e.get("buildHitTask");e.set("buildHitTask",e=>{const t=e.get("queueTime")||0;e.set(dimensions.HIT_TIME,String(new Date-t),!0),e.set(dimensions.HIT_ID,uuid(),!0),e.set(dimensions.HIT_TYPE,e.get("hitType"),!0),e.set(dimensions.VISIBILITY_STATE,document.visibilityState,!0),n(e)})})},sendInitialPageview=()=>{ga("send","pageview",{[dimensions.HIT_SOURCE]:"pageload"})},sendNavigationTimingMetrics=()=>{if(!window.performance||!window.performance.timing)return;if("complete"!=document.readyState)return void window.addEventListener("load",sendNavigationTimingMetrics);const e=performance.timing,n=e.navigationStart,t=Math.round(e.responseEnd-n),i=Math.round(e.domContentLoadedEventStart-n),o=Math.round(e.loadEventStart-n);((...e)=>e.every(e=>e>0&&e<6e6))(t,i,o)&&ga("send","event",{eventCategory:"Navigation Timing",eventAction:"track",eventLabel:NULL_VALUE,nonInteraction:!0,[metrics.RESPONSE_END_TIME]:t,[metrics.DOM_LOAD_TIME]:i,[metrics.WINDOW_LOAD_TIME]:o})},uuid=function e(n){return n?(n^16*Math.random()>>n/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,e);}; \ No newline at end of file diff --git a/store.js b/store.js index da2a875..c8ac2a0 100644 --- a/store.js +++ b/store.js @@ -1,5 +1,3 @@ - - import { useMemo } from 'react'; import { createStore, applyMiddleware } from 'redux'; import thunkMiddleware from 'redux-thunk'; @@ -51,4 +49,4 @@ export const initializeStore = (preloadedState) => { export function useStore(initialState) { const store = useMemo(() => initializeStore(initialState), [initialState]); return store; -}; +} diff --git a/utils/hocs/withAuth/index.js b/utils/hocs/withAuth/index.js index a7711fb..72dbdd6 100644 --- a/utils/hocs/withAuth/index.js +++ b/utils/hocs/withAuth/index.js @@ -1,5 +1,7 @@ +import React from "react"; + import NotFound from 'parts/NotFound'; import { useAuth } from 'utils/hocs/AuthProvider'; diff --git a/utils/hocs/withTheme/index.js b/utils/hocs/withTheme/index.js index c8c0924..75209ff 100644 --- a/utils/hocs/withTheme/index.js +++ b/utils/hocs/withTheme/index.js @@ -1,4 +1,5 @@ +import React from "react"; /** * TODO: * Could remove all theme prop and just wrap it with this HOC whenever the component needs theme properties for consistency and simplicity. @@ -19,6 +20,6 @@ function withTheme(WrappedComponent) { </ThemeContext.Consumer> ); }); -}; +} export default withTheme; diff --git a/utils/hooks/useClickAway.js b/utils/hooks/useClickAway.js index 6238b2f..7471906 100644 --- a/utils/hooks/useClickAway.js +++ b/utils/hooks/useClickAway.js @@ -1,33 +1,33 @@ - -import { useEffect, useRef } from 'react'; - -import { off, on } from './util'; - -const defaultEvents = ['mousedown', 'touchstart']; - -const useClickAway = ( - ref, - onClickAway, - events = defaultEvents -) => { - const savedCallback = useRef(onClickAway); - useEffect(() => { - savedCallback.current = onClickAway; - }, [onClickAway]); - useEffect(() => { - const handler = event => { - const { current: el } = ref; - el && !el.contains(event.target) && savedCallback.current(event); - }; - for (const eventName of events) { - on(document, eventName, handler); - } - return () => { - for (const eventName of events) { - off(document, eventName, handler); - } - }; - }, [events, ref]); -}; - -export default useClickAway; + +import { useEffect, useRef } from 'react'; + +import { off, on } from './util'; + +const defaultEvents = ['mousedown', 'touchstart']; + +const useClickAway = ( + ref, + onClickAway, + events = defaultEvents +) => { + const savedCallback = useRef(onClickAway); + useEffect(() => { + savedCallback.current = onClickAway; + }, [onClickAway]); + useEffect(() => { + const handler = event => { + const { current: el } = ref; + el && !el.contains(event.target) && savedCallback.current(event); + }; + for (const eventName of events) { + on(document, eventName, handler); + } + return () => { + for (const eventName of events) { + off(document, eventName, handler); + } + }; + }, [events, ref]); +}; + +export default useClickAway; diff --git a/utils/hooks/useMedia.js b/utils/hooks/useMedia.js index 8a4352e..bcdd4b5 100644 --- a/utils/hooks/useMedia.js +++ b/utils/hooks/useMedia.js @@ -1,32 +1,32 @@ - -// MEMO: not used but keep it for future use -const isClient = typeof window === 'object'; - -import { useEffect, useState } from 'react'; - -const useMedia = (query, defaultState = false) => { - const [state, setState] = useState(isClient ? () => window.matchMedia(query).matches : defaultState); - - useEffect(() => { - let mounted = true; - const mql = window.matchMedia(query); - const onChange = () => { - if (!mounted) { - return; - } - setState(!!mql.matches); - }; - - mql.addListener(onChange); - setState(mql.matches); - - return () => { - mounted = false; - mql.removeListener(onChange); - }; - }, [query]); - - return state; -}; - -export default useMedia; + +// MEMO: not used but keep it for future use +const isClient = typeof window === 'object'; + +import { useEffect, useState } from 'react'; + +const useMedia = (query, defaultState = false) => { + const [state, setState] = useState(isClient ? () => window.matchMedia(query).matches : defaultState); + + useEffect(() => { + let mounted = true; + const mql = window.matchMedia(query); + const onChange = () => { + if (!mounted) { + return; + } + setState(!!mql.matches); + }; + + mql.addListener(onChange); + setState(mql.matches); + + return () => { + mounted = false; + mql.removeListener(onChange); + }; + }, [query]); + + return state; +}; + +export default useMedia;