diff --git a/app/Http/Middleware/HandleAppearance.php b/app/Http/Middleware/HandleAppearance.php new file mode 100644 index 00000000..c4ed399b --- /dev/null +++ b/app/Http/Middleware/HandleAppearance.php @@ -0,0 +1,23 @@ +cookie('appearance') ?? 'system'); + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index c987856b..204e9280 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Http\Request; use Inertia\Middleware; +use Tighten\Ziggy\Ziggy; class HandleInertiaRequests extends Middleware { @@ -45,6 +46,10 @@ public function share(Request $request): array 'auth' => [ 'user' => $request->user(), ], + 'ziggy' => [ + ...(new Ziggy)->toArray(), + 'location' => $request->url(), + ], ]; } } diff --git a/bootstrap/app.php b/bootstrap/app.php index a827a7f5..18d301a4 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withMiddleware(function (Middleware $middleware) { + $middleware->encryptCookies(except: ['appearance']); + $middleware->web(append: [ + HandleAppearance::class, HandleInertiaRequests::class, AddLinkHeadersForPreloadedAssets::class, ]); diff --git a/composer.json b/composer.json index 81e3105b..e028b341 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,11 @@ "dev": [ "Composer\\Config::disableProcessTimeout", "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite" + ], + "dev:ssr": [ + "npm run build:ssr", + "Composer\\Config::disableProcessTimeout", + "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr" ] }, "extra": { diff --git a/resources/js/components/TextLink.vue b/resources/js/components/TextLink.vue index 67fdf450..f1c4a337 100644 --- a/resources/js/components/TextLink.vue +++ b/resources/js/components/TextLink.vue @@ -17,7 +17,7 @@ defineProps(); :tabindex="tabindex" :method="method" :as="as" - class="hover:!decoration-current text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out dark:decoration-neutral-500" + class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:!decoration-current dark:decoration-neutral-500" > diff --git a/resources/js/composables/useAppearance.ts b/resources/js/composables/useAppearance.ts index 26a137ff..79adbeb7 100644 --- a/resources/js/composables/useAppearance.ts +++ b/resources/js/composables/useAppearance.ts @@ -3,28 +3,63 @@ import { onMounted, ref } from 'vue'; type Appearance = 'light' | 'dark' | 'system'; export function updateTheme(value: Appearance) { + if (typeof window === 'undefined') { + return; + } + if (value === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); + const systemTheme = mediaQueryList.matches ? 'dark' : 'light'; + document.documentElement.classList.toggle('dark', systemTheme === 'dark'); } else { document.documentElement.classList.toggle('dark', value === 'dark'); } } -const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); +const setCookie = (name: string, value: string, days = 365) => { + if (typeof document === 'undefined') { + return; + } + + const maxAge = days * 24 * 60 * 60; + + document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`; +}; + +const mediaQuery = () => { + if (typeof window === 'undefined') { + return null; + } + + return window.matchMedia('(prefers-color-scheme: dark)'); +}; + +const getStoredAppearance = () => { + if (typeof window === 'undefined') { + return null; + } + + return localStorage.getItem('appearance') as Appearance | null; +}; const handleSystemThemeChange = () => { - const currentAppearance = localStorage.getItem('appearance') as Appearance | null; + const currentAppearance = getStoredAppearance(); + updateTheme(currentAppearance || 'system'); }; export function initializeTheme() { + if (typeof window === 'undefined') { + return; + } + // Initialize theme from saved preference or default to system... - const savedAppearance = localStorage.getItem('appearance') as Appearance | null; + const savedAppearance = getStoredAppearance(); updateTheme(savedAppearance || 'system'); // Set up system theme change listener... - mediaQuery.addEventListener('change', handleSystemThemeChange); + mediaQuery()?.addEventListener('change', handleSystemThemeChange); } export function useAppearance() { @@ -42,7 +77,13 @@ export function useAppearance() { function updateAppearance(value: Appearance) { appearance.value = value; + + // Store in localStorage for client-side persistence... localStorage.setItem('appearance', value); + + // Store in cookie for SSR... + setCookie('appearance', value); + updateTheme(value); } diff --git a/resources/js/layouts/settings/Layout.vue b/resources/js/layouts/settings/Layout.vue index dcedde51..0b321e93 100644 --- a/resources/js/layouts/settings/Layout.vue +++ b/resources/js/layouts/settings/Layout.vue @@ -3,7 +3,7 @@ import Heading from '@/components/Heading.vue'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; import { type NavItem } from '@/types'; -import { Link } from '@inertiajs/vue3'; +import { Link, usePage } from '@inertiajs/vue3'; const sidebarNavItems: NavItem[] = [ { @@ -20,7 +20,9 @@ const sidebarNavItems: NavItem[] = [ }, ]; -const currentPath = window.location.pathname; +const page = usePage(); + +const currentPath = page.props.ziggy?.location ? new URL(page.props.ziggy.location).pathname : '';