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 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) {
|
||||
return (
|
||||
<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}
|
||||
alt="Doormile"
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
width: size,
|
||||
height: size,
|
||||
display: 'block',
|
||||
objectFit: 'cover',
|
||||
flexShrink: 0
|
||||
|
||||
@@ -8,7 +8,7 @@ import Logo from '@/components/Logo';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { fetchUsers } from '@/utils/apiClient';
|
||||
|
||||
export default function Header() {
|
||||
export default function Header({ isSidebarCollapsed }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
let storedUserObj = { name: 'Admin', role: 'Operations Admin', id: 0 };
|
||||
@@ -18,9 +18,6 @@ export default function Header() {
|
||||
} catch (e) {}
|
||||
|
||||
const [activeUserName, setActiveUserName] = useState(storedUserObj.name || 'Admin');
|
||||
const displayRole = storedUserObj.role === 'admin' ? 'Administrator' :
|
||||
storedUserObj.role === 'manager' ? 'Manager' :
|
||||
storedUserObj.role === 'executive' ? 'Executive' : 'Operations Admin';
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers().then((users) => {
|
||||
@@ -36,7 +33,6 @@ export default function Header() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopNav
|
||||
label="Main navigation"
|
||||
style={{
|
||||
@@ -46,9 +42,17 @@ export default function Header() {
|
||||
}}
|
||||
heading={
|
||||
<TopNavHeading
|
||||
logo={<Logo compact height={28} />}
|
||||
heading="Doormile CRM"
|
||||
subheading={displayRole}
|
||||
logo={
|
||||
<Logo
|
||||
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"
|
||||
/>
|
||||
}
|
||||
@@ -74,6 +78,5 @@ export default function Header() {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<>
|
||||
<SideNav
|
||||
className="doormile-side-nav"
|
||||
collapsible={{ defaultIsCollapsed: true, buttonLabel: 'Collapse navigation' }}
|
||||
collapsible={{ isCollapsed, onCollapsedChange, buttonLabel: 'Collapse navigation' }}
|
||||
style={{
|
||||
// White + a right border/shadow matches the TopNav's chrome so the
|
||||
// top and side nav read as one unified surface instead of two
|
||||
@@ -138,6 +140,16 @@ export default function Sidebar() {
|
||||
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
|
||||
right on the sidebar's edge, so the default square ghost-button
|
||||
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 { 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 (
|
||||
<AppShell
|
||||
variant="section"
|
||||
height="fill"
|
||||
contentPadding={0}
|
||||
topNav={<Header />}
|
||||
sideNav={<Sidebar />}
|
||||
topNav={<Header isSidebarCollapsed={isSidebarCollapsed} />}
|
||||
sideNav={<Sidebar isCollapsed={isSidebarCollapsed} onCollapsedChange={setIsSidebarCollapsed} />}
|
||||
mobileNav={{ breakpoint: 'lg' }}
|
||||
>
|
||||
<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 {
|
||||
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'
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '64px', height: '64px', borderRadius: '8px', backgroundColor: '#C01227', color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Building size={32} />
|
||||
<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} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
@@ -257,7 +320,17 @@ export default function Settings() {
|
||||
</div>
|
||||
<Text type="supporting" color="secondary">{general.supportEmail} · {general.contact}</Text>
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -252,6 +252,8 @@ export default function TeamUsers() {
|
||||
hasClear
|
||||
value={search}
|
||||
onChange={(v) => { setSearch(v); setPage(0); }}
|
||||
htmlName="team-users-search"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user