fix: server-side UA mobile detection — prevents desktop sidebar flash on mobile before JS hydration

This commit is contained in:
egecankomur
2026-05-14 20:03:41 +03:00
parent 0d6e773197
commit 856e577f4b
4 changed files with 25 additions and 20 deletions
+4
View File
@@ -33,6 +33,7 @@ export function DashboardShell({
initialPrefs,
pendingMatchCount = 0,
role = "member",
serverIsMobile = false,
}: {
user: ShellUser;
company: ShellCompany;
@@ -40,6 +41,7 @@ export function DashboardShell({
initialPrefs: ThemePrefs;
pendingMatchCount?: number;
role?: ShellRole;
serverIsMobile?: boolean;
}) {
const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false);
const { config } = useSidebarConfig();
@@ -47,6 +49,8 @@ export function DashboardShell({
return (
<IconContext.Provider value={{ weight: "bold" }}>
<SidebarProvider
defaultOpen={!serverIsMobile}
defaultIsMobile={serverIsMobile}
style={
{
"--sidebar-width": "16rem",
+10 -1
View File
@@ -1,4 +1,5 @@
import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { Query } from "node-appwrite";
import { getActiveContext } from "@/lib/appwrite/active-context";
@@ -9,11 +10,19 @@ import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions";
import { PLAN_LIMITS } from "@/lib/plans";
import { DashboardShell } from "./dashboard-shell";
function detectMobileUA(ua: string | null): boolean {
if (!ua) return false;
return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(ua);
}
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const hdrs = await headers();
const serverIsMobile = detectMobileUA(hdrs.get("user-agent"));
const sessionUser = await getCurrentUser();
if (!sessionUser) redirect("/sign-in");
@@ -61,7 +70,7 @@ export default async function DashboardLayout({
};
return (
<DashboardShell user={user} company={company} initialPrefs={themePrefs} pendingMatchCount={pendingMatchCount} role={ctx.role}>
<DashboardShell user={user} company={company} initialPrefs={themePrefs} pendingMatchCount={pendingMatchCount} role={ctx.role} serverIsMobile={serverIsMobile}>
{children}
</DashboardShell>
);
+3 -1
View File
@@ -54,6 +54,7 @@ function useSidebar() {
function SidebarProvider({
defaultOpen = true,
defaultIsMobile = false,
open: openProp,
onOpenChange: setOpenProp,
className,
@@ -62,10 +63,11 @@ function SidebarProvider({
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
defaultIsMobile?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const isMobile = useIsMobile(defaultIsMobile)
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
+8 -18
View File
@@ -2,26 +2,16 @@ import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
export function useIsMobile(defaultValue = false) {
const [isMobile, setIsMobile] = React.useState<boolean>(defaultValue)
React.useEffect(() => {
const mql = typeof window !== "undefined" ? window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) : null
const onChange = () => {
setIsMobile(typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false)
}
if (mql) {
mql.addEventListener("change", onChange)
}
setIsMobile(typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false)
return () => {
if (mql) {
mql.removeEventListener("change", onChange)
}
}
const update = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
mql.addEventListener("change", update)
update()
return () => mql.removeEventListener("change", update)
}, [])
return !!isMobile
return isMobile
}