Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
Refactor cookies handling
Browse files Browse the repository at this point in the history
  • Loading branch information
obulat committed Nov 12, 2022
1 parent f8e2cdc commit 9b599fa
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 109 deletions.
2 changes: 1 addition & 1 deletion src/composables/use-browser-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const useBrowserDetection = () => {
return app.$ua
}

export const useBrowserIsMobile = () => {
export const useBrowserIsMobile = (): boolean => {
const browser = useBrowserDetection()
if (browser === null) {
return false
Expand Down
57 changes: 15 additions & 42 deletions src/composables/use-cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,33 @@
*/
import { isProd } from '~/utils/node-env'

import type { OpenverseCookies } from '~/types/cookies'

import type { NuxtAppOptions } from '@nuxt/types'
import type { CookieSerializeOptions } from 'cookie'

export type CookieValue =
| string
| boolean
| Record<string, boolean | string | undefined>

const cookieOptions: CookieSerializeOptions = {
path: '/',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 60,
secure: isProd,
}

export const useCookies = <T extends CookieValue>(
app: NuxtAppOptions,
name: string
) => {
/**
* Sets the cookie with the `value`, using the useCookie's `name`.
*
* @param value - if not a string, this value is converted to string using
* `JSON.stringify`.
*/
const setCookie = (value: T) => {
const cookieValue =
typeof value === 'string' ? value : JSON.stringify(value)

app.$cookies.set(name, cookieValue, cookieOptions)
}

const updateCookie = (value: Omit<T, string>) => {
const currentValue = app.$cookies.get(name) ?? {}

app.$cookies.set(
name,
JSON.stringify({ ...currentValue, ...value }),
cookieOptions
)
export const useCookies = (app: NuxtAppOptions) => {
const get = <Key extends keyof OpenverseCookies>(
key: Key
): OpenverseCookies[Key] => {
return app.$cookies.get(key)
}

const getCookie = (cookieName: string): string | boolean | undefined => {
const cookieValueObject = app.$cookies.get(name)
if (cookieValueObject && cookieName in cookieValueObject) {
return cookieValueObject[cookieName]
}
return undefined
const set = <Key extends keyof OpenverseCookies>(
key: Key,
value: OpenverseCookies[Key]
) => {
app.$cookies.set(key, value, cookieOptions)
}

return {
setCookie,
getCookie,
updateCookie,
}
return { get, set }
}
export default useCookies

export type UseCookies = ReturnType<typeof useCookies>
47 changes: 47 additions & 0 deletions src/composables/use-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { computed, watch } from '@nuxtjs/composition-api'

import { useUiStore } from '~/stores/ui'
import { useFeatureFlagStore } from '~/stores/feature-flag'
import { useCookies } from '~/composables/use-cookies'
import { isMinScreen } from '~/composables/use-media-query'

import type { NuxtAppOptions } from '@nuxt/types'

/**
* This composable updates the UI store when the screen width changes or
* when the SSR layout settings are different from the cookie settings.
*
* The threshold for switching between mobile and desktop layout is
* `lg` for the `new_header` and `md` for the `old_header`.
*/
export function useLayout({ app }: { app: NuxtAppOptions }) {
const uiStore = useUiStore()
const featureFlagStore = useFeatureFlagStore()

const cookies = useCookies(app)

const isNewHeaderEnabled = computed(() => featureFlagStore.isOn('new_header'))

// `isMobile` is set in the middleware for each server request.
const shouldPassInSSR = uiStore.isDesktopLayout
const desktopBreakpoint = computed(() =>
isNewHeaderEnabled.value ? 'lg' : 'md'
)

const isDesktopLayout = isMinScreen(desktopBreakpoint, { shouldPassInSSR })

watch(isDesktopLayout, (isDesktop) => {
updateLayout(isDesktop)
})

const updateLayout = (isDesktop: boolean) => {
if (isDesktop !== uiStore.isDesktopLayout) {
uiStore.updateBreakpoint(isDesktop, cookies.set)
}
}

return {
isDesktopLayout,
updateLayout,
}
}
29 changes: 20 additions & 9 deletions src/composables/use-media-query.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/* this implementation is from https://github.com/vueuse/vueuse/packages/core/useMediaQuery/
which, in turn, is ported from https://github.com/logaretm/vue-use-web by Abdelrahman Awad */
import { ref, watchEffect } from '@nuxtjs/composition-api'
import { computed, ref, watchEffect } from '@nuxtjs/composition-api'

import { resolveRef } from '@vueuse/core'

import { SCREEN_SIZES, Breakpoint } from '~/constants/screens'
import { defaultWindow } from '~/constants/window'
import { tryOnScopeDispose } from '~/utils/try-on-scope-dispose'
import { useSupported } from '~/composables/use-supported'

import type { MaybeComputedRef } from '@vueuse/core'

interface Options {
shouldPassInSSR?: boolean
window?: Window
Expand All @@ -16,7 +20,7 @@ interface Options {
* Reactive Media Query.
*/
export function useMediaQuery(
query: string,
query: MaybeComputedRef<string>,
options: Options = { shouldPassInSSR: false }
) {
const { window = defaultWindow } = options
Expand Down Expand Up @@ -49,7 +53,7 @@ export function useMediaQuery(

cleanup()

mediaQuery = window.matchMedia(query)
mediaQuery = window.matchMedia(resolveRef(query).value)
matches.value = mediaQuery.matches

if ('addEventListener' in mediaQuery) {
Expand All @@ -66,21 +70,28 @@ export function useMediaQuery(

return matches
}

type BreakpointWithoutXs = Exclude<Breakpoint, 'xs'>
/**
* Check whether the current screen meets
* or exceeds the provided breakpoint size.
*/
export const isMinScreen = (breakpointName: Breakpoint, options?: Options) => {
export const isMinScreen = (
breakpointName: MaybeComputedRef<Breakpoint>,
options?: Options
) => {
if (breakpointName === 'xs') {
// `xs` is the "minimum" so it is always true
return ref(true)
}

return useMediaQuery(
`(min-width: ${SCREEN_SIZES.get(breakpointName)}px)`,
options
)
const query = computed(() => {
const sizeInPx = SCREEN_SIZES.get(
resolveRef(breakpointName as BreakpointWithoutXs).value
)
return `(min-width: ${sizeInPx}px)`
})

return useMediaQuery(query, options)
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/middleware/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const middleware: Middleware = async ({ app, query, route, $pinia }) => {

const uiStore = useUiStore($pinia)
const isMobileUa = app.$ua ? app.$ua.isMobile : false
uiStore.initFromCookies(app.$cookies.get('ui') ?? {}, isMobileUa)
app.$cookies.set('uiIsMobileUa', isMobileUa)
uiStore.initFromCookies(app.$cookies.getAll() ?? {})
}
export default middleware
4 changes: 2 additions & 2 deletions src/plugins/ua-parse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import useragent from 'express-useragent'
import useragent, { Details as UADetails } from 'express-useragent'

import type { Plugin } from '@nuxt/types'

Expand All @@ -10,7 +10,7 @@ const uaParsePlugin: Plugin = (context, inject) => {
} else if (typeof navigator !== 'undefined') {
userAgent = navigator.userAgent
}
let ua
let ua: UADetails | null
if (typeof userAgent == 'string') {
ua = useragent.parse(userAgent)
} else {
Expand Down
61 changes: 19 additions & 42 deletions src/stores/ui.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { defineStore } from 'pinia'

export type SnackbarState = 'not_shown' | 'visible' | 'dismissed'
import type { UseCookies } from '~/composables/use-cookies'
import type { OpenverseCookies, SnackbarState } from '~/types/cookies'

export interface UiStateCookie {
isDesktopLayout?: boolean
isMobileUa?: boolean
isFilterDismissed?: boolean
}
export type CookieSetter = UseCookies['set']

export interface UiState {
/**
Expand All @@ -33,8 +30,6 @@ export interface UiState {
isMobileUa: boolean
}

type UiCookieSetter = (value: UiStateCookie) => void

export const useUiStore = defineStore('ui', {
state: (): UiState => ({
instructionsSnackbarState: 'not_shown',
Expand All @@ -48,13 +43,6 @@ export const useUiStore = defineStore('ui', {
areInstructionsVisible(state): boolean {
return state.instructionsSnackbarState === 'visible'
},
uiCookie(state: UiState): UiStateCookie {
return {
isDesktopLayout: state.isDesktopLayout,
isMobileUa: state.isMobileUa,
isFilterDismissed: state.isFilterDismissed,
}
},
/**
* On desktop, we only hide the filters sidebar if it was
* specifically dismissed on the desktop layout.
Expand Down Expand Up @@ -83,53 +71,41 @@ export const useUiStore = defineStore('ui', {
this.instructionsSnackbarState = 'visible'
}
},

hideInstructionsSnackbar() {
this.instructionsSnackbarState = 'dismissed'
},
/**
* Given a list of key value pairs of UI state parameters and their states,
* populate the store state to match the cookie.
*
* Cookie is updated if one of the cookie values (see UiStateCookie) changes.
*
* @param cookies - mapping of UI state parameters and their states.
* @param isMobile - whether the request has a mobile user agent, set in middleware.
*/
initFromCookies(cookies: UiStateCookie, isMobile: boolean | null) {
this.isDesktopLayout = cookies.isDesktopLayout ?? false
this.isFilterDismissed = cookies.isFilterDismissed ?? false
initFromCookies(cookies: OpenverseCookies) {
this.isDesktopLayout = cookies.uiIsDesktopLayout ?? false
this.isFilterDismissed = cookies.uiIsFilterDismissed ?? false

// True if the request has a mobile user agent, or
// if the cookie `isMobileUa` value is true.
this.isMobileUa = Boolean((isMobile ?? false) || cookies.isMobileUa)
// Middleware sets the cookie value before calling `initFromCookies`.
this.isMobileUa = cookies.uiIsMobileUa ?? false
this.innerFilterVisible = this.isDesktopLayout
? !this.isFilterDismissed
: false
},

/**
* If the breakpoint or UA are different from the state,
* updates the state, and saves it into app cookies.
* If the breakpoint is different from the state, updates the state, and saves it into app cookies.
*
* @param isDesktopLayout - whether the layout is desktop (`lg` with the `new_header`
* and `md` with the `old_header`).
* @param isMobileUa - whether the request's user agent is `mobile` or not.
* @param setCookieFn - sets the app cookie.
*/
updateBreakpoint(
isDesktopLayout: boolean,
isMobileUa: boolean,
setCookieFn: UiCookieSetter
) {
if (
this.isDesktopLayout !== isDesktopLayout ||
this.isMobileUa !== isMobileUa
) {
updateBreakpoint(isDesktopLayout: boolean, setCookieFn: CookieSetter) {
if (this.isDesktopLayout !== isDesktopLayout) {
this.isDesktopLayout = isDesktopLayout
this.isMobileUa = isMobileUa

setCookieFn(this.uiCookie)
setCookieFn('uiIsDesktopLayout', this.isDesktopLayout)
}
},

/**
* Sets the filter state based on the `visible` parameter.
* If the filter state is changed on desktop, updates the `isFilterDismissed`
Expand All @@ -138,17 +114,18 @@ export const useUiStore = defineStore('ui', {
* @param visible - whether the filters should be visible.
* @param setCookieFn - the function that sets the app cookies
*/
setFiltersState(visible: boolean, setCookieFn: UiCookieSetter) {
setFiltersState(visible: boolean, setCookieFn: CookieSetter) {
this.innerFilterVisible = visible
if (this.isDesktopLayout) {
this.isFilterDismissed = !visible
setCookieFn(this.uiCookie)
setCookieFn('uiIsFilterDismissed', this.isFilterDismissed)
}
},

/**
* Toggles filter state and saves the new state in a cookie.
*/
toggleFilters(setCookieFn: UiCookieSetter) {
toggleFilters(setCookieFn: CookieSetter) {
this.setFiltersState(!this.isFilterVisible, setCookieFn)
},
},
Expand Down
29 changes: 29 additions & 0 deletions src/types/cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { FeatureState } from '~/constants/feature-flag'

export type SnackbarState = 'not_shown' | 'visible' | 'dismissed'

/**
* The cookies that Openverse uses to store the UI state.
*/
export interface OpenverseCookies {
/**
* The state of the instructions snackbar for audio component.
*/
uiInstructionsSnackbarState?: SnackbarState
/**
* Whether the filters were dismissed on desktop layout.
*/
uiIsFilterDismissed?: boolean
/**
* Whether the site layout is desktop (or mobile).
*/
uiIsDesktopLayout?: boolean
/**
* Whether the request user agent is mobile or not.
*/
uiIsMobileUa?: boolean
/**
* The state of the feature flags.
*/
features?: Record<string, FeatureState>
}
Loading

0 comments on commit 9b599fa

Please sign in to comment.