Add left mini sidebar with page-based settings (#79)

* Add MiniSidebar component and update routing for Conversations and Agents pages

- Introduced a new MiniSidebar component for navigation, enhancing user experience.
- Updated App component to include MiniSidebar and adjusted layout for better responsiveness.
- Added new routes for Conversations and Agents pages, ensuring they are protected routes.
- Refactored Settings route to render as a page instead of a modal, streamlining settings management.
- Removed obsolete SettingsModal and SettingsLayout components to clean up the codebase.

* Update layout and styles in Settings components for improved user experience

- Updated the `MiniSidebar` component to enhance navigation with adjusted z-index and background styles.
- Refactored `SettingsHome` and `SettingsHeader` components to improve layout and responsiveness.
- Added max-width constraints to various elements for better alignment and presentation.
- Cleaned up commented-out code in `MiniSidebar` for clarity.
- Updated subproject commit reference in the skills directory.
This commit is contained in:
Steven Enamakel
2026-02-09 17:47:59 +05:30
committed by GitHub
parent ab9e7b7a0f
commit 83cfa95378
13 changed files with 418 additions and 355 deletions
+117
View File
@@ -0,0 +1,117 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useAppSelector } from '../store/hooks';
const navItems = [
{
id: 'home',
label: 'Home',
path: '/home',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a2 2 0 01-2-2v-4a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2z"
/>
</svg>
),
},
// {
// id: 'conversations',
// label: 'Conversations',
// path: '/conversations',
// icon: (
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// strokeWidth={2}
// d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
// />
// </svg>
// ),
// },
// {
// id: 'agents',
// label: 'Agents',
// path: '/agents',
// icon: (
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// strokeWidth={2}
// d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
// />
// </svg>
// ),
// },
{
id: 'settings',
label: 'Settings',
path: '/settings',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
),
},
];
const MiniSidebar = () => {
const location = useLocation();
const navigate = useNavigate();
const token = useAppSelector(state => state.auth.token);
// Hide sidebar when not authenticated or on public/onboarding routes
const hiddenPaths = ['/', '/login', '/onboarding'];
if (!token || hiddenPaths.includes(location.pathname)) {
return null;
}
const isActive = (path: string) => {
if (path === '/settings') return location.pathname.startsWith('/settings');
return location.pathname === path;
};
return (
<div className="w-14 flex-shrink-0 bg-black backdrop-blur-md border-r border-white/10 flex flex-col items-center py-4 gap-2 z-50 relative">
{navItems.map(item => {
const active = isActive(item.path);
return (
<div key={item.id} className="relative group">
<button
onClick={() => navigate(item.path)}
className={`w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-200 cursor-pointer ${
active
? 'bg-white/10 text-black'
: 'text-stone-500 hover:text-stone-300 hover:bg-white/5'
}`}
aria-label={item.label}>
{item.icon}
</button>
{/* Tooltip - appears to the right */}
<div className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 bg-stone-800 text-white text-xs rounded-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-150">
{item.label}
</div>
</div>
);
})}
</div>
);
};
export default MiniSidebar;
+2 -2
View File
@@ -196,10 +196,10 @@ const SettingsHome = () => {
];
return (
<div className="overflow-hidden h-full flex flex-col">
<div className="overflow-hidden h-full flex flex-col z-10 relative">
<SettingsHeader />
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto max-w-md mx-auto">
<div className="p-4 space-y-6">
{/* Main Settings */}
<div>
@@ -1,73 +0,0 @@
import { ReactNode, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
interface SettingsLayoutProps {
children: ReactNode;
onClose: () => void;
}
const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
const modalRef = useRef<HTMLDivElement>(null);
// Handle escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose]);
// Handle backdrop click
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
// Focus management for accessibility
useEffect(() => {
const previousFocus = document.activeElement as HTMLElement;
// Focus the modal container
if (modalRef.current) {
modalRef.current.focus();
}
// Restore focus when modal closes
return () => {
if (previousFocus && previousFocus.focus) {
previousFocus.focus();
}
};
}, []);
const modalContent = (
<div
className="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-lg flex items-center justify-center p-4"
onClick={handleBackdropClick}
role="dialog"
aria-modal="true"
aria-labelledby="settings-modal-title">
<div
ref={modalRef}
className="bg-stone-900 rounded-3xl shadow-large w-full max-w-[520px] max-h-[90vh] overflow-hidden animate-slide-right focus:outline-none focus:ring-0"
style={{
animationDuration: '300ms',
animationTimingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
animationFillMode: 'both',
}}
tabIndex={-1}
onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
);
return createPortal(modalContent, document.body);
};
export default SettingsLayout;
-45
View File
@@ -1,45 +0,0 @@
import { Route, Routes, useLocation } from 'react-router-dom';
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
import AdvancedPanel from './panels/AdvancedPanel';
import BillingPanel from './panels/BillingPanel';
import ConnectionsPanel from './panels/ConnectionsPanel';
import MessagingPanel from './panels/MessagingPanel';
import PrivacyPanel from './panels/PrivacyPanel';
import ProfilePanel from './panels/ProfilePanel';
import TeamInvitesPanel from './panels/TeamInvitesPanel';
import TeamMembersPanel from './panels/TeamMembersPanel';
import TeamPanel from './panels/TeamPanel';
import SettingsHome from './SettingsHome';
import SettingsLayout from './SettingsLayout';
const SettingsModal = () => {
const location = useLocation();
const { closeSettings } = useSettingsNavigation();
// Only render modal when on settings routes
const isSettingsRoute = location.pathname.startsWith('/settings');
if (!isSettingsRoute) {
return null;
}
return (
<SettingsLayout onClose={closeSettings}>
<Routes>
<Route path="/settings" element={<SettingsHome />} />
<Route path="/settings/connections" element={<ConnectionsPanel />} />
<Route path="/settings/messaging" element={<MessagingPanel />} />
<Route path="/settings/privacy" element={<PrivacyPanel />} />
<Route path="/settings/profile" element={<ProfilePanel />} />
<Route path="/settings/advanced" element={<AdvancedPanel />} />
<Route path="/settings/billing" element={<BillingPanel />} />
<Route path="/settings/team" element={<TeamPanel />} />
<Route path="/settings/team/members" element={<TeamMembersPanel />} />
<Route path="/settings/team/invites" element={<TeamInvitesPanel />} />
</Routes>
</SettingsLayout>
);
};
export default SettingsModal;
@@ -1,5 +1,3 @@
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
interface SettingsHeaderProps {
className?: string;
title?: string;
@@ -13,53 +11,32 @@ const SettingsHeader = ({
showBackButton = false,
onBack,
}: SettingsHeaderProps) => {
const { closeSettings } = useSettingsNavigation();
return (
<div className={`bg-black/30 border-b border-stone-700 p-6 relative ${className}`}>
<div className="flex items-center justify-between">
<div className="flex items-center">
{/* Back button */}
{showBackButton && onBack && (
<button
onClick={onBack}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors mr-3"
aria-label="Go back">
<svg
className="w-5 h-5 opacity-70"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
)}
<div className={`bg-black/30 border-b border-stone-700 p-3 relative ${className}`}>
<div className="flex items-center">
{/* Back button */}
{showBackButton && onBack && (
<button
onClick={onBack}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors mr-3"
aria-label="Go back">
<svg
className="w-5 h-5 opacity-70"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
)}
{/* Title */}
<h2 className="text-lg font-semibold text-white" id="settings-modal-title">
{title}
</h2>
</div>
{/* Close button */}
<button
onClick={closeSettings}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors"
aria-label="Close settings">
<svg className="w-5 h-5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/* Title */}
<h2 className="text-lg font-semibold text-white">{title}</h2>
</div>
</div>
);
+140 -138
View File
@@ -225,152 +225,137 @@ const BillingPanel = () => {
</button>
</div>
{/* ── Plan tier cards ───────────────────────────────────── */}
<div className="space-y-2 px-4">
{PLANS.map(plan => {
const isCurrent = plan.tier === currentTier;
const isUpgrade = checkIsUpgrade(plan.tier, currentTier);
const savings = annualSavings(plan, billingInterval);
const isThisPurchasing = isPurchasing && purchasingTier === plan.tier;
<div className="max-w-md mx-auto">
{/* ── Plan tier cards ───────────────────────────────────── */}
<div className="space-y-2 px-4">
{PLANS.map(plan => {
const isCurrent = plan.tier === currentTier;
const isUpgrade = checkIsUpgrade(plan.tier, currentTier);
const savings = annualSavings(plan, billingInterval);
const isThisPurchasing = isPurchasing && purchasingTier === plan.tier;
return (
<div
key={plan.tier}
className={`rounded-2xl border p-3 transition-all ${
isCurrent
? 'border-primary-500/40 bg-primary-500/5'
: 'border-stone-700/50 bg-stone-800/40'
}`}>
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h4 className="text-sm font-semibold text-white">{plan.name}</h4>
{/* Features inline with title */}
{plan.features.map(f => (
<span key={f.text} className="text-xs text-stone-300">
<span className="text-stone-500 mx-1"></span>
{f.text}
return (
<div
key={plan.tier}
className={`rounded-2xl border p-3 transition-all ${
isCurrent
? 'border-primary-500/40 bg-primary-500/5'
: 'border-stone-700/50 bg-stone-800/40'
}`}>
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h4 className="text-sm font-semibold text-white">{plan.name}</h4>
{/* Features inline with title */}
{plan.features.map(f => (
<span key={f.text} className="text-xs text-stone-300">
<span className="text-stone-500 mx-1"></span>
{f.text}
</span>
))}
{isCurrent && (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-full bg-primary-500/20 text-primary-400 border border-primary-500/30">
Current
</span>
)}
{savings && (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-full bg-sage-500/20 text-sage-400 border border-sage-500/30">
Save {savings}%
</span>
)}
</div>
<div className="mt-0.5 flex items-baseline gap-1">
<span className="text-xl font-bold text-white">
{displayPrice(plan, billingInterval)}
</span>
))}
{isCurrent && (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-full bg-primary-500/20 text-primary-400 border border-primary-500/30">
Current
</span>
)}
{savings && (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded-full bg-sage-500/20 text-sage-400 border border-sage-500/30">
Save {savings}%
</span>
)}
</div>
<div className="mt-0.5 flex items-baseline gap-1">
<span className="text-xl font-bold text-white">
{displayPrice(plan, billingInterval)}
</span>
{plan.tier !== 'FREE' && (
<span className="text-xs text-stone-400">/mo</span>
)}
{plan.tier !== 'FREE' && billingInterval === 'annual' && (
<span className="text-xs text-stone-500 ml-1">
(billed ${plan.annualPrice}/yr)
</span>
)}
{plan.tier !== 'FREE' && (
<span className="text-xs text-stone-400">/mo</span>
)}
{plan.tier !== 'FREE' && billingInterval === 'annual' && (
<span className="text-xs text-stone-500 ml-1">
(billed ${plan.annualPrice}/yr)
</span>
)}
</div>
</div>
{/* Action button */}
{isUpgrade && (
<button
onClick={() => handleUpgrade(plan.tier)}
disabled={isPurchasing}
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors flex-shrink-0 ${
isPurchasing
? 'bg-stone-700/40 text-stone-500 cursor-not-allowed'
: 'bg-primary-500 hover:bg-primary-600 text-white'
}`}>
{isThisPurchasing ? 'Waiting...' : 'Upgrade'}
</button>
)}
</div>
{/* Action button */}
{isUpgrade && (
<button
onClick={() => handleUpgrade(plan.tier)}
disabled={isPurchasing}
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors flex-shrink-0 ${
isPurchasing
? 'bg-stone-700/40 text-stone-500 cursor-not-allowed'
: 'bg-primary-500 hover:bg-primary-600 text-white'
}`}>
{isThisPurchasing ? 'Waiting...' : 'Upgrade'}
</button>
)}
</div>
</div>
);
})}
</div>
{/* ── Purchasing overlay message ────────────────────────── */}
{isPurchasing && (
<div className="rounded-xl bg-amber-500/10 border border-amber-500/20 p-3 mx-4">
<div className="flex items-center gap-2">
<svg
className="w-4 h-4 text-amber-400 animate-spin"
fill="none"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<p className="text-xs text-amber-300">
Waiting for payment confirmation... Complete checkout in the browser window that
opened.
</p>
</div>
);
})}
</div>
)}
{/* ── Pay with crypto toggle ────────────────────────────── */}
<div className="flex items-center justify-between rounded-xl bg-stone-800/40 border border-stone-700/40 p-3 mx-4">
<div>
<p className="text-xs font-medium text-white">Pay with Crypto</p>
<p className="text-[11px] text-stone-400 mt-0.5">
You can choose to pay annually using crypto
</p>
</div>
<button
onClick={() => setPaymentMethod(m => (m === 'card' ? 'crypto' : 'card'))}
className={`relative w-10 h-5 rounded-full transition-colors ${
paymentMethod === 'crypto' ? 'bg-primary-500' : 'bg-stone-600'
}`}
role="switch"
aria-checked={paymentMethod === 'crypto'}>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
paymentMethod === 'crypto' ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
{/* ── Upgrade benefits ───────────────────────────────────── */}
<div className="px-4 pb-4 pt-2">
<div className="rounded-xl bg-gradient-to-br from-primary-500/10 to-sage-500/10 border border-primary-500/20 p-4">
<h3 className="text-sm font-semibold text-white mb-2">Why upgrade?</h3>
<ul className="space-y-1.5 text-xs text-stone-300">
<li className="flex items-start gap-2">
{/* ── Purchasing overlay message ────────────────────────── */}
{isPurchasing && (
<div className="rounded-xl bg-amber-500/10 border border-amber-500/20 p-3 mx-4">
<div className="flex items-center gap-2">
<svg
className="w-4 h-4 text-sage-400 flex-shrink-0 mt-0.5"
className="w-4 h-4 text-amber-400 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<span>Unlock higher daily limits for more AI interactions</span>
</li>
{currentTier === 'FREE' && (
<p className="text-xs text-amber-300">
Waiting for payment confirmation... Complete checkout in the browser window that
opened.
</p>
</div>
</div>
)}
{/* ── Pay with crypto toggle ────────────────────────────── */}
<div className="flex items-center justify-between rounded-xl bg-stone-800/40 border border-stone-700/40 p-3 mx-4">
<div>
<p className="text-xs font-medium text-white">Pay with Crypto</p>
<p className="text-[11px] text-stone-400 mt-0.5">
You can choose to pay annually using crypto
</p>
</div>
<button
onClick={() => setPaymentMethod(m => (m === 'card' ? 'crypto' : 'card'))}
className={`relative w-10 h-5 rounded-full transition-colors ${
paymentMethod === 'crypto' ? 'bg-primary-500' : 'bg-stone-600'
}`}
role="switch"
aria-checked={paymentMethod === 'crypto'}>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
paymentMethod === 'crypto' ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
{/* ── Upgrade benefits ───────────────────────────────────── */}
<div className="px-4 pb-4 pt-2">
<div className="rounded-xl bg-gradient-to-br from-primary-500/10 to-sage-500/10 border border-primary-500/20 p-4">
<h3 className="text-sm font-semibold text-white mb-2">Why upgrade?</h3>
<ul className="space-y-1.5 text-xs text-stone-300">
<li className="flex items-start gap-2">
<svg
className="w-4 h-4 text-sage-400 flex-shrink-0 mt-0.5"
@@ -384,12 +369,29 @@ const BillingPanel = () => {
d="M5 13l4 4L19 7"
/>
</svg>
<span>
Save up to 20% with annual plans and never worry about hitting limits
</span>
<span>Unlock higher daily limits for more AI interactions</span>
</li>
)}
</ul>
{currentTier === 'FREE' && (
<li className="flex items-start gap-2">
<svg
className="w-4 h-4 text-sage-400 flex-shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span>
Save up to 20% with annual plans and never worry about hitting limits
</span>
</li>
)}
</ul>
</div>
</div>
</div>
</div>