Skip to content

Commit

Permalink
refactor: replace button theme switcher with select
Browse files Browse the repository at this point in the history
  • Loading branch information
riipandi committed Sep 18, 2024
1 parent a0e2786 commit 41beb6a
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 30 deletions.
53 changes: 38 additions & 15 deletions app/components/theme.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
import { useState } from 'react'
import { Theme, useTheme } from '#/context/providers/theme-provider'
import { clx } from '#/utils/ui-helper'

const themes = [
{ id: Theme.LIGHT, name: 'Light' },
{ id: Theme.DARK, name: 'Dark' },
{ id: Theme.SYSTEM, name: 'System' },
] as const

type ThemeOption = (typeof themes)[number]

export default function ThemeSwitcher() {
const [, setTheme] = useTheme()
const [theme, setTheme] = useTheme()
const [selectedTheme, setSelectedTheme] = useState<ThemeOption>(
themes.find((t) => t.id === theme) || themes[0]
)

const handleThemeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const newTheme = themes.find((theme) => theme.id === (event.target.value as Theme))
if (newTheme) {
setSelectedTheme(newTheme)
setTheme(newTheme.id)
}
}

return (
<div className="flex flex-wrap justify-center gap-4 rounded-2xl p-1">
<button
type="button"
className="rounded-xl border-2 border-gray-900 bg-white px-4 py-1 text-gray-900 text-sm"
onClick={() => setTheme(Theme.LIGHT)}
>
Light
</button>
<button
type="button"
className="rounded-xl border-2 border-gray-50 bg-gray-900 px-4 py-1 text-gray-50 text-sm"
onClick={() => setTheme(Theme.DARK)}
<div className="relative">
<select
value={selectedTheme.id}
onChange={handleThemeChange}
className={clx(
'w-full appearance-none rounded-lg border px-3 py-1.5 pr-8 text-sm focus:outline-none',
'border-gray-300 bg-white text-gray-900 focus:border-primary-500',
'dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:focus:border-primary-400'
)}
>
Dark
</button>
{themes.map((theme) => (
<option key={theme.id} value={theme.id}>
{theme.name}
</option>
))}
</select>
</div>
)
}
8 changes: 5 additions & 3 deletions app/context/providers/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { Dispatch, ReactNode, SetStateAction } from 'react'
enum Theme {
DARK = 'dark',
LIGHT = 'light',
SYSTEM = 'system',
}

const themes: Theme[] = Object.values(Theme)
Expand Down Expand Up @@ -46,18 +47,19 @@ function ThemeProvider({
return typeof window === 'object' ? getPreferredTheme() : null
})

const persistTheme = useFetcher()
const mountRun = useRef(false)
const persistTheme = useFetcher()

// biome-ignore lint/correctness/useExhaustiveDependencies: need to render once
useEffect(() => {
if (!mountRun.current) {
mountRun.current = true
return
}
if (theme) {
persistTheme.submit({ theme }, { action: 'set-theme', method: 'post' })
persistTheme.submit({ theme }, { action: 'set-theme', method: 'POST' })
}
}, [theme, persistTheme])
}, [theme])

useEffect(() => {
const mediaQuery = window.matchMedia(prefersLightMQ)
Expand Down
9 changes: 8 additions & 1 deletion app/routes/set-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ export const action = async ({ request }: ActionFunctionArgs) => {

themeSession.setTheme(theme)

return json({ success: true }, { headers: { 'Set-Cookie': await themeSession.commit() } })
return json(
{ success: true },
{
headers: {
'Set-Cookie': await themeSession.commit(),
},
}
)
}

export const loader = () => redirect('/', { status: 404 })
14 changes: 9 additions & 5 deletions app/utils/theme.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,35 @@
* Credits to Matt Stobbs: https://github.com/mattstobbs/remix-dark-mode
*/

import { type SessionIdStorageStrategy, createCookieSessionStorage } from '@remix-run/node'
import { type CookieOptions, createCookieSessionStorage } from '@remix-run/node'
import { Theme, isTheme } from '#/context/providers/theme-provider'

/**
* Determines if the application is running in development mode.
*/
const isDevelopment = process.env.NODE_ENV === 'development'
const expirationTime = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days from now

/**
* Configuration options for the theme cookie.
*/
const cookiesOptions: Omit<SessionIdStorageStrategy['cookie'], 'name'> = {
const cookiesOptions: Omit<CookieOptions, 'name' | 'expires'> = {
path: '/',
sameSite: 'lax',
secure: !isDevelopment,
secrets: isDevelopment ? [] : [process.env.APP_SECRET_KEY],
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now
secrets: !isDevelopment ? [process.env.APP_SECRET_KEY] : undefined,
httpOnly: true,
}

/**
* Creates a session storage for managing theme preferences.
*/
const themeStorage = createCookieSessionStorage({
cookie: { name: 'remix_theme', ...cookiesOptions },
cookie: {
name: 'remix_start_theme',
expires: expirationTime,
...cookiesOptions,
},
})

/**
Expand Down
12 changes: 6 additions & 6 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
// "disableReferencedProjectLoad": true, // Reduce automatic project loading

/* Language and Environment */
"target": "ES2022", // Set JavaScript language version
"lib": ["DOM", "DOM.Iterable", "ES2022"], // Specify library files to include
"jsx": "react-jsx", // Specify JSX code generation
"target": "ES2022", // Set JavaScript language version
"lib": ["DOM", "DOM.Iterable", "ES2022"], // Specify library files to include
"jsx": "react-jsx", // Specify JSX code generation
// "experimentalDecorators": true, // Enable experimental decorator support
// "emitDecoratorMetadata": true, // Emit decorator metadata
// "jsxFactory": "", // Specify JSX factory function
Expand All @@ -24,7 +24,7 @@
// "reactNamespace": "", // Specify React namespace
// "noLib": true, // Disable including default lib files
// "useDefineForClassFields": true, // Use define semantics for class fields
"moduleDetection": "auto", // Control module detection method
"moduleDetection": "auto", // Control module detection method

/* Modules */
"module": "ESNext", // Specify module code generation
Expand Down Expand Up @@ -74,7 +74,7 @@
// "declarationDir": "./", // Output directory for generated declarations

/* Interop Constraints */
"verbatimModuleSyntax": true, // Do not transform imports/exports
"verbatimModuleSyntax": true, // Do not transform imports/exports, enforced 'type-only' imports
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
"esModuleInterop": true, // Emit additional JavaScript for CommonJS modules
// "preserveSymlinks": true, // Do not resolve symlinks to their real path
Expand All @@ -95,7 +95,7 @@
// "exactOptionalPropertyTypes": true, // Enable strict checking of optional property types
// "noImplicitReturns": true, // Report error when not all code paths return a value
// "noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch
// "noUncheckedIndexedAccess": true, // Include 'undefined' in index signature results
"noUncheckedIndexedAccess": true, // Include 'undefined' in index signature results, make indexing stricter
// "noImplicitOverride": true, // Ensure overriding members are marked with override
// "noPropertyAccessFromIndexSignature": true, // Require explicit indexing when accessing properties
// "allowUnusedLabels": true, // Do not report errors on unused labels
Expand Down

0 comments on commit 41beb6a

Please sign in to comment.