This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { Space, useSpaceStore } from "@/stores/spaceStore";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import SpaceSettingsModal from "@/components/SpaceSettingsModal";
|
||||
import apiClient from "@/lib/apiClient";
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const ensureInitialized = useAuthStore((s) => s.ensureInitialized);
|
||||
const fetchSpaces = useSpaceStore((s) => s.fetchSpaces);
|
||||
const currentSpace = useSpaceStore((s) => s.currentSpace!);
|
||||
|
||||
const [authChecked, setAuthChecked] = useState(false);
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const navbarRef = useRef<HTMLElement>(null);
|
||||
const [navbarHeight, setNavbarHeight] = useState(56);
|
||||
|
||||
const [showCreateCategoryModal, setShowCreateCategoryModal] = useState(false);
|
||||
const [showSpaceSettingsModal, setShowSpaceSettingsModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const theme = localStorage.getItem("theme") === "dark" ? "dark" : "light";
|
||||
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||
|
||||
ensureInitialized().then(() => {
|
||||
if (!useAuthStore.getState().user) {
|
||||
router.replace("/login");
|
||||
} else {
|
||||
setAuthChecked(true);
|
||||
fetchSpaces();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = navbarRef.current;
|
||||
if (!el) return;
|
||||
setNavbarHeight(el.offsetHeight);
|
||||
const ro = new ResizeObserver(() => setNavbarHeight(el.offsetHeight));
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [authChecked]);
|
||||
|
||||
if (!authChecked) {
|
||||
return (
|
||||
<div className="d-flex align-items-center justify-content-center min-vh-100">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading…</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function handleCreateCategory(name: string) {
|
||||
apiClient.post(`/api/v1/spaces/${currentSpace?.id}/categories`, { name }).then(() => {
|
||||
useSpaceStore.getState().fetchCategories(currentSpace?.id || "");
|
||||
});
|
||||
}
|
||||
|
||||
function handleSpaceSaved(_updatedSpace: Space) {
|
||||
useSpaceStore.getState().fetchSpaces();
|
||||
setShowSpaceSettingsModal(false);
|
||||
}
|
||||
|
||||
function handleSpaceDeleted() {
|
||||
useSpaceStore.getState().fetchSpaces();
|
||||
setShowSpaceSettingsModal(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="app-container">
|
||||
<nav ref={navbarRef}>
|
||||
<Navbar onToggleSidebar={() => setShowSidebar((v) => !v)} showSidebarToggle />
|
||||
</nav>
|
||||
|
||||
<div className="app-main d-flex">
|
||||
<Sidebar
|
||||
open={showSidebar}
|
||||
onClose={() => setShowSidebar(false)}
|
||||
navbarHeight={navbarHeight}
|
||||
onOpenCreateCategory={() => setShowCreateCategoryModal(true)}
|
||||
onOpenSpaceSettings={() => setShowSpaceSettingsModal(true)}
|
||||
/>
|
||||
<main className="main-content flex-grow-1">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCreateCategoryModal && (
|
||||
<CreateCategoryModal
|
||||
onClose={() => setShowCreateCategoryModal(false)}
|
||||
onSave={(name) => {
|
||||
handleCreateCategory(name);
|
||||
setShowCreateCategoryModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSpaceSettingsModal && currentSpace && (
|
||||
<SpaceSettingsModal
|
||||
space={currentSpace}
|
||||
onClose={() => setShowSpaceSettingsModal(false)}
|
||||
onSaved={handleSpaceSaved}
|
||||
onDeleted={handleSpaceDeleted}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateCategoryModal({ onClose, onSave }: { onClose: () => void; onSave: (name: string) => void }) {
|
||||
const [categoryName, setCategoryName] = useState("");
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === "Enter") onSave(categoryName);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal fade show d-block"
|
||||
tabIndex={-1}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
onClick={(e) => e.target === e.currentTarget && onClose()}
|
||||
>
|
||||
<div className="modal-dialog modal-dialog-centered" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Create Category</h5>
|
||||
<button type="button" className="btn-close" aria-label="Close" onClick={onClose} />
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-3">
|
||||
<label htmlFor="categoryName" className="form-label">
|
||||
Category Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="categoryName"
|
||||
placeholder="Enter category name"
|
||||
value={categoryName}
|
||||
onChange={(e) => setCategoryName(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-outline-secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={() => onSave(categoryName)} disabled={!categoryName.trim()}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-backdrop fade show" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user