new frontend
Build and Push App Image / build-and-push (push) Successful in 3m38s

This commit is contained in:
domrichardson
2026-06-17 12:08:20 +01:00
parent b690b00016
commit ead8219f3b
40 changed files with 10149 additions and 37 deletions
+167
View File
@@ -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>
);
}