From 5544ceaec7c261d5a63390edc320432609fb979c Mon Sep 17 00:00:00 2001 From: Ido Shamun Date: Thu, 18 Mar 2021 14:05:38 +0200 Subject: [PATCH 1/3] feat: add settings panel The new settings panel allows changing the following: * Theme * Density of feed * Open links in the same tab or new tab * Hide read articles --- __tests__/Settings.tsx | 183 ++++++++++++++++++++++++++++++ __tests__/setup.ts | 16 --- components/Feed.tsx | 18 ++- components/FooterNavBar.tsx | 14 ++- components/Settings.tsx | 106 +++++++++++++++++ components/Switch.tsx | 30 ++++- components/cards/PostCard.tsx | 7 +- components/fields/Radio.tsx | 33 ++++++ components/fields/RadioItem.tsx | 47 ++++++++ components/layouts/MainLayout.tsx | 33 +++++- contexts/SettingsContext.ts | 15 +++ graphql/settings.ts | 29 +++++ hooks/useFeed.ts | 4 +- hooks/useSettings.ts | 120 ++++++++++++++++++++ icons/layout.svg | 11 ++ lib/theme.ts | 19 ---- pages/_app.tsx | 145 ++++++++++++----------- pages/settings.tsx | 9 ++ postcss.config.js | 2 +- styles/feed.module.css | 36 ++++++ styles/radio.module.css | 39 +++++++ tailwind.config.js | 9 +- 22 files changed, 802 insertions(+), 123 deletions(-) create mode 100644 __tests__/Settings.tsx create mode 100644 components/Settings.tsx create mode 100644 components/fields/Radio.tsx create mode 100644 components/fields/RadioItem.tsx create mode 100644 contexts/SettingsContext.ts create mode 100644 graphql/settings.ts create mode 100644 hooks/useSettings.ts create mode 100644 icons/layout.svg delete mode 100644 lib/theme.ts create mode 100644 pages/settings.tsx create mode 100644 styles/radio.module.css diff --git a/__tests__/Settings.tsx b/__tests__/Settings.tsx new file mode 100644 index 0000000000..0d89d06791 --- /dev/null +++ b/__tests__/Settings.tsx @@ -0,0 +1,183 @@ +import React, { ReactElement } from 'react'; +import nock from 'nock'; +import { MockedGraphQLResponse, mockGraphQL } from './helpers/graphql'; +import { + fireEvent, + queryByText, + render, + RenderResult, + screen, + waitFor, +} from '@testing-library/preact'; +import useSettings from '../hooks/useSettings'; +import SettingsContext from '../contexts/SettingsContext'; +import Settings from '../components/Settings'; +import { + RemoteSettings, + UPDATE_USER_SETTINGS_MUTATION, + USER_SETTINGS_QUERY, + UserSettingsData, +} from '../graphql/settings'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +beforeEach(() => { + jest.clearAllMocks(); + nock.cleanAll(); +}); + +const defaultSettings: RemoteSettings = { + theme: 'bright', + openNewTab: false, + showOnlyUnreadPosts: true, + spaciness: 'roomy', +}; + +const createSettingsMock = ( + settings: RemoteSettings = defaultSettings, +): MockedGraphQLResponse => ({ + request: { + query: USER_SETTINGS_QUERY, + }, + result: { + data: { + userSettings: settings, + }, + }, +}); + +const SettingsWithContext = ({ + userId, +}: { + userId: string | null; +}): ReactElement => { + const settingsContext = useSettings(userId, true); + return ( + + + + ); +}; + +const renderComponent = ( + mocks: MockedGraphQLResponse[] = [createSettingsMock()], + userId: string | null = '1', +): RenderResult => { + mocks.forEach(mockGraphQL); + const queryClient = new QueryClient(); + return render( + + + , + ); +}; + +it('should fetch remote settings', async () => { + renderComponent(); + await waitFor(() => expect(nock.isDone()).toBeTruthy()); + + const radio = await screen.findAllByRole('radio'); + await waitFor(() => + expect( + radio.find((el) => queryByText(el.parentElement, 'roomy')), + ).toBeChecked(), + ); + + const checkbox = await screen.findAllByRole('checkbox'); + await waitFor(() => + expect( + checkbox.find((el) => queryByText(el.parentElement, 'Light theme')), + ).toBeChecked(), + ); + await waitFor(() => + expect( + checkbox.find((el) => queryByText(el.parentElement, 'Hide read posts')), + ).toBeChecked(), + ); + await waitFor(() => + expect( + checkbox.find((el) => + queryByText(el.parentElement, 'Open links in new tab'), + ), + ).not.toBeChecked(), + ); +}); + +const testSettingsMutation = async ( + settings: Partial, + updateFunc: () => Promise, + initialSettings = defaultSettings, +) => { + renderComponent([createSettingsMock(initialSettings)]); + await waitFor(() => expect(nock.isDone()).toBeTruthy()); + + let mutationCalled = false; + mockGraphQL({ + request: { + query: UPDATE_USER_SETTINGS_MUTATION, + variables: { data: { ...defaultSettings, ...settings } }, + }, + result: () => { + mutationCalled = true; + return { data: { updateUserSettings: { updatedAt: new Date(0) } } }; + }, + }); + + if (initialSettings.theme === 'bright') { + const checkbox = await screen.findAllByRole('checkbox'); + await waitFor(() => + expect( + checkbox.find((el) => queryByText(el.parentElement, 'Light theme')), + ).toBeChecked(), + ); + } + + await updateFunc(); + await waitFor(() => expect(mutationCalled).toBeTruthy()); +}; + +it('should mutate density setting', () => + testSettingsMutation({ spaciness: 'cozy' }, async () => { + const radio = await screen.findAllByRole('radio'); + const cozy = radio.find((el) => queryByText(el.parentElement, 'cozy')); + fireEvent.change(cozy); + })); + +it('should set theme to dark mode setting', () => + testSettingsMutation({ theme: 'darcula' }, async () => { + const checkboxes = await screen.findAllByRole('checkbox'); + const checkbox = checkboxes.find((el) => + queryByText(el.parentElement, 'Light theme'), + ) as HTMLInputElement; + fireEvent.change(checkbox); + })); + +it('should set light to dark mode setting', () => + testSettingsMutation( + { theme: 'bright' }, + async () => { + const checkboxes = await screen.findAllByRole('checkbox'); + const checkbox = checkboxes.find((el) => + queryByText(el.parentElement, 'Light theme'), + ) as HTMLInputElement; + fireEvent.change(checkbox); + }, + { ...defaultSettings, theme: 'darcula' }, + )); + +it('should mutate hide read posts setting', () => + testSettingsMutation({ showOnlyUnreadPosts: false }, async () => { + const checkboxes = await screen.findAllByRole('checkbox'); + const checkbox = checkboxes.find((el) => + queryByText(el.parentElement, 'Hide read posts'), + ) as HTMLInputElement; + fireEvent.change(checkbox); + })); + +it('should mutate open links in new tab setting', () => + testSettingsMutation({ openNewTab: true }, async () => { + const checkboxes = await screen.findAllByRole('checkbox'); + const checkbox = checkboxes.find((el) => + queryByText(el.parentElement, 'Open links in new tab'), + ) as HTMLInputElement; + fireEvent.change(checkbox); + })); diff --git a/__tests__/setup.ts b/__tests__/setup.ts index 564380a589..7448c615c4 100644 --- a/__tests__/setup.ts +++ b/__tests__/setup.ts @@ -11,22 +11,6 @@ process.env.NEXT_PUBLIC_API_URL = 'http://localhost:3000'; window.ga = ((...args) => {}) as UniversalAnalytics.ga; /* eslint-disable @typescript-eslint/no-explicit-any */ -jest.mock('../hooks/usePersistentState', () => ({ - __esModule: true, - default: jest - .fn() - .mockImplementation( - ( - key: string, - initialValue: any, - valueWhenCacheEmpty: any, - ): [any, (value: any) => Promise, boolean] => [ - valueWhenCacheEmpty, - jest.fn().mockResolvedValue(undefined), - true, - ], - ), -})); jest.mock('next/dynamic', () => (func: () => Promise) => { let component: any = null; diff --git a/components/Feed.tsx b/components/Feed.tsx index 04539428b0..874db55811 100644 --- a/components/Feed.tsx +++ b/components/Feed.tsx @@ -32,6 +32,7 @@ import dynamic from 'next/dynamic'; import requestIdleCallback from 'next/dist/client/request-idle-callback'; import styles from '../styles/feed.module.css'; import classNames from 'classnames'; +import SettingsContext from '../contexts/SettingsContext'; const CommentPopup = dynamic(() => import('./cards/CommentPopup')); @@ -68,6 +69,10 @@ export default function Feed({ emptyScreen, }: FeedProps): ReactElement { const currentSettings = useContext(FeedContext); + const { user, showLogin } = useContext(AuthContext); + const { openNewTab, showOnlyUnreadPosts, spaciness } = useContext( + SettingsContext, + ); const { items, updatePost, @@ -79,11 +84,11 @@ export default function Feed({ currentSettings.pageSize, currentSettings.adSpot, currentSettings.numCards, + showOnlyUnreadPosts, query, variables, dep, ); - const { user, showLogin } = useContext(AuthContext); // const { nativeShareSupport } = useContext(ProgressiveEnhancementContext); const nativeShareSupport = false; const [disableFetching, setDisableFetching] = useState(false); @@ -319,6 +324,7 @@ export default function Feed({ } showShare={nativeShareSupport} onShare={onShare} + openNewTab={openNewTab} > {showCommentPopupId === item.post.id && ( ({ const hasNonPlaceholderCard = items.findIndex((item) => item.type !== 'placeholder') >= 0; + const gap = + spaciness === 'cozy' + ? 'gap-14' + : spaciness === 'roomy' + ? 'gap-12' + : 'gap-8'; return emptyScreen && emptyFeed ? ( <>{emptyScreen} ) : (
{items.map(itemToComponent)} diff --git a/components/FooterNavBar.tsx b/components/FooterNavBar.tsx index 87d8ae0849..994ab55912 100644 --- a/components/FooterNavBar.tsx +++ b/components/FooterNavBar.tsx @@ -8,12 +8,13 @@ import rem from '../macros/rem.macro'; import HomeIcon from '../icons/home.svg'; import BookmarkIcon from '../icons/bookmark.svg'; import FilterIcon from '../icons/filter.svg'; +import LayoutIcon from '../icons/layout.svg'; import AuthContext from '../contexts/AuthContext'; import { useRouter } from 'next/router'; import Button from './buttons/Button'; import { getTooltipProps } from '../lib/tooltip'; -export const navBarHeight = '3.063rem'; +export const navBarHeight = '3.0625rem'; const NavBar = styled(Flipper)` position: fixed; @@ -24,6 +25,7 @@ const NavBar = styled(Flipper)` height: ${navBarHeight}; grid-column-gap: ${sizeN(2)}; grid-auto-flow: column; + align-items: center; background: var(--theme-background-primary); border-top: ${rem(1)} solid var(--theme-divider-tertiary); z-index: 2; @@ -61,6 +63,11 @@ export const tabs: Tab[] = [ title: 'Filters', icon: , }, + { + path: '/settings', + title: 'Settings', + icon: , + }, ]; export default function FooterNavBar(): ReactElement { @@ -94,7 +101,10 @@ export default function FooterNavBar(): ReactElement { )} {selectedTab === index && ( - + )}
diff --git a/components/Settings.tsx b/components/Settings.tsx new file mode 100644 index 0000000000..6e5b784017 --- /dev/null +++ b/components/Settings.tsx @@ -0,0 +1,106 @@ +import React, { + HTMLAttributes, + ReactElement, + useContext, + useMemo, +} from 'react'; +import classed from '../lib/classed'; +import Radio from './fields/Radio'; +import Switch from './Switch'; +import SettingsContext from '../contexts/SettingsContext'; +import classNames from 'classnames'; + +const densities = ['eco', 'roomy', 'cozy']; + +export default function Settings({ + panelMode = false, + className, + ...props +}: { + panelMode?: boolean; +} & HTMLAttributes): ReactElement { + const { + spaciness, + setSpaciness, + lightMode, + toggleLightMode, + showOnlyUnreadPosts, + toggleShowOnlyUnreadPosts, + openNewTab, + toggleOpenNewTab, + } = useContext(SettingsContext); + + const Section = useMemo( + () => + classed( + 'section', + 'flex flex-col font-bold', + panelMode ? 'mx-5' : 'mt-6', + ), + [panelMode], + ); + + const SectionTitle = useMemo( + () => + classed( + 'h3', + 'text-theme-label-tertiary mb-5 font-bold', + panelMode ? 'typo-callout' : 'typo-body', + ), + [panelMode], + ); + + return ( +
+

Settings

+
+ Density + +
+
+ Preferences +
+ + Light theme + + + Hide read posts + + + Open links in new tab + +
+
+
+ ); +} diff --git a/components/Switch.tsx b/components/Switch.tsx index b947ea20fc..ea0f90b8cb 100644 --- a/components/Switch.tsx +++ b/components/Switch.tsx @@ -46,6 +46,7 @@ const Children = styled.span` `; const Container = styled.label` + position: relative; display: flex; align-items: center; cursor: pointer; @@ -63,7 +64,10 @@ const Container = styled.label` } input { - display: none; + position: absolute; + width: 0; + height: 0; + opacity: 0; &:checked { & ~ ${SwitchContainer} ${SwitchTrack} { @@ -80,6 +84,25 @@ const Container = styled.label` } } } + + &.big { + & ${SwitchTrack} { + width: ${sizeN(10)}; + height: ${sizeN(3)}; + border-radius: ${sizeN(1)}; + } + + & ${SwitchContainer} { + width: ${sizeN(10)}; + height: ${sizeN(5)}; + } + + & ${SwitchKnob} { + width: ${sizeN(5)}; + height: ${sizeN(5)}; + border-radius: ${sizeN(1.5)}; + } + } `; export interface Props { @@ -88,6 +111,7 @@ export interface Props { inputId: string; name: string; checked?: boolean; + onToggle?: () => unknown; } export default function Switch({ @@ -96,6 +120,7 @@ export default function Switch({ name, checked, children, + onToggle, }: Props): ReactElement { return ( @@ -103,7 +128,8 @@ export default function Switch({ id={inputId} name={name} type="checkbox" - defaultChecked={checked} + checked={checked} + onChange={onToggle} /> diff --git a/components/cards/PostCard.tsx b/components/cards/PostCard.tsx index df7a28142d..d75a55dddf 100644 --- a/components/cards/PostCard.tsx +++ b/components/cards/PostCard.tsx @@ -41,6 +41,7 @@ export type PostCardProps = { onBookmarkClick?: (post: Post, bookmarked: boolean) => unknown; showShare?: boolean; onShare?: Callback; + openNewTab?: boolean; } & HTMLAttributes; export function PostCard({ @@ -51,6 +52,7 @@ export function PostCard({ onBookmarkClick, showShare, onShare, + openNewTab, className, children, ...props @@ -75,8 +77,9 @@ export function PostCard({ > onLinkClick?.(post)} onMouseUp={(event) => event.button === 1 && onLinkClick?.(post)} diff --git a/components/fields/Radio.tsx b/components/fields/Radio.tsx new file mode 100644 index 0000000000..94ba823244 --- /dev/null +++ b/components/fields/Radio.tsx @@ -0,0 +1,33 @@ +import React, { ReactElement } from 'react'; +import RadioItem from './RadioItem'; + +export type RadioProps = { + name: string; + options: string[]; + value?: string; + onChange: (value: string) => unknown; +}; + +export default function Radio({ + name, + options, + value, + onChange, +}: RadioProps): ReactElement { + return ( +
+ {options.map((option, key) => ( + onChange(option)} + className="my-0.5 capitalize" + > + {option} + + ))} +
+ ); +} diff --git a/components/fields/RadioItem.tsx b/components/fields/RadioItem.tsx new file mode 100644 index 0000000000..84b24550a6 --- /dev/null +++ b/components/fields/RadioItem.tsx @@ -0,0 +1,47 @@ +import React, { InputHTMLAttributes, ReactElement } from 'react'; +import styles from '../../styles/radio.module.css'; +import classNames from 'classnames'; + +export type RadioItemProps = Omit< + InputHTMLAttributes, + 'type' +>; + +export default function RadioItem({ + children, + className, + checked, + ...props +}: RadioItemProps): ReactElement { + return ( + + ); +} diff --git a/components/layouts/MainLayout.tsx b/components/layouts/MainLayout.tsx index e54d80b04b..1c3627340f 100644 --- a/components/layouts/MainLayout.tsx +++ b/components/layouts/MainLayout.tsx @@ -3,12 +3,14 @@ import React, { ReactElement, ReactNode, useContext, + useState, } from 'react'; import Link from 'next/link'; import LazyImage from '../LazyImage'; import AuthContext from '../../contexts/AuthContext'; import Button from '../buttons/Button'; import BookmarkIcon from '../../icons/bookmark.svg'; +import LayoutIcon from '../../icons/layout.svg'; import Logo from '../svg/Logo'; import LogoTextBeta from '../svg/LogoTextBeta'; import dynamic from 'next/dynamic'; @@ -30,7 +32,11 @@ const HeaderRankProgress = dynamic( ), ); -const BookmarksButton = classed(Button, 'hidden mx-0.5 laptop:flex'); +const Settings = dynamic( + () => import(/* webpackChunkName: "settings" */ '../Settings'), +); + +const HeaderButton = classed(Button, 'hidden mx-0.5 laptop:flex'); export default function MainLayout({ children, @@ -40,13 +46,24 @@ export default function MainLayout({ }: MainLayoutProps): ReactElement { const { windowLoaded } = useContext(ProgressiveEnhancementContext); const { user, showLogin, loadingUser } = useContext(AuthContext); + const [showSettings, setShowSettings] = useState(false); + + const settingsButton = !responsive ? ( + } + {...getTooltipProps('Settings', { position: 'down' })} + className="btn-tertiary" + onClick={() => setShowSettings(!showSettings)} + pressed={showSettings} + /> + ) : undefined; return ( <>
- } {...getTooltipProps('Bookmarks', { position: 'down' })} className="btn-tertiary" /> + {settingsButton} ) : ( <> - } {...getTooltipProps('Bookmarks', { position: 'down' })} onClick={() => showLogin()} /> + {settingsButton} @@ -111,6 +130,12 @@ export default function MainLayout({ )}
+ {showSettings && ( + + )} {children} ); diff --git a/contexts/SettingsContext.ts b/contexts/SettingsContext.ts new file mode 100644 index 0000000000..d91339e727 --- /dev/null +++ b/contexts/SettingsContext.ts @@ -0,0 +1,15 @@ +import React from 'react'; + +export type SettingsContextData = { + spaciness: 'eco' | 'roomy' | 'cozy'; + lightMode: boolean; + showOnlyUnreadPosts: boolean; + openNewTab: boolean; + toggleLightMode: () => Promise; + toggleShowOnlyUnreadPosts: () => Promise; + toggleOpenNewTab: () => Promise; + setSpaciness: (density: string) => Promise; +}; + +const SettingsContext = React.createContext(null); +export default SettingsContext; diff --git a/graphql/settings.ts b/graphql/settings.ts new file mode 100644 index 0000000000..d15cabf06e --- /dev/null +++ b/graphql/settings.ts @@ -0,0 +1,29 @@ +import { gql } from 'graphql-request'; + +export type RemoteSettings = { + openNewTab: boolean; + showOnlyUnreadPosts: boolean; + theme: 'darcula' | 'bright'; + spaciness: string; +}; + +export type UserSettingsData = { userSettings: RemoteSettings }; + +export const USER_SETTINGS_QUERY = gql` + query UserSettings { + userSettings { + openNewTab + showOnlyUnreadPosts + theme + spaciness + } + } +`; + +export const UPDATE_USER_SETTINGS_MUTATION = gql` + mutation UpdateUserSettings($data: UpdateSettingsInput!) { + updateUserSettings(data: $data) { + updatedAt + } + } +`; diff --git a/hooks/useFeed.ts b/hooks/useFeed.ts index ae5a82eb9b..3753d3589b 100644 --- a/hooks/useFeed.ts +++ b/hooks/useFeed.ts @@ -36,6 +36,7 @@ export default function useFeed( pageSize: number, adSpot: number, placeholdersPerPage: number, + showOnlyUnreadPosts: boolean, query?: string, variables?: T, dep?: DependencyList, @@ -115,6 +116,7 @@ export default function useFeed( first: pageSize, after: lastPageParam?.page.pageInfo.endCursor, loggedIn: !!user, + unreadOnly: showOnlyUnreadPosts, }); if (!lastPageParam && !res.page.edges.length) { setEmptyFeed(true); @@ -142,7 +144,7 @@ export default function useFeed( if (query && tokenRefreshed) { refreshFeed(); } - }, [query, tokenRefreshed, variables, ...(dep ?? [])]); + }, [query, tokenRefreshed, showOnlyUnreadPosts, variables, ...(dep ?? [])]); // Add new posts to feed with a placeholder for the ad useEffect(() => { diff --git a/hooks/useSettings.ts b/hooks/useSettings.ts new file mode 100644 index 0000000000..a38ff2aeb5 --- /dev/null +++ b/hooks/useSettings.ts @@ -0,0 +1,120 @@ +import usePersistentState from './usePersistentState'; +import { useEffect, useState } from 'react'; +import { SettingsContextData } from '../contexts/SettingsContext'; +import { useMutation, useQuery } from 'react-query'; +import request from 'graphql-request'; +import { apiUrl } from '../lib/config'; +import { + RemoteSettings, + UPDATE_USER_SETTINGS_MUTATION, + USER_SETTINGS_QUERY, + UserSettingsData, +} from '../graphql/settings'; + +type Settings = { + spaciness: string; + showOnlyUnreadPosts: boolean; + openNewTab: boolean; +}; + +const themeCookieName = 'showmethelight'; +const defaultSettings: Settings = { + spaciness: 'eco', + showOnlyUnreadPosts: false, + openNewTab: true, +}; + +function toggleTheme(lightMode: boolean): void { + if (lightMode) { + document.documentElement.classList.add('light'); + } else { + document.documentElement.classList.remove('light'); + } + document.cookie = `${themeCookieName}=${lightMode};path=/;domain=${ + process.env.NEXT_PUBLIC_DOMAIN + };samesite=lax;expires=${60 * 60 * 24 * 365 * 10}`; +} + +export default function useSettings( + userId: string | null, + canFetchRemote: boolean, +): SettingsContextData { + const [settings, setCachedSettings] = usePersistentState( + 'settings', + defaultSettings, + ); + const [lightMode, setLightMode] = useState(false); + + const { data: remoteSettings } = useQuery( + ['userSettings', userId], + () => request(`${apiUrl}/graphql`, USER_SETTINGS_QUERY), + { + enabled: canFetchRemote && !!userId, + }, + ); + + const { mutateAsync: updateRemoteSettings } = useMutation< + unknown, + unknown, + RemoteSettings + >((settings) => + request(`${apiUrl}/graphql`, UPDATE_USER_SETTINGS_MUTATION, { + data: settings, + }), + ); + + useEffect(() => { + if (remoteSettings) { + const lightMode = remoteSettings.userSettings.theme === 'bright'; + setLightMode(lightMode); + toggleTheme(lightMode); + const cloneSettings = { ...remoteSettings.userSettings }; + delete cloneSettings.theme; + setCachedSettings(cloneSettings); + } + }, [remoteSettings]); + + useEffect(() => { + const lightModeCookieValue = document.cookie + .split('; ') + .find((row) => row.startsWith(themeCookieName)) + ?.split('=')[1]; + if (lightModeCookieValue === 'true') { + setLightMode(true); + document.documentElement.classList.add('light'); + } + }, []); + + const setSettings = async (settings: Settings): Promise => { + await setCachedSettings(settings); + if (userId) { + await updateRemoteSettings({ + ...settings, + theme: lightMode ? 'bright' : 'darcula', + }); + } + }; + + return { + ...settings, + lightMode, + toggleLightMode: async () => { + setLightMode(!lightMode); + toggleTheme(!lightMode); + if (userId) { + await updateRemoteSettings({ + ...settings, + theme: !lightMode ? 'bright' : 'darcula', + }); + } + }, + toggleShowOnlyUnreadPosts: () => + setSettings({ + ...settings, + showOnlyUnreadPosts: !settings.showOnlyUnreadPosts, + }), + toggleOpenNewTab: () => + setSettings({ ...settings, openNewTab: !settings.openNewTab }), + setSpaciness: (density) => setSettings({ ...settings, spaciness: density }), + }; +} diff --git a/icons/layout.svg b/icons/layout.svg new file mode 100644 index 0000000000..5906aca6b1 --- /dev/null +++ b/icons/layout.svg @@ -0,0 +1,11 @@ + + + + Layout + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/lib/theme.ts b/lib/theme.ts deleted file mode 100644 index 1a38ad04d6..0000000000 --- a/lib/theme.ts +++ /dev/null @@ -1,19 +0,0 @@ -const themeCookieName = 'showmethelight'; - -export function toggleTheme(): boolean { - const isLight = document.documentElement.classList.toggle('light'); - document.cookie = `${themeCookieName}=${isLight};path=/;domain=${ - process.env.NEXT_PUBLIC_DOMAIN - };samesite=lax;expires=${60 * 60 * 24 * 365 * 10}`; - return isLight; -} - -export function applyTheme(): void { - const isLight = document.cookie - .split('; ') - .find((row) => row.startsWith(themeCookieName)) - ?.split('=')[1]; - if (isLight === 'true') { - document.documentElement.classList.add('light'); - } -} diff --git a/pages/_app.tsx b/pages/_app.tsx index 96e73df781..c7cbeddf02 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -38,7 +38,8 @@ import SubscriptionContext from '../contexts/SubscriptionContext'; import useSubscriptionClient from '../hooks/useSubscriptionClient'; import useProgressiveEnhancement from '../hooks/useProgressiveEnhancement'; import { canonicalFromRouter } from '../lib/canonical'; -import { applyTheme } from '../lib/theme'; +import useSettings from '../hooks/useSettings'; +import SettingsContext from '../contexts/SettingsContext'; import '../styles/globals.css'; const queryClient = new QueryClient(); @@ -100,10 +101,10 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement { }), [user, loginMode, loadingUser, tokenRefreshed], ); + const canFetchUserData = progressiveContext.windowLoaded && tokenRefreshed; const onboardingContext = useOnboarding(user, loadedUserFromCache); - const subscriptionContext = useSubscriptionClient( - progressiveContext.windowLoaded && tokenRefreshed, - ); + const subscriptionContext = useSubscriptionClient(canFetchUserData); + const settingsContext = useSettings(user?.id, canFetchUserData); useEffect(() => { if (trackingId && !initializedGA) { @@ -132,10 +133,6 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement { updateCookieBanner(user); }, [user, loadingUser]); - useEffect(() => { - applyTheme(); - }, []); - const getLayout = (Component as CompnentGetLayout).getLayout || ((page) => page); const { layoutProps } = Component as CompnentGetLayout; @@ -144,74 +141,76 @@ function InternalApp({ Component, pageProps, router }: AppProps): ReactElement { - - - - - - - - - - - - - - - - - - - - - -