}
/>
- >
);
}
diff --git a/src/layout/MainLayout/Sidebar.jsx b/src/layout/MainLayout/Sidebar.jsx
index a33bc85..f5305b2 100644
--- a/src/layout/MainLayout/Sidebar.jsx
+++ b/src/layout/MainLayout/Sidebar.jsx
@@ -6,10 +6,12 @@ import navItems from '@/menu/navItems';
// ==============================|| DOORMILE - SIDE NAV ||============================== //
// Thin wrapper around Astryx's SideNav template: sections + items driven by navItems.
-// Branding lives in the TopNav heading, so this stays icon+label only (Astryx guidance:
-// avoid a SideNavHeading when TopNav already carries app identity).
+// Branding lives in the TopNav logo only — a second logo here (above the nav items)
+// duplicated it, so this stays icon+label only. Collapse state is controlled from
+// MainLayout so the TopNav logo can track it (round mark collapsed, full wordmark
+// expanded).
-export default function Sidebar() {
+export default function Sidebar({ isCollapsed, onCollapsedChange }) {
const location = useLocation();
const isActive = (url) => !!url && location.pathname.startsWith(url);
@@ -17,7 +19,7 @@ export default function Sidebar() {
<>
div:last-child {
+ padding-block: 8px !important;
+ }
+
/* Collapse/expand toggle (the "<" / ">" chevron button). It sits
right on the sidebar's edge, so the default square ghost-button
hover clips against that border and looks broken. Give it a
diff --git a/src/layout/MainLayout/index.jsx b/src/layout/MainLayout/index.jsx
index 5504d9f..dd135d5 100644
--- a/src/layout/MainLayout/index.jsx
+++ b/src/layout/MainLayout/index.jsx
@@ -1,3 +1,4 @@
+import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { AppShell } from '@astryxdesign/core/AppShell';
@@ -9,13 +10,17 @@ import Sidebar from './Sidebar';
// and the auto-generated mobile drawer. Pages keep their own internal padding.
export default function MainLayout() {
+ // Lifted here (rather than left as SideNav's own uncontrolled state) so the
+ // navbar's logo can track the sidebar's collapse state — one logo, not two.
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true);
+
return (
}
- sideNav={}
+ topNav={}
+ sideNav={}
mobileNav={{ breakpoint: 'lg' }}
>
diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx
index 03939ee..d1f6ce9 100644
--- a/src/pages/Settings.jsx
+++ b/src/pages/Settings.jsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useRef } from 'react';
import {
Save,
Sliders,
@@ -40,6 +40,24 @@ const INITIAL_NOTIFY = {
};
const INITIAL_SECURITY = { currentPassword: '', newPassword: '', confirmPassword: '', twoFactor: false };
+// No backend endpoint exists yet for organisation settings (confirmed: apiClient.js has
+// none, and there's no swagger/docs to design one against — see git history for this
+// page). Persisting to localStorage keeps Save/Discard honest and durable across reloads
+// without pretending to sync to a server. Password fields are deliberately excluded —
+// never write plaintext credentials to localStorage.
+const SETTINGS_STORAGE_KEY = 'doormile_settings_v1';
+// data-URL logos bloat localStorage fast (5-10MB browser quota); cap the source file.
+const MAX_LOGO_BYTES = 1.5 * 1024 * 1024;
+
+function loadSavedSettings() {
+ try {
+ const raw = localStorage.getItem(SETTINGS_STORAGE_KEY);
+ return raw ? JSON.parse(raw) : null;
+ } catch {
+ return null;
+ }
+}
+
const NAV = [
{ icon: Sliders, label: 'General', desc: 'Organisation profile' },
{ icon: Bell, label: 'Notifications', desc: 'Alerts & channels' },
@@ -161,19 +179,58 @@ export default function Settings() {
const [toast, setToast] = useState(false);
const [dirty, setDirty] = useState(false);
- const [general, setGeneral] = useState(INITIAL_GENERAL);
- const [notify, setNotify] = useState(INITIAL_NOTIFY);
- const [security, setSecurity] = useState(INITIAL_SECURITY);
+ // savedRef tracks the last-persisted snapshot so Discard reverts to "what's actually
+ // saved" rather than jumping back to hardcoded factory defaults.
+ const savedRef = useRef(loadSavedSettings());
+ const saved = savedRef.current;
+
+ const [general, setGeneral] = useState(saved?.general || INITIAL_GENERAL);
+ const [notify, setNotify] = useState(saved?.notify || INITIAL_NOTIFY);
+ const [security, setSecurity] = useState({ ...INITIAL_SECURITY, twoFactor: saved?.twoFactor ?? false });
+ const [logoUrl, setLogoUrl] = useState(saved?.logoUrl ?? null);
+ const [logoError, setLogoError] = useState('');
+ const logoInputRef = useRef(null);
const setG = (k) => (val) => { setGeneral((p) => ({ ...p, [k]: val })); setDirty(true); };
const setN = (k) => (val) => { setNotify((p) => ({ ...p, [k]: val })); setDirty(true); };
const setSText = (k) => (val) => { setSecurity((p) => ({ ...p, [k]: val })); setDirty(true); };
- const save = () => { setToast(true); setDirty(false); setTimeout(() => setToast(false), 2500); };
+ const handleLogoChange = (e) => {
+ const file = e.target.files?.[0];
+ e.target.value = '';
+ if (!file) return;
+ if (file.size > MAX_LOGO_BYTES) {
+ setLogoError(`Logo is too large (${(file.size / 1024 / 1024).toFixed(1)}MB) — please use an image under ${MAX_LOGO_BYTES / 1024 / 1024}MB.`);
+ return;
+ }
+ setLogoError('');
+ const reader = new FileReader();
+ reader.onload = () => { setLogoUrl(reader.result); setDirty(true); };
+ reader.readAsDataURL(file);
+ };
+
+ const save = () => {
+ const snapshot = { general, notify, twoFactor: security.twoFactor, logoUrl };
+ try {
+ localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(snapshot));
+ } catch {
+ // Most likely QuotaExceededError from a large logo data-URL — surface it instead
+ // of showing a false "saved successfully" toast.
+ setLogoError('Could not save — local storage is full. Try a smaller logo image.');
+ return;
+ }
+ savedRef.current = snapshot;
+ setToast(true);
+ setDirty(false);
+ setTimeout(() => setToast(false), 2500);
+ };
const discard = () => {
- setGeneral(INITIAL_GENERAL);
- setNotify(INITIAL_NOTIFY);
- setSecurity(INITIAL_SECURITY);
+ const last = savedRef.current;
+ setGeneral(last?.general || INITIAL_GENERAL);
+ setNotify(last?.notify || INITIAL_NOTIFY);
+ setSecurity({ ...INITIAL_SECURITY, twoFactor: last?.twoFactor ?? false });
+ setLogoUrl(last?.logoUrl ?? null);
+ setLogoError('');
setDirty(false);
};
@@ -247,8 +304,14 @@ export default function Settings() {
flexWrap: 'wrap'
}}
>
-
-
+
+ {logoUrl ? (
+
+ ) : (
+
+ )}
@@ -257,7 +320,17 @@ export default function Settings() {