updates on the new changes and logo fix and some ui changes as well
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
const LOGO_SRC = '/Doormile-logo.png';
|
const LOGO_SRC = '/Doormile-logo.png';
|
||||||
const NAVBAR_MARK_SRC = '/navbarLogo.png';
|
const NAVBAR_MARK_SRC = '/navbarLogo.png';
|
||||||
|
|
||||||
export default function Logo({ onDark = false, compact = false, height = 26, style = {} }) {
|
export default function Logo({ onDark = false, compact = false, height = 26, size = 64, style = {} }) {
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', ...style }}>
|
<div style={{ display: 'flex', alignItems: 'center', ...style }}>
|
||||||
@@ -14,8 +14,8 @@ export default function Logo({ onDark = false, compact = false, height = 26, sty
|
|||||||
src={NAVBAR_MARK_SRC}
|
src={NAVBAR_MARK_SRC}
|
||||||
alt="Doormile"
|
alt="Doormile"
|
||||||
style={{
|
style={{
|
||||||
width: 64,
|
width: size,
|
||||||
height: 64,
|
height: size,
|
||||||
display: 'block',
|
display: 'block',
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
flexShrink: 0
|
flexShrink: 0
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Logo from '@/components/Logo';
|
|||||||
import UserAvatar from '@/components/UserAvatar';
|
import UserAvatar from '@/components/UserAvatar';
|
||||||
import { fetchUsers } from '@/utils/apiClient';
|
import { fetchUsers } from '@/utils/apiClient';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header({ isSidebarCollapsed }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
let storedUserObj = { name: 'Admin', role: 'Operations Admin', id: 0 };
|
let storedUserObj = { name: 'Admin', role: 'Operations Admin', id: 0 };
|
||||||
@@ -18,9 +18,6 @@ export default function Header() {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
const [activeUserName, setActiveUserName] = useState(storedUserObj.name || 'Admin');
|
const [activeUserName, setActiveUserName] = useState(storedUserObj.name || 'Admin');
|
||||||
const displayRole = storedUserObj.role === 'admin' ? 'Administrator' :
|
|
||||||
storedUserObj.role === 'manager' ? 'Manager' :
|
|
||||||
storedUserObj.role === 'executive' ? 'Executive' : 'Operations Admin';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers().then((users) => {
|
fetchUsers().then((users) => {
|
||||||
@@ -36,7 +33,6 @@ export default function Header() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<TopNav
|
<TopNav
|
||||||
label="Main navigation"
|
label="Main navigation"
|
||||||
style={{
|
style={{
|
||||||
@@ -46,9 +42,17 @@ export default function Header() {
|
|||||||
}}
|
}}
|
||||||
heading={
|
heading={
|
||||||
<TopNavHeading
|
<TopNavHeading
|
||||||
logo={<Logo compact height={28} />}
|
logo={
|
||||||
heading="Doormile CRM"
|
<Logo
|
||||||
subheading={displayRole}
|
compact={isSidebarCollapsed}
|
||||||
|
size={32}
|
||||||
|
height={28}
|
||||||
|
// Scaling visually enlarges the mark without growing its layout
|
||||||
|
// box, so the navbar row keeps its original width/height instead
|
||||||
|
// of stretching taller every time the logo gets bigger.
|
||||||
|
style={isSidebarCollapsed ? { transform: 'scale(1.6)' } : undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
headingHref="/dashboard"
|
headingHref="/dashboard"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -74,6 +78,5 @@ export default function Header() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import navItems from '@/menu/navItems';
|
|||||||
|
|
||||||
// ==============================|| DOORMILE - SIDE NAV ||============================== //
|
// ==============================|| DOORMILE - SIDE NAV ||============================== //
|
||||||
// Thin wrapper around Astryx's SideNav template: sections + items driven by navItems.
|
// 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:
|
// Branding lives in the TopNav logo only — a second logo here (above the nav items)
|
||||||
// avoid a SideNavHeading when TopNav already carries app identity).
|
// 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 location = useLocation();
|
||||||
const isActive = (url) => !!url && location.pathname.startsWith(url);
|
const isActive = (url) => !!url && location.pathname.startsWith(url);
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ export default function Sidebar() {
|
|||||||
<>
|
<>
|
||||||
<SideNav
|
<SideNav
|
||||||
className="doormile-side-nav"
|
className="doormile-side-nav"
|
||||||
collapsible={{ defaultIsCollapsed: true, buttonLabel: 'Collapse navigation' }}
|
collapsible={{ isCollapsed, onCollapsedChange, buttonLabel: 'Collapse navigation' }}
|
||||||
style={{
|
style={{
|
||||||
// White + a right border/shadow matches the TopNav's chrome so the
|
// White + a right border/shadow matches the TopNav's chrome so the
|
||||||
// top and side nav read as one unified surface instead of two
|
// top and side nav read as one unified surface instead of two
|
||||||
@@ -138,6 +140,16 @@ export default function Sidebar() {
|
|||||||
color: #C01227 !important;
|
color: #C01227 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Astryx's sticky-bottom footer wrapper (the toggle button's direct
|
||||||
|
parent, always the nav's last direct child) drops its top padding
|
||||||
|
to 0 in the collapsed state while keeping 8px on the bottom, so
|
||||||
|
the arrow sits flush against the top of its box instead of
|
||||||
|
centered in the rail's height. Restore matching padding on both
|
||||||
|
sides so it centers correctly. */
|
||||||
|
.doormile-side-nav > div:last-child {
|
||||||
|
padding-block: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Collapse/expand toggle (the "<" / ">" chevron button). It sits
|
/* Collapse/expand toggle (the "<" / ">" chevron button). It sits
|
||||||
right on the sidebar's edge, so the default square ghost-button
|
right on the sidebar's edge, so the default square ghost-button
|
||||||
hover clips against that border and looks broken. Give it a
|
hover clips against that border and looks broken. Give it a
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { AppShell } from '@astryxdesign/core/AppShell';
|
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.
|
// and the auto-generated mobile drawer. Pages keep their own internal padding.
|
||||||
|
|
||||||
export default function MainLayout() {
|
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 (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
variant="section"
|
variant="section"
|
||||||
height="fill"
|
height="fill"
|
||||||
contentPadding={0}
|
contentPadding={0}
|
||||||
topNav={<Header />}
|
topNav={<Header isSidebarCollapsed={isSidebarCollapsed} />}
|
||||||
sideNav={<Sidebar />}
|
sideNav={<Sidebar isCollapsed={isSidebarCollapsed} onCollapsedChange={setIsSidebarCollapsed} />}
|
||||||
mobileNav={{ breakpoint: 'lg' }}
|
mobileNav={{ breakpoint: 'lg' }}
|
||||||
>
|
>
|
||||||
<div className="main-content-area" style={{ minHeight: '100%', boxSizing: 'border-box' }}>
|
<div className="main-content-area" style={{ minHeight: '100%', boxSizing: 'border-box' }}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Save,
|
Save,
|
||||||
Sliders,
|
Sliders,
|
||||||
@@ -40,6 +40,24 @@ const INITIAL_NOTIFY = {
|
|||||||
};
|
};
|
||||||
const INITIAL_SECURITY = { currentPassword: '', newPassword: '', confirmPassword: '', twoFactor: false };
|
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 = [
|
const NAV = [
|
||||||
{ icon: Sliders, label: 'General', desc: 'Organisation profile' },
|
{ icon: Sliders, label: 'General', desc: 'Organisation profile' },
|
||||||
{ icon: Bell, label: 'Notifications', desc: 'Alerts & channels' },
|
{ icon: Bell, label: 'Notifications', desc: 'Alerts & channels' },
|
||||||
@@ -161,19 +179,58 @@ export default function Settings() {
|
|||||||
const [toast, setToast] = useState(false);
|
const [toast, setToast] = useState(false);
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
|
|
||||||
const [general, setGeneral] = useState(INITIAL_GENERAL);
|
// savedRef tracks the last-persisted snapshot so Discard reverts to "what's actually
|
||||||
const [notify, setNotify] = useState(INITIAL_NOTIFY);
|
// saved" rather than jumping back to hardcoded factory defaults.
|
||||||
const [security, setSecurity] = useState(INITIAL_SECURITY);
|
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 setG = (k) => (val) => { setGeneral((p) => ({ ...p, [k]: val })); setDirty(true); };
|
||||||
const setN = (k) => (val) => { setNotify((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 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 = () => {
|
const discard = () => {
|
||||||
setGeneral(INITIAL_GENERAL);
|
const last = savedRef.current;
|
||||||
setNotify(INITIAL_NOTIFY);
|
setGeneral(last?.general || INITIAL_GENERAL);
|
||||||
setSecurity(INITIAL_SECURITY);
|
setNotify(last?.notify || INITIAL_NOTIFY);
|
||||||
|
setSecurity({ ...INITIAL_SECURITY, twoFactor: last?.twoFactor ?? false });
|
||||||
|
setLogoUrl(last?.logoUrl ?? null);
|
||||||
|
setLogoError('');
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -247,8 +304,14 @@ export default function Settings() {
|
|||||||
flexWrap: 'wrap'
|
flexWrap: 'wrap'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ width: '64px', height: '64px', borderRadius: '8px', backgroundColor: '#C01227', color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
<div
|
||||||
|
style={{ width: '64px', height: '64px', borderRadius: '8px', backgroundColor: logoUrl ? 'transparent' : '#C01227', color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{logoUrl ? (
|
||||||
|
<img src={logoUrl} alt="Organisation logo" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||||
|
) : (
|
||||||
<Building size={32} />
|
<Building size={32} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flexGrow: 1, minWidth: 0 }}>
|
<div style={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
@@ -257,7 +320,17 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<Text type="supporting" color="secondary">{general.supportEmail} · {general.contact}</Text>
|
<Text type="supporting" color="secondary">{general.supportEmail} · {general.contact}</Text>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="secondary" size="sm" label="Change logo" />
|
<input
|
||||||
|
ref={logoInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleLogoChange}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px' }}>
|
||||||
|
<Button variant="secondary" size="sm" label="Change logo" onClick={() => logoInputRef.current?.click()} />
|
||||||
|
{logoError && <Text type="supporting" style={{ color: '#ef4444', textAlign: 'right', maxWidth: '220px' }}>{logoError}</Text>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,8 @@ export default function TeamUsers() {
|
|||||||
hasClear
|
hasClear
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(v) => { setSearch(v); setPage(0); }}
|
onChange={(v) => { setSearch(v); setPage(0); }}
|
||||||
|
htmlName="team-users-search"
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user