mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
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:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user