Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ const DatabaseHeader = ({
<header className="db-header glass-panel" style={{
padding: '0.75rem 1.5rem',
display: 'flex',
flexWrap: 'wrap',
gap: '1rem',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: '1px solid var(--color-border)',
height: 'var(--header-height)'
minHeight: 'var(--header-height)',
height: 'auto'
}}>
<div className="header-left" style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<button
Expand All @@ -37,8 +40,8 @@ const DatabaseHeader = ({
<h1 style={{ fontSize: '1.1rem', fontWeight: 700, margin: 0 }}>{activeCollection?.name}</h1>
</div>
</div>

<div className="header-actions" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div className="header-actions" style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap' }}>
{activeCollection?.name !== 'users' && (
<div style={{ marginRight: '10px' }}>
<AiQueryBar
Expand Down
15 changes: 8 additions & 7 deletions apps/web-dashboard/src/components/DatabaseSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ export default function DatabaseSidebar({

<style>{`
/* Sidebar Styles - Scoped */
.db-sidebar {
.db-sidebar {
width: 280px;
background: transparent;
border-right: none;
background: var(--color-bg-sidebar);
border-right: 1px solid var(--color-border);
display: flex;
flex-direction: column;
z-index: 100;
Expand All @@ -137,10 +137,11 @@ export default function DatabaseSidebar({
}

.badge {
background: rgba(255,255,255,0.1);
background: var(--color-surface-hover-strong);
border: 1px solid var(--color-border);
padding: 2px 6px;
border-radius: 4px;
color: white;
border-radius: 12px;
color: var(--color-text-main);
font-size: 0.7rem;
}

Expand All @@ -164,7 +165,7 @@ export default function DatabaseSidebar({
}

.collection-item:hover {
background: rgba(255,255,255,0.03);
background: var(--color-surface-hover);
color: var(--color-text-main);
}

Expand Down
150 changes: 129 additions & 21 deletions apps/web-dashboard/src/components/Layout/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,74 @@
import { useState, useEffect, useRef } from 'react';
import { useParams, Link, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import { Menu } from 'lucide-react'; // Import Menu Icon
import { Menu, ChevronRight, Search } from 'lucide-react';
import api from '../../utils/api';

function Header({ onToggleSidebar, showToggle = true, children }) { // Default showToggle to true
function Header({ onToggleSidebar, showToggle = true, isSidebarCollapsed = false }) {
const { user } = useAuth();
const { projectId } = useParams();
const [projectName, setProjectName] = useState('');
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();

const [inputValue, setInputValue] = useState(searchParams.get('q') || '');
const searchInputRef = useRef(null);
const initial = user?.email ? user.email[0].toUpperCase() : 'D';

useEffect(() => {
let isMounted = true;
if (!projectId) {
queueMicrotask(() => {
if (isMounted) setProjectName('');
});
return;
}
api.get(`/api/projects/${projectId}`)
.then(res => {
if (isMounted) setProjectName(res.data.name);
})
.catch(err => console.error("Failed to fetch project name for header:", err));
return () => { isMounted = false; };
}, [projectId]);

// Sync input value with URL search param
useEffect(() => {
let isMounted = true;
queueMicrotask(() => {
if (isMounted) {
setInputValue(searchParams.get('q') || '');
}
});
return () => { isMounted = false; };
}, [searchParams]);

// Keyboard shortcut to focus search input
useEffect(() => {
const handleKeyPress = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
searchInputRef.current?.focus();
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);

const handleSearchChange = (e) => {
const val = e.target.value;
setInputValue(val);
if (location.pathname === '/dashboard') {
setSearchParams(val ? { q: val } : {});
}
};

const handleSearchKeyDown = (e) => {
if (e.key === 'Enter') {
navigate(`/dashboard?q=${encodeURIComponent(inputValue)}`);
}
};

return (
<header style={{
height: 'var(--header-height)',
Expand All @@ -13,17 +77,16 @@ function Header({ onToggleSidebar, showToggle = true, children }) { // Default s
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 1rem',
padding: '0 1.5rem',
position: 'fixed',
top: 0,
right: 0,
left: 0,
zIndex: 1000,
width: '100%',
// Only add paddingLeft if we expect a sidebar (showToggle is a proxy for sidebar existence here)
// But wait, showToggle means "can we toggle it". Even if we can't toggle, if sidebar is gone, padding should be 0 (or standard).
// In Project Mode, showToggle is false. So paddingLeft should be 0 (or standard 1rem from padding above).
paddingLeft: showToggle ? 'calc(var(--sidebar-width) + 1rem)' : '1rem'
paddingLeft: showToggle
? `calc(${isSidebarCollapsed ? 'var(--sidebar-width-collapsed)' : 'var(--sidebar-width)'} + 1.5rem)`
: '1.5rem'
}} className="responsive-header">

{/* CSS override for mobile padding in style tag below */}
Expand All @@ -35,35 +98,80 @@ function Header({ onToggleSidebar, showToggle = true, children }) { // Default s
.mobile-toggle {
display: block !important;
}
.search-container {
display: none !important; /* Hide search bar on mobile headers to save space */
}
}
.mobile-toggle {
display: none;
}
`}</style>

{/* Mobile Menu Button - Only show if toggle is allowed */}
{showToggle && (
<button
onClick={onToggleSidebar}
className="btn btn-ghost mobile-toggle"
style={{ padding: '8px', color: 'var(--color-text-main)' }}
>
<Menu size={24} />
</button>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
{showToggle && (
<button
onClick={onToggleSidebar}
className="btn btn-ghost mobile-toggle"
style={{ padding: '8px', color: 'var(--color-text-main)' }}
>
<Menu size={20} />
</button>
)}

{/* Breadcrumbs for Project View */}
{projectId && projectName && (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '0.8125rem' }}>
<Link to="/dashboard" style={{ color: 'var(--color-text-muted)', textDecoration: 'none' }}>
Personal
</Link>
<ChevronRight size={14} color="var(--color-text-muted)" />
<span style={{ fontWeight: 500, color: 'var(--color-text-main)' }}>
{projectName}
</span>
</div>
)}
</div>

{/* Search / Center Content Slot */}
<div style={{ flex: 1, display: 'flex', justifyContent: 'center', padding: '0 2rem' }}>
{children}
{/* Persistent Global Search Input */}
<div className="search-container" style={{ flex: 1, display: 'flex', justifyContent: 'center', padding: '0 2rem' }}>
<div className="auth-input-wrap" style={{ width: '100%', maxWidth: '480px', position: 'relative' }}>
<Search size={15} style={{ left: '12px', position: 'absolute', color: 'var(--color-text-muted)', zIndex: 1, top: '50%', transform: 'translateY(-50%)' }} />
<input
ref={searchInputRef}
type="text"
className="input-field"
placeholder="Search projects..."
style={{ paddingLeft: '2.4rem', paddingRight: '4rem', height: '32px', fontSize: '0.75rem', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '6px' }}
value={inputValue}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
/>
<div style={{
position: 'absolute',
right: '8px',
top: '50%',
transform: 'translateY(-50%)',
padding: '1px 5px',
background: 'var(--color-bg-main)',
border: '1px solid var(--color-border)',
borderRadius: '4px',
fontSize: '0.6rem',
color: 'var(--color-text-muted)',
pointerEvents: 'none'
}}>
{navigator.platform.includes('Mac') ? '⌘ K' : 'Ctrl K'}
</div>
</div>
</div>

{/* User Profile */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style={{ fontSize: '0.85rem', color: 'var(--color-text-muted)' }}>
<span style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)' }}>
{user?.email || 'Dev'}
</span>
<div style={{
width: '28px', height: '28px', borderRadius: '4px',
width: '28px', height: '28px', borderRadius: '4px',
background: 'var(--color-bg-input)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-main)', display: 'flex', alignItems: 'center', justifyContent: 'center',
Expand Down
70 changes: 39 additions & 31 deletions apps/web-dashboard/src/components/Layout/MainLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,70 @@ import { useState } from 'react';
import { useLocation, matchPath } from 'react-router-dom';
import Sidebar from './Sidebar';
import Header from './Header';
import ProjectNavbar from './ProjectNavbar';
import { useLayout } from '../../context/LayoutContext';
import BackToTop from './BackToTop';
// Use the new official logo from public directory
const logoImage = "https://cdn.jsdelivr.net/gh/yash-pouranik/urBackend@main/frontend/public/logo.png";

function MainLayout({ children }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
try {
return localStorage.getItem('urbackend-sidebar-collapsed') === 'true';
} catch {
return false;
}
});
const location = useLocation();
const { headerContent } = useLayout();

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

const toggleSidebarCollapse = () => {
setIsSidebarCollapsed(prev => {
const nextVal = !prev;
try {
localStorage.setItem('urbackend-sidebar-collapsed', String(nextVal));
} catch (err) {
console.warn("localStorage not accessible", err);
}
return nextVal;
});
};

return (
<div className="app-shell">
<div className={`app-shell ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
{/* Mobile Overlay - Only visible when sidebar is open on mobile */}
{isSidebarOpen && !isProjectRoute && (
{isSidebarOpen && (
<div
className="sidebar-overlay"
onClick={() => setIsSidebarOpen(false)}
></div>
)}

{/* Sidebar - Only show if NOT in a project route (or if we want global sidebar always, but plan said hide it) */}
{!isProjectRoute && (
<Sidebar
logo={logoImage}
isOpen={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}
/>
)}
{/* Sidebar - Always visible */}
<Sidebar
logo={logoImage}
isOpen={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}
isCollapsed={isSidebarCollapsed}
onToggleCollapse={toggleSidebarCollapse}
/>

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

{/* Global Header */}
{!isProjectRoute && (
<Header
logo={logoImage}
onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
// Hide toggle button if sidebar is hidden
showToggle={true}
>
{headerContent}
</Header>
)}

{/* Project Navigation Bar - Only visible in project routes */}
{isProjectRoute && <ProjectNavbar />}
{/* Global Header - Always visible */}
<Header
logo={logoImage}
onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
showToggle={true}
isSidebarCollapsed={isSidebarCollapsed}
>
{headerContent}
</Header>

{/* Dynamic Page Content */}
{/* Remove default margin-top as main-content has padding now. Remove padding for Database page. */}
<div
className="content-wrapper"
style={{
Expand Down
Loading
Loading