feat: enhance Ihateserverside component with client-side hydration and sidebar functionality
This commit is contained in:
@@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_COMMIT_SHA=dd1a5b1
|
NEXT_PUBLIC_COMMIT_SHA=1c60db5
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Menu } from "@/app/components/menu";
|
import { Menu } from "@/app/components/menu";
|
||||||
import { Sidebar } from "@/app/components/sidebar";
|
import { Sidebar } from "@/app/components/sidebar";
|
||||||
import { useNavidrome } from "@/app/components/NavidromeContext";
|
import { useNavidrome } from "@/app/components/NavidromeContext";
|
||||||
@@ -15,14 +15,17 @@ const Ihateserverside: React.FC<IhateserversideProps> = ({ children }) => {
|
|||||||
const [isSidebarVisible, setIsSidebarVisible] = useState(true);
|
const [isSidebarVisible, setIsSidebarVisible] = useState(true);
|
||||||
const [isStatusBarVisible, setIsStatusBarVisible] = useState(true);
|
const [isStatusBarVisible, setIsStatusBarVisible] = useState(true);
|
||||||
const [isSidebarHidden, setIsSidebarHidden] = useState(false);
|
const [isSidebarHidden, setIsSidebarHidden] = useState(false);
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
if (typeof window !== 'undefined') {
|
const [isClient, setIsClient] = useState(false);
|
||||||
return localStorage.getItem('sidebar-collapsed') === 'true';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const { playlists } = useNavidrome();
|
const { playlists } = useNavidrome();
|
||||||
|
|
||||||
|
// Handle client-side hydration
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
const savedCollapsed = localStorage.getItem('sidebar-collapsed') === 'true';
|
||||||
|
setIsSidebarCollapsed(savedCollapsed);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleSidebarCollapse = () => {
|
const toggleSidebarCollapse = () => {
|
||||||
const newCollapsed = !isSidebarCollapsed;
|
const newCollapsed = !isSidebarCollapsed;
|
||||||
setIsSidebarCollapsed(newCollapsed);
|
setIsSidebarCollapsed(newCollapsed);
|
||||||
@@ -36,6 +39,49 @@ const Ihateserverside: React.FC<IhateserversideProps> = ({ children }) => {
|
|||||||
setIsSidebarHidden(true); // This will fully hide the sidebar after transition
|
setIsSidebarHidden(true); // This will fully hide the sidebar after transition
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isClient) {
|
||||||
|
// Return a basic layout during SSR to match initial client render
|
||||||
|
return (
|
||||||
|
<div className="hidden md:flex md:flex-col md:h-screen">
|
||||||
|
{/* Top Menu */}
|
||||||
|
<div
|
||||||
|
className="sticky z-10 bg-background border-b"
|
||||||
|
style={{
|
||||||
|
left: 'env(titlebar-area-x, 0)',
|
||||||
|
top: 'env(titlebar-area-y, 0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
toggleSidebar={() => setIsSidebarVisible(!isSidebarVisible)}
|
||||||
|
isSidebarVisible={isSidebarVisible}
|
||||||
|
toggleStatusBar={() => setIsStatusBarVisible(!isStatusBarVisible)}
|
||||||
|
isStatusBarVisible={isStatusBarVisible}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div className="flex-1 flex overflow-hidden">
|
||||||
|
<div className="w-64 flex-shrink-0 border-r transition-all duration-200">
|
||||||
|
<Sidebar
|
||||||
|
playlists={playlists}
|
||||||
|
className="h-full overflow-y-auto"
|
||||||
|
collapsed={false}
|
||||||
|
onToggle={toggleSidebarCollapse}
|
||||||
|
onTransitionEnd={handleTransitionEnd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Floating Audio Player */}
|
||||||
|
<AudioPlayer />
|
||||||
|
<Toaster />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="hidden md:flex md:flex-col md:h-screen">
|
<div className="hidden md:flex md:flex-col md:h-screen">
|
||||||
{/* Top Menu */}
|
{/* Top Menu */}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { isConnected } = useNavidrome();
|
const { isConnected } = useNavidrome();
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
const [navidromeUrl, setNavidromeUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
// For this demo, we'll show connection status instead of user auth
|
// For this demo, we'll show connection status instead of user auth
|
||||||
const connectionStatus = isConnected ? "Connected to Navidrome" : "Not connected";
|
const connectionStatus = isConnected ? "Connected to Navidrome" : "Not connected";
|
||||||
@@ -57,6 +59,29 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
|
|||||||
setIsFullScreen(!isFullScreen)
|
setIsFullScreen(!isFullScreen)
|
||||||
}, [isFullScreen])
|
}, [isFullScreen])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
|
||||||
|
// Get Navidrome URL from localStorage
|
||||||
|
const config = localStorage.getItem("navidrome-config");
|
||||||
|
if (config) {
|
||||||
|
try {
|
||||||
|
const { serverUrl } = JSON.parse(config);
|
||||||
|
if (serverUrl) {
|
||||||
|
// Remove protocol (http:// or https://) and trailing slash
|
||||||
|
const prettyUrl = serverUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
||||||
|
setNavidromeUrl(prettyUrl);
|
||||||
|
} else {
|
||||||
|
setNavidromeUrl(null);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setNavidromeUrl(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setNavidromeUrl(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if ((event.metaKey || event.ctrlKey) && event.key === ',') {
|
if ((event.metaKey || event.ctrlKey) && event.key === ',') {
|
||||||
@@ -73,12 +98,16 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
if (isClient) {
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
if (isClient) {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [router, toggleSidebar, handleFullScreen]);
|
}, [router, toggleSidebar, handleFullScreen, isClient]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -100,7 +129,7 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
|
|||||||
Preferences <MenubarShortcut>⌘,</MenubarShortcut>
|
Preferences <MenubarShortcut>⌘,</MenubarShortcut>
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
<MenubarSeparator />
|
<MenubarSeparator />
|
||||||
<MenubarItem onClick={() => window.close()}>
|
<MenubarItem onClick={() => isClient && window.close()}>
|
||||||
Quit Music <MenubarShortcut>⌘Q</MenubarShortcut>
|
Quit Music <MenubarShortcut>⌘Q</MenubarShortcut>
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
</MenubarContent>
|
</MenubarContent>
|
||||||
@@ -281,25 +310,13 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-muted-foreground">Navidrome URL</span>
|
<span className="text-xs text-muted-foreground">Navidrome URL</span>
|
||||||
<span className="text-xs truncate max-w-[160px] text-right">
|
<span className="text-xs truncate max-w-[160px] text-right">
|
||||||
{typeof window !== "undefined"
|
{!isClient ? (
|
||||||
? (() => {
|
<span className="italic text-gray-400">Loading...</span>
|
||||||
const config = localStorage.getItem("navidrome-config");
|
) : navidromeUrl ? (
|
||||||
if (config) {
|
navidromeUrl
|
||||||
try {
|
) : (
|
||||||
const { serverUrl } = JSON.parse(config);
|
<span className="italic text-gray-400">Not set</span>
|
||||||
if (serverUrl) {
|
)}
|
||||||
// Remove protocol (http:// or https://) and trailing slash
|
|
||||||
const prettyUrl = serverUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
||||||
return prettyUrl;
|
|
||||||
}
|
|
||||||
return <span className="italic text-gray-400">Not set</span>;
|
|
||||||
} catch {
|
|
||||||
return <span className="italic text-gray-400">Invalid config</span>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <span className="italic text-gray-400">Not set</span>;
|
|
||||||
})()
|
|
||||||
: <span className="italic text-gray-400">Not available</span>}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user