updates on the new changes and logo fix and some ui changes as well

This commit is contained in:
2026-07-15 12:35:05 +05:30
parent 77ecb83cef
commit 03f07c79ee
6 changed files with 124 additions and 29 deletions

View File

@@ -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

View File

@@ -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>
}
/>
</>
);
}

View File

@@ -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

View File

@@ -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' }}>

View File

@@ -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>

View File

@@ -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