Procházet zdrojové kódy

refactor: standardize using cookie as persist state (#187)

* refactor: standardize cookie handling and js-cookie dependency

- Create cookie utility functions (getCookie, setCookie, removeCookie)
- Replace js-cookie usage in layout-context, authStore, and authenticated-layout
- Remove js-cookie and @types/js-cookie dependencies
- Update prettier import order configuration
- Maintain consistent cookie pattern across codebase

* refactor: update context providers to use cookie-based storage

- Replace localStorage with cookie handling for direction, font, and theme contexts.
- Introduce utility functions for cookie management (getCookie, setCookie, removeCookie).
- Ensure consistent cookie usage across all context providers for improved state persistence.

* refactor: simplify layout and search context providers
Sat Naing před 11 měsíci
rodič
revize
dd582a7987

+ 0 - 1
.prettierrc

@@ -23,7 +23,6 @@
     "^zod$",
     "^axios$",
     "^date-fns$",
-    "^js-cookie$",
     "^react-hook-form$",
     "^use-intl$",
     "^@radix-ui/(.*)$",

+ 0 - 2
package.json

@@ -43,7 +43,6 @@
     "cmdk": "1.1.1",
     "date-fns": "^4.1.0",
     "input-otp": "^1.4.2",
-    "js-cookie": "^3.0.5",
     "lucide-react": "^0.525.0",
     "react": "^19.1.0",
     "react-day-picker": "9.8.0",
@@ -66,7 +65,6 @@
     "@tanstack/react-router-devtools": "^1.129.0",
     "@tanstack/router-plugin": "^1.129.0",
     "@trivago/prettier-plugin-sort-imports": "^5.2.2",
-    "@types/js-cookie": "^3.0.6",
     "@types/node": "^24.0.15",
     "@types/react": "^19.1.8",
     "@types/react-dom": "^19.1.6",

+ 0 - 11
pnpm-lock.yaml

@@ -98,9 +98,6 @@ importers:
       input-otp:
         specifier: ^1.4.2
         version: 1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
-      js-cookie:
-        specifier: ^3.0.5
-        version: 3.0.5
       lucide-react:
         specifier: ^0.525.0
         version: 0.525.0(react@19.1.0)
@@ -162,9 +159,6 @@ importers:
       '@trivago/prettier-plugin-sort-imports':
         specifier: ^5.2.2
         version: 5.2.2(prettier@3.6.2)
-      '@types/js-cookie':
-        specifier: ^3.0.6
-        version: 3.0.6
       '@types/node':
         specifier: ^24.0.15
         version: 24.0.15
@@ -1666,9 +1660,6 @@ packages:
   '@types/estree@1.0.8':
     resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
 
-  '@types/js-cookie@3.0.6':
-    resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
-
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
@@ -4410,8 +4401,6 @@ snapshots:
 
   '@types/estree@1.0.8': {}
 
-  '@types/js-cookie@3.0.6': {}
-
   '@types/json-schema@7.0.15': {}
 
   '@types/node@24.0.15':

+ 2 - 2
src/components/layout/authenticated-layout.tsx

@@ -1,5 +1,5 @@
-import Cookies from 'js-cookie'
 import { Outlet } from '@tanstack/react-router'
+import { getCookie } from '@/lib/cookies'
 import { cn } from '@/lib/utils'
 import { LayoutProvider } from '@/context/layout-context'
 import { SearchProvider } from '@/context/search-context'
@@ -23,7 +23,7 @@ interface Props {
 }
 
 export function AuthenticatedLayout({ children }: Props) {
-  const defaultOpen = Cookies.get('sidebar_state') !== 'false'
+  const defaultOpen = getCookie('sidebar_state') !== 'false'
   return (
     <SearchProvider>
       <SidebarProvider defaultOpen={defaultOpen}>

+ 13 - 5
src/context/direction-context.tsx

@@ -1,9 +1,12 @@
 import { createContext, useContext, useEffect, useState } from 'react'
 import { DirectionProvider as RdxDirProvider } from '@radix-ui/react-direction'
+import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
 
 export type Direction = 'ltr' | 'rtl'
 
 const DEFAULT_DIRECTION = 'ltr'
+const DIRECTION_COOKIE_NAME = 'dir'
+const DIRECTION_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
 
 interface DirectionContextType {
   defaultDir: Direction
@@ -21,7 +24,7 @@ const DirectionContext = createContext<DirectionContextType>({
 
 export function DirectionProvider({ children }: { children: React.ReactNode }) {
   const [dir, _setDir] = useState<Direction>(
-    () => (localStorage.getItem('dir') as Direction) || DEFAULT_DIRECTION
+    () => (getCookie(DIRECTION_COOKIE_NAME) as Direction) || DEFAULT_DIRECTION
   )
 
   useEffect(() => {
@@ -31,17 +34,22 @@ export function DirectionProvider({ children }: { children: React.ReactNode }) {
 
   const setDir = (dir: Direction) => {
     _setDir(dir)
-    localStorage.setItem('dir', dir)
+    setCookie(DIRECTION_COOKIE_NAME, dir, DIRECTION_COOKIE_MAX_AGE)
   }
 
   const resetDir = () => {
     _setDir(DEFAULT_DIRECTION)
-    localStorage.removeItem('dir')
+    removeCookie(DIRECTION_COOKIE_NAME)
   }
 
   return (
     <DirectionContext
-      value={{ defaultDir: DEFAULT_DIRECTION, dir, setDir, resetDir }}
+      value={{
+        defaultDir: DEFAULT_DIRECTION,
+        dir,
+        setDir,
+        resetDir,
+      }}
     >
       <RdxDirProvider dir={dir}>{children}</RdxDirProvider>
     </DirectionContext>
@@ -52,7 +60,7 @@ export function DirectionProvider({ children }: { children: React.ReactNode }) {
 export function useDirection() {
   const context = useContext(DirectionContext)
   if (!context) {
-    throw new Error('useDirection must be used within a DirectionContext')
+    throw new Error('useDirection must be used within a DirectionProvider')
   }
   return context
 }

+ 15 - 3
src/context/font-context.tsx

@@ -1,11 +1,16 @@
 import React, { createContext, useContext, useEffect, useState } from 'react'
 import { fonts } from '@/config/fonts'
+import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
 
 type Font = (typeof fonts)[number]
 
+const FONT_COOKIE_NAME = 'font'
+const FONT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
+
 interface FontContextType {
   font: Font
   setFont: (font: Font) => void
+  resetFont: () => void
 }
 
 const FontContext = createContext<FontContextType | undefined>(undefined)
@@ -14,7 +19,7 @@ export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
   children,
 }) => {
   const [font, _setFont] = useState<Font>(() => {
-    const savedFont = localStorage.getItem('font')
+    const savedFont = getCookie(FONT_COOKIE_NAME)
     return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
   })
 
@@ -31,11 +36,18 @@ export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
   }, [font])
 
   const setFont = (font: Font) => {
-    localStorage.setItem('font', font)
+    setCookie(FONT_COOKIE_NAME, font, FONT_COOKIE_MAX_AGE)
     _setFont(font)
   }
 
-  return <FontContext value={{ font, setFont }}>{children}</FontContext>
+  const resetFont = () => {
+    removeCookie(FONT_COOKIE_NAME)
+    _setFont(fonts[0])
+  }
+
+  return (
+    <FontContext value={{ font, setFont, resetFont }}>{children}</FontContext>
+  )
 }
 
 // eslint-disable-next-line react-refresh/only-export-components

+ 11 - 13
src/context/layout-context.tsx

@@ -1,5 +1,5 @@
 import { createContext, useContext, useEffect, useState } from 'react'
-import Cookies from 'js-cookie'
+import { getCookie, setCookie } from '@/lib/cookies'
 
 export type Collapsible = 'offcanvas' | 'icon' | 'none'
 export type Variant = 'inset' | 'sidebar' | 'floating'
@@ -40,28 +40,26 @@ interface LayoutProviderProps {
   children: React.ReactNode
 }
 
-// Helper function to set cookie
-function setCookie(name: string, value: string): void {
-  document.cookie = `${name}=${value}; path=/; max-age=${LAYOUT_COOKIE_MAX_AGE}`
-}
-
 export function LayoutProvider({ children }: LayoutProviderProps) {
   const [collapsible, setCollapsible] = useState<Collapsible>(
     () =>
-      (Cookies.get(LAYOUT_COLLAPSIBLE_COOKIE_NAME) as Collapsible) ||
+      (getCookie(LAYOUT_COLLAPSIBLE_COOKIE_NAME) as Collapsible) ||
       DEFAULT_COLLAPSIBLE
   )
   const [variant, setVariant] = useState<Variant>(
-    () =>
-      (Cookies.get(LAYOUT_VARIANT_COOKIE_NAME) as Variant) || DEFAULT_VARIANT
+    () => (getCookie(LAYOUT_VARIANT_COOKIE_NAME) as Variant) || DEFAULT_VARIANT
   )
 
   useEffect(() => {
-    setCookie(LAYOUT_COLLAPSIBLE_COOKIE_NAME, collapsible)
+    setCookie(
+      LAYOUT_COLLAPSIBLE_COOKIE_NAME,
+      collapsible,
+      LAYOUT_COOKIE_MAX_AGE
+    )
   }, [collapsible])
 
   useEffect(() => {
-    setCookie(LAYOUT_VARIANT_COOKIE_NAME, variant)
+    setCookie(LAYOUT_VARIANT_COOKIE_NAME, variant, LAYOUT_COOKIE_MAX_AGE)
   }, [variant])
 
   const resetLayout = () => {
@@ -70,7 +68,7 @@ export function LayoutProvider({ children }: LayoutProviderProps) {
   }
 
   return (
-    <LayoutContext.Provider
+    <LayoutContext
       value={{
         defaultCollapsible: DEFAULT_COLLAPSIBLE,
         defaultVariant: DEFAULT_VARIANT,
@@ -82,7 +80,7 @@ export function LayoutProvider({ children }: LayoutProviderProps) {
       }}
     >
       {children}
-    </LayoutContext.Provider>
+    </LayoutContext>
   )
 }
 

+ 3 - 3
src/context/search-context.tsx

@@ -27,10 +27,10 @@ export function SearchProvider({ children }: Props) {
   }, [])
 
   return (
-    <SearchContext.Provider value={{ open, setOpen }}>
+    <SearchContext value={{ open, setOpen }}>
       {children}
       <CommandMenu />
-    </SearchContext.Provider>
+    </SearchContext>
   )
 }
 
@@ -39,7 +39,7 @@ export const useSearch = () => {
   const searchContext = React.useContext(SearchContext)
 
   if (!searchContext) {
-    throw new Error('useSearch has to be used within <SearchContext.Provider>')
+    throw new Error('useSearch has to be used within SearchProvider')
   }
 
   return searchContext

+ 12 - 9
src/context/theme-context.tsx

@@ -1,9 +1,12 @@
 import { createContext, useContext, useEffect, useState, useMemo } from 'react'
+import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
 
 type Theme = 'dark' | 'light' | 'system'
 type ResolvedTheme = Exclude<Theme, 'system'>
 
 const DEFAULT_THEME = 'system'
+const THEME_COOKIE_NAME = 'vite-ui-theme'
+const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
 
 type ThemeProviderProps = {
   children: React.ReactNode
@@ -27,16 +30,16 @@ const initialState: ThemeProviderState = {
   resetTheme: () => {},
 }
 
-const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
+const ThemeContext = createContext<ThemeProviderState>(initialState)
 
 export function ThemeProvider({
   children,
   defaultTheme = DEFAULT_THEME,
-  storageKey = 'vite-ui-theme',
+  storageKey = THEME_COOKIE_NAME,
   ...props
 }: ThemeProviderProps) {
   const [theme, _setTheme] = useState<Theme>(
-    () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
+    () => (getCookie(storageKey) as Theme) || defaultTheme
   )
 
   // Optimized: Memoize the resolved theme calculation to prevent unnecessary re-computations
@@ -73,16 +76,16 @@ export function ThemeProvider({
   }, [theme, resolvedTheme])
 
   const setTheme = (theme: Theme) => {
-    localStorage.setItem(storageKey, theme)
+    setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE)
     _setTheme(theme)
   }
 
   const resetTheme = () => {
-    localStorage.removeItem(storageKey)
+    removeCookie(storageKey)
     _setTheme(DEFAULT_THEME)
   }
 
-  const value = {
+  const contextValue = {
     defaultTheme,
     resolvedTheme,
     resetTheme,
@@ -91,15 +94,15 @@ export function ThemeProvider({
   }
 
   return (
-    <ThemeProviderContext.Provider {...props} value={value}>
+    <ThemeContext value={contextValue} {...props}>
       {children}
-    </ThemeProviderContext.Provider>
+    </ThemeContext>
   )
 }
 
 // eslint-disable-next-line react-refresh/only-export-components
 export const useTheme = () => {
-  const context = useContext(ThemeProviderContext)
+  const context = useContext(ThemeContext)
 
   if (context === undefined)
     throw new Error('useTheme must be used within a ThemeProvider')

+ 43 - 0
src/lib/cookies.ts

@@ -0,0 +1,43 @@
+/**
+ * Cookie utility functions using manual document.cookie approach
+ * Replaces js-cookie dependency for better consistency
+ */
+
+const DEFAULT_MAX_AGE = 60 * 60 * 24 * 7 // 7 days
+
+/**
+ * Get a cookie value by name
+ */
+export function getCookie(name: string): string | undefined {
+  if (typeof document === 'undefined') return undefined
+
+  const value = `; ${document.cookie}`
+  const parts = value.split(`; ${name}=`)
+  if (parts.length === 2) {
+    const cookieValue = parts.pop()?.split(';').shift()
+    return cookieValue
+  }
+  return undefined
+}
+
+/**
+ * Set a cookie with name, value, and optional max age
+ */
+export function setCookie(
+  name: string,
+  value: string,
+  maxAge: number = DEFAULT_MAX_AGE
+): void {
+  if (typeof document === 'undefined') return
+
+  document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`
+}
+
+/**
+ * Remove a cookie by setting its max age to 0
+ */
+export function removeCookie(name: string): void {
+  if (typeof document === 'undefined') return
+
+  document.cookie = `${name}=; path=/; max-age=0`
+}

+ 5 - 7
src/stores/authStore.ts

@@ -1,5 +1,5 @@
-import Cookies from 'js-cookie'
 import { create } from 'zustand'
+import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
 
 const ACCESS_TOKEN = 'thisisjustarandomstring'
 
@@ -22,7 +22,7 @@ interface AuthState {
 }
 
 export const useAuthStore = create<AuthState>()((set) => {
-  const cookieState = Cookies.get(ACCESS_TOKEN)
+  const cookieState = getCookie(ACCESS_TOKEN)
   const initToken = cookieState ? JSON.parse(cookieState) : ''
   return {
     auth: {
@@ -32,17 +32,17 @@ export const useAuthStore = create<AuthState>()((set) => {
       accessToken: initToken,
       setAccessToken: (accessToken) =>
         set((state) => {
-          Cookies.set(ACCESS_TOKEN, JSON.stringify(accessToken))
+          setCookie(ACCESS_TOKEN, JSON.stringify(accessToken))
           return { ...state, auth: { ...state.auth, accessToken } }
         }),
       resetAccessToken: () =>
         set((state) => {
-          Cookies.remove(ACCESS_TOKEN)
+          removeCookie(ACCESS_TOKEN)
           return { ...state, auth: { ...state.auth, accessToken: '' } }
         }),
       reset: () =>
         set((state) => {
-          Cookies.remove(ACCESS_TOKEN)
+          removeCookie(ACCESS_TOKEN)
           return {
             ...state,
             auth: { ...state.auth, user: null, accessToken: '' },
@@ -51,5 +51,3 @@ export const useAuthStore = create<AuthState>()((set) => {
     },
   }
 })
-
-// export const useAuth = () => useAuthStore((state) => state.auth)