Skip to content

Commit 82c06a3

Browse files
refactor(dashboard): UI redesign, persistent left sidebar, and light mode compatibility fixes (#344)
* feat(dashboard): complete redesign of inner project layout to Vercel-style Left Sidebar * fix(dashboard): import missing Mail icon in Sidebar * feat(dashboard): add persistent global search bar and refactor query state sync * feat(dashboard): add Vercel-style collapsible/expandable sidebar navigation with persistent state * fix(dashboard): resolve database page light mode style bugs and empty state flash * fix(dashboard): allow wrap on database action buttons to prevent layout overflow * fix(dashboard): resolve light mode style bugs in schema collection builder * fix(dashboard): resolve layout issues in create project page and database sidebar
1 parent 1cab6de commit 82c06a3

11 files changed

Lines changed: 292 additions & 362 deletions

File tree

apps/web-dashboard/src/components/Database/DatabaseHeader.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ const DatabaseHeader = ({
1515
<header className="db-header glass-panel" style={{
1616
padding: '0.75rem 1.5rem',
1717
display: 'flex',
18+
flexWrap: 'wrap',
19+
gap: '1rem',
1820
justifyContent: 'space-between',
1921
alignItems: 'center',
2022
borderBottom: '1px solid var(--color-border)',
21-
height: 'var(--header-height)'
23+
minHeight: 'var(--header-height)',
24+
height: 'auto'
2225
}}>
2326
<div className="header-left" style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
2427
<button
@@ -37,8 +40,8 @@ const DatabaseHeader = ({
3740
<h1 style={{ fontSize: '1.1rem', fontWeight: 700, margin: 0 }}>{activeCollection?.name}</h1>
3841
</div>
3942
</div>
40-
41-
<div className="header-actions" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
43+
44+
<div className="header-actions" style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
4245
{activeCollection?.name !== 'users' && (
4346
<div style={{ marginRight: '10px' }}>
4447
<AiQueryBar

apps/web-dashboard/src/components/DatabaseSidebar.jsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ export default function DatabaseSidebar({
108108

109109
<style>{`
110110
/* Sidebar Styles - Scoped */
111-
.db-sidebar {
111+
.db-sidebar {
112112
width: 280px;
113-
background: transparent;
114-
border-right: none;
113+
background: var(--color-bg-sidebar);
114+
border-right: 1px solid var(--color-border);
115115
display: flex;
116116
flex-direction: column;
117117
z-index: 100;
@@ -137,10 +137,11 @@ export default function DatabaseSidebar({
137137
}
138138
139139
.badge {
140-
background: rgba(255,255,255,0.1);
140+
background: var(--color-surface-hover-strong);
141+
border: 1px solid var(--color-border);
141142
padding: 2px 6px;
142-
border-radius: 4px;
143-
color: white;
143+
border-radius: 12px;
144+
color: var(--color-text-main);
144145
font-size: 0.7rem;
145146
}
146147
@@ -164,7 +165,7 @@ export default function DatabaseSidebar({
164165
}
165166
166167
.collection-item:hover {
167-
background: rgba(255,255,255,0.03);
168+
background: var(--color-surface-hover);
168169
color: var(--color-text-main);
169170
}
170171

apps/web-dashboard/src/components/Layout/Header.jsx

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,74 @@
1+
import { useState, useEffect, useRef } from 'react';
2+
import { useParams, Link, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
13
import { useAuth } from '../../context/AuthContext';
2-
import { Menu } from 'lucide-react'; // Import Menu Icon
4+
import { Menu, ChevronRight, Search } from 'lucide-react';
5+
import api from '../../utils/api';
36

4-
function Header({ onToggleSidebar, showToggle = true, children }) { // Default showToggle to true
7+
function Header({ onToggleSidebar, showToggle = true, isSidebarCollapsed = false }) {
58
const { user } = useAuth();
9+
const { projectId } = useParams();
10+
const [projectName, setProjectName] = useState('');
11+
const [searchParams, setSearchParams] = useSearchParams();
12+
const navigate = useNavigate();
13+
const location = useLocation();
14+
15+
const [inputValue, setInputValue] = useState(searchParams.get('q') || '');
16+
const searchInputRef = useRef(null);
617
const initial = user?.email ? user.email[0].toUpperCase() : 'D';
718

19+
useEffect(() => {
20+
let isMounted = true;
21+
if (!projectId) {
22+
queueMicrotask(() => {
23+
if (isMounted) setProjectName('');
24+
});
25+
return;
26+
}
27+
api.get(`/api/projects/${projectId}`)
28+
.then(res => {
29+
if (isMounted) setProjectName(res.data.name);
30+
})
31+
.catch(err => console.error("Failed to fetch project name for header:", err));
32+
return () => { isMounted = false; };
33+
}, [projectId]);
34+
35+
// Sync input value with URL search param
36+
useEffect(() => {
37+
let isMounted = true;
38+
queueMicrotask(() => {
39+
if (isMounted) {
40+
setInputValue(searchParams.get('q') || '');
41+
}
42+
});
43+
return () => { isMounted = false; };
44+
}, [searchParams]);
45+
46+
// Keyboard shortcut to focus search input
47+
useEffect(() => {
48+
const handleKeyPress = (e) => {
49+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
50+
e.preventDefault();
51+
searchInputRef.current?.focus();
52+
}
53+
};
54+
window.addEventListener('keydown', handleKeyPress);
55+
return () => window.removeEventListener('keydown', handleKeyPress);
56+
}, []);
57+
58+
const handleSearchChange = (e) => {
59+
const val = e.target.value;
60+
setInputValue(val);
61+
if (location.pathname === '/dashboard') {
62+
setSearchParams(val ? { q: val } : {});
63+
}
64+
};
65+
66+
const handleSearchKeyDown = (e) => {
67+
if (e.key === 'Enter') {
68+
navigate(`/dashboard?q=${encodeURIComponent(inputValue)}`);
69+
}
70+
};
71+
872
return (
973
<header style={{
1074
height: 'var(--header-height)',
@@ -13,17 +77,16 @@ function Header({ onToggleSidebar, showToggle = true, children }) { // Default s
1377
display: 'flex',
1478
alignItems: 'center',
1579
justifyContent: 'space-between',
16-
padding: '0 1rem',
80+
padding: '0 1.5rem',
1781
position: 'fixed',
1882
top: 0,
1983
right: 0,
2084
left: 0,
2185
zIndex: 1000,
2286
width: '100%',
23-
// Only add paddingLeft if we expect a sidebar (showToggle is a proxy for sidebar existence here)
24-
// But wait, showToggle means "can we toggle it". Even if we can't toggle, if sidebar is gone, padding should be 0 (or standard).
25-
// In Project Mode, showToggle is false. So paddingLeft should be 0 (or standard 1rem from padding above).
26-
paddingLeft: showToggle ? 'calc(var(--sidebar-width) + 1rem)' : '1rem'
87+
paddingLeft: showToggle
88+
? `calc(${isSidebarCollapsed ? 'var(--sidebar-width-collapsed)' : 'var(--sidebar-width)'} + 1.5rem)`
89+
: '1.5rem'
2790
}} className="responsive-header">
2891

2992
{/* CSS override for mobile padding in style tag below */}
@@ -35,35 +98,80 @@ function Header({ onToggleSidebar, showToggle = true, children }) { // Default s
3598
.mobile-toggle {
3699
display: block !important;
37100
}
101+
.search-container {
102+
display: none !important; /* Hide search bar on mobile headers to save space */
103+
}
38104
}
39105
.mobile-toggle {
40106
display: none;
41107
}
42108
`}</style>
43109

44110
{/* Mobile Menu Button - Only show if toggle is allowed */}
45-
{showToggle && (
46-
<button
47-
onClick={onToggleSidebar}
48-
className="btn btn-ghost mobile-toggle"
49-
style={{ padding: '8px', color: 'var(--color-text-main)' }}
50-
>
51-
<Menu size={24} />
52-
</button>
53-
)}
111+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
112+
{showToggle && (
113+
<button
114+
onClick={onToggleSidebar}
115+
className="btn btn-ghost mobile-toggle"
116+
style={{ padding: '8px', color: 'var(--color-text-main)' }}
117+
>
118+
<Menu size={20} />
119+
</button>
120+
)}
121+
122+
{/* Breadcrumbs for Project View */}
123+
{projectId && projectName && (
124+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8125rem' }}>
125+
<Link to="/dashboard" style={{ color: 'var(--color-text-muted)', textDecoration: 'none' }}>
126+
Personal
127+
</Link>
128+
<ChevronRight size={14} color="var(--color-text-muted)" />
129+
<span style={{ fontWeight: 500, color: 'var(--color-text-main)' }}>
130+
{projectName}
131+
</span>
132+
</div>
133+
)}
134+
</div>
54135

55-
{/* Search / Center Content Slot */}
56-
<div style={{ flex: 1, display: 'flex', justifyContent: 'center', padding: '0 2rem' }}>
57-
{children}
136+
{/* Persistent Global Search Input */}
137+
<div className="search-container" style={{ flex: 1, display: 'flex', justifyContent: 'center', padding: '0 2rem' }}>
138+
<div className="auth-input-wrap" style={{ width: '100%', maxWidth: '480px', position: 'relative' }}>
139+
<Search size={15} style={{ left: '12px', position: 'absolute', color: 'var(--color-text-muted)', zIndex: 1, top: '50%', transform: 'translateY(-50%)' }} />
140+
<input
141+
ref={searchInputRef}
142+
type="text"
143+
className="input-field"
144+
placeholder="Search projects..."
145+
style={{ paddingLeft: '2.4rem', paddingRight: '4rem', height: '32px', fontSize: '0.75rem', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '6px' }}
146+
value={inputValue}
147+
onChange={handleSearchChange}
148+
onKeyDown={handleSearchKeyDown}
149+
/>
150+
<div style={{
151+
position: 'absolute',
152+
right: '8px',
153+
top: '50%',
154+
transform: 'translateY(-50%)',
155+
padding: '1px 5px',
156+
background: 'var(--color-bg-main)',
157+
border: '1px solid var(--color-border)',
158+
borderRadius: '4px',
159+
fontSize: '0.6rem',
160+
color: 'var(--color-text-muted)',
161+
pointerEvents: 'none'
162+
}}>
163+
{navigator.platform.includes('Mac') ? '⌘ K' : 'Ctrl K'}
164+
</div>
165+
</div>
58166
</div>
59167

60168
{/* User Profile */}
61169
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
62-
<span style={{ fontSize: '0.85rem', color: 'var(--color-text-muted)' }}>
170+
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)' }}>
63171
{user?.email || 'Dev'}
64172
</span>
65173
<div style={{
66-
width: '28px', height: '28px', borderRadius: '4px',
174+
width: '28px', height: '28px', borderRadius: '4px',
67175
background: 'var(--color-bg-input)',
68176
border: '1px solid var(--color-border)',
69177
color: 'var(--color-text-main)', display: 'flex', alignItems: 'center', justifyContent: 'center',

apps/web-dashboard/src/components/Layout/MainLayout.jsx

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,70 @@ import { useState } from 'react';
22
import { useLocation, matchPath } from 'react-router-dom';
33
import Sidebar from './Sidebar';
44
import Header from './Header';
5-
import ProjectNavbar from './ProjectNavbar';
65
import { useLayout } from '../../context/LayoutContext';
76
import BackToTop from './BackToTop';
87
// Use the new official logo from public directory
98
const logoImage = "https://cdn.jsdelivr.net/gh/yash-pouranik/urBackend@main/frontend/public/logo.png";
109

1110
function MainLayout({ children }) {
1211
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
12+
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
13+
try {
14+
return localStorage.getItem('urbackend-sidebar-collapsed') === 'true';
15+
} catch {
16+
return false;
17+
}
18+
});
1319
const location = useLocation();
1420
const { headerContent } = useLayout();
1521

16-
// Check if we are inside a project route to toggle layout mode
17-
// Paths like /project/:projectId/...
1822
const isProjectRoute = matchPath("/project/:projectId/*", location.pathname);
1923

24+
const toggleSidebarCollapse = () => {
25+
setIsSidebarCollapsed(prev => {
26+
const nextVal = !prev;
27+
try {
28+
localStorage.setItem('urbackend-sidebar-collapsed', String(nextVal));
29+
} catch (err) {
30+
console.warn("localStorage not accessible", err);
31+
}
32+
return nextVal;
33+
});
34+
};
35+
2036
return (
21-
<div className="app-shell">
37+
<div className={`app-shell ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
2238
{/* Mobile Overlay - Only visible when sidebar is open on mobile */}
23-
{isSidebarOpen && !isProjectRoute && (
39+
{isSidebarOpen && (
2440
<div
2541
className="sidebar-overlay"
2642
onClick={() => setIsSidebarOpen(false)}
2743
></div>
2844
)}
2945

30-
{/* Sidebar - Only show if NOT in a project route (or if we want global sidebar always, but plan said hide it) */}
31-
{!isProjectRoute && (
32-
<Sidebar
33-
logo={logoImage}
34-
isOpen={isSidebarOpen}
35-
onClose={() => setIsSidebarOpen(false)}
36-
/>
37-
)}
46+
{/* Sidebar - Always visible */}
47+
<Sidebar
48+
logo={logoImage}
49+
isOpen={isSidebarOpen}
50+
onClose={() => setIsSidebarOpen(false)}
51+
isCollapsed={isSidebarCollapsed}
52+
onToggleCollapse={toggleSidebarCollapse}
53+
/>
3854

3955
{/* Main Content Area */}
40-
{/* If Project Route, remove margin-left (full width) */}
41-
{/* Add paddingTop to account for fixed global header only if not in project route */}
42-
<div className={`main-content ${isProjectRoute ? 'full-width' : ''}`} style={{ paddingTop: isProjectRoute ? '0' : 'var(--header-height)' }}>
56+
<div className="main-content" style={{ paddingTop: 'var(--header-height)' }}>
4357

44-
{/* Global Header */}
45-
{!isProjectRoute && (
46-
<Header
47-
logo={logoImage}
48-
onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
49-
// Hide toggle button if sidebar is hidden
50-
showToggle={true}
51-
>
52-
{headerContent}
53-
</Header>
54-
)}
55-
56-
{/* Project Navigation Bar - Only visible in project routes */}
57-
{isProjectRoute && <ProjectNavbar />}
58+
{/* Global Header - Always visible */}
59+
<Header
60+
logo={logoImage}
61+
onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
62+
showToggle={true}
63+
isSidebarCollapsed={isSidebarCollapsed}
64+
>
65+
{headerContent}
66+
</Header>
5867

5968
{/* Dynamic Page Content */}
60-
{/* Remove default margin-top as main-content has padding now. Remove padding for Database page. */}
6169
<div
6270
className="content-wrapper"
6371
style={{

0 commit comments

Comments
 (0)