168 lines
6.4 KiB
TypeScript
168 lines
6.4 KiB
TypeScript
"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>
|
|
);
|
|
}
|