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, initialPrefs,
pendingMatchCount = 0, pendingMatchCount = 0,
role = "member", role = "member",
serverIsMobile = false,
}: { }: {
user: ShellUser; user: ShellUser;
company: ShellCompany; company: ShellCompany;
@@ -40,6 +41,7 @@ export function DashboardShell({
initialPrefs: ThemePrefs; initialPrefs: ThemePrefs;
pendingMatchCount?: number; pendingMatchCount?: number;
role?: ShellRole; role?: ShellRole;
serverIsMobile?: boolean;
}) { }) {
const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false); const [themeCustomizerOpen, setThemeCustomizerOpen] = React.useState(false);
const { config } = useSidebarConfig(); const { config } = useSidebarConfig();
@@ -47,6 +49,8 @@ export function DashboardShell({
return ( return (
<IconContext.Provider value={{ weight: "bold" }}> <IconContext.Provider value={{ weight: "bold" }}>
<SidebarProvider <SidebarProvider
defaultOpen={!serverIsMobile}
defaultIsMobile={serverIsMobile}
style={ style={
{ {
"--sidebar-width": "16rem", "--sidebar-width": "16rem",
+10 -1
View File
@@ -1,4 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { Query } from "node-appwrite"; import { Query } from "node-appwrite";
import { getActiveContext } from "@/lib/appwrite/active-context"; 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 { PLAN_LIMITS } from "@/lib/plans";
import { DashboardShell } from "./dashboard-shell"; 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({ export default async function DashboardLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const hdrs = await headers();
const serverIsMobile = detectMobileUA(hdrs.get("user-agent"));
const sessionUser = await getCurrentUser(); const sessionUser = await getCurrentUser();
if (!sessionUser) redirect("/sign-in"); if (!sessionUser) redirect("/sign-in");
@@ -61,7 +70,7 @@ export default async function DashboardLayout({
}; };
return ( 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} {children}
</DashboardShell> </DashboardShell>
); );
+3 -1
View File
@@ -54,6 +54,7 @@ function useSidebar() {
function SidebarProvider({ function SidebarProvider({
defaultOpen = true, defaultOpen = true,
defaultIsMobile = false,
open: openProp, open: openProp,
onOpenChange: setOpenProp, onOpenChange: setOpenProp,
className, className,
@@ -62,10 +63,11 @@ function SidebarProvider({
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
defaultOpen?: boolean defaultOpen?: boolean
defaultIsMobile?: boolean
open?: boolean open?: boolean
onOpenChange?: (open: boolean) => void onOpenChange?: (open: boolean) => void
}) { }) {
const isMobile = useIsMobile() const isMobile = useIsMobile(defaultIsMobile)
const [openMobile, setOpenMobile] = React.useState(false) const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar. // 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 const MOBILE_BREAKPOINT = 768
export function useIsMobile() { export function useIsMobile(defaultValue = false) {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) const [isMobile, setIsMobile] = React.useState<boolean>(defaultValue)
React.useEffect(() => { React.useEffect(() => {
const mql = typeof window !== "undefined" ? window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) : null const update = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
const onChange = () => { const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
setIsMobile(typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false) mql.addEventListener("change", update)
} update()
return () => mql.removeEventListener("change", update)
if (mql) {
mql.addEventListener("change", onChange)
}
setIsMobile(typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false)
return () => {
if (mql) {
mql.removeEventListener("change", onChange)
}
}
}, []) }, [])
return !!isMobile return isMobile
} }