diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index f6199d629..4176f1cc1 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -6,49 +6,65 @@ import Home from './pages/Home'; import PublicRoute from './components/PublicRoute'; import ProtectedRoute from './components/ProtectedRoute'; import DefaultRedirect from './components/DefaultRedirect'; +import SettingsModal from './components/settings/SettingsModal'; const AppRoutes = () => { return ( - - {/* Public routes - redirect to /home or /onboarding if logged in */} - - - - } - /> - - - - } - /> + <> + + {/* Public routes - redirect to /home or /onboarding if logged in */} + + + + } + /> + + + + } + /> - {/* Protected routes */} - - - - } - /> - - - - } - /> + {/* Protected routes */} + + + + } + /> + + + + } + /> - {/* Default redirect based on auth status */} - } /> - + {/* Settings modal routes - protected */} + + + + } + /> + + {/* Default redirect based on auth status */} + } /> + + + {/* Settings Modal - renders over existing content when on settings routes */} + + ); }; diff --git a/src/components/settings/SettingsHome.tsx b/src/components/settings/SettingsHome.tsx new file mode 100644 index 000000000..2d8b4abea --- /dev/null +++ b/src/components/settings/SettingsHome.tsx @@ -0,0 +1,125 @@ +import { useAppDispatch } from '../../store/hooks'; +import { clearToken } from '../../store/authSlice'; +import { useSettingsNavigation } from './hooks/useSettingsNavigation'; +import SettingsHeader from './components/SettingsHeader'; +import SettingsMenuItem from './components/SettingsMenuItem'; + +const SettingsHome = () => { + const dispatch = useAppDispatch(); + const { navigateToSettings, closeSettings } = useSettingsNavigation(); + + const handleLogout = async () => { + await dispatch(clearToken()); + closeSettings(); + }; + + const menuItems = [ + { + id: 'connections', + title: 'Connections', + description: 'Manage your connected accounts and services', + icon: ( + + + + ), + onClick: () => navigateToSettings('connections') + }, + { + id: 'messaging', + title: 'Messaging', + description: 'Configure messaging preferences and templates', + icon: ( + + + + ), + onClick: () => navigateToSettings('messaging') + }, + { + id: 'privacy', + title: 'Privacy & Security', + description: 'Control your privacy and security settings', + icon: ( + + + + ), + onClick: () => navigateToSettings('privacy') + }, + { + id: 'profile', + title: 'Profile', + description: 'Update your profile information and preferences', + icon: ( + + + + ), + onClick: () => navigateToSettings('profile') + }, + { + id: 'advanced', + title: 'Advanced', + description: 'Advanced configuration and developer options', + icon: ( + + + + + ), + onClick: () => navigateToSettings('advanced') + }, + { + id: 'billing', + title: 'Billing', + description: 'Manage your subscription and payment methods', + icon: ( + + + + ), + onClick: () => navigateToSettings('billing') + } + ]; + + return ( +
+ + +
+ {/* Menu items */} +
+ {menuItems.map((item, index) => ( + + ))} + + {/* Separator before logout */} +
+ + {/* Logout */} + + + + } + title="Log out" + description="Sign out of your account" + onClick={handleLogout} + dangerous + /> +
+
+
+ ); +}; + +export default SettingsHome; \ No newline at end of file diff --git a/src/components/settings/SettingsLayout.tsx b/src/components/settings/SettingsLayout.tsx new file mode 100644 index 000000000..aa47ab5de --- /dev/null +++ b/src/components/settings/SettingsLayout.tsx @@ -0,0 +1,75 @@ +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(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 = ( +
+
e.stopPropagation()} + > + {children} +
+
+ ); + + return createPortal(modalContent, document.body); +}; + +export default SettingsLayout; \ No newline at end of file diff --git a/src/components/settings/SettingsModal.tsx b/src/components/settings/SettingsModal.tsx new file mode 100644 index 000000000..fefc5fb34 --- /dev/null +++ b/src/components/settings/SettingsModal.tsx @@ -0,0 +1,36 @@ +import { Routes, Route } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import SettingsLayout from './SettingsLayout'; +import SettingsHome from './SettingsHome'; +import ConnectionsPanel from './panels/ConnectionsPanel'; +import { useSettingsNavigation } from './hooks/useSettingsNavigation'; + +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 ( + + + } /> + } /> + + {/* Future settings panels */} + Messaging settings coming soon} /> + Privacy settings coming soon} /> + Profile settings coming soon} /> + Advanced settings coming soon} /> + Billing settings coming soon} /> + + + ); +}; + +export default SettingsModal; \ No newline at end of file diff --git a/src/components/settings/components/SettingsBackButton.tsx b/src/components/settings/components/SettingsBackButton.tsx new file mode 100644 index 000000000..a970bd845 --- /dev/null +++ b/src/components/settings/components/SettingsBackButton.tsx @@ -0,0 +1,38 @@ +interface SettingsBackButtonProps { + onClick: () => void; + title?: string; + className?: string; +} + +const SettingsBackButton = ({ + onClick, + title = 'Settings', + className = '' +}: SettingsBackButtonProps) => { + return ( +
+ +
+ ); +}; + +export default SettingsBackButton; \ No newline at end of file diff --git a/src/components/settings/components/SettingsHeader.tsx b/src/components/settings/components/SettingsHeader.tsx new file mode 100644 index 000000000..4887be2b3 --- /dev/null +++ b/src/components/settings/components/SettingsHeader.tsx @@ -0,0 +1,46 @@ +import { useAppSelector } from '../../../store/hooks'; + +interface SettingsHeaderProps { + className?: string; +} + +const SettingsHeader = ({ className = '' }: SettingsHeaderProps) => { + const user = useAppSelector((state) => state.user.user); + + // Get user initials for avatar + const getInitials = (firstName?: string, lastName?: string): string => { + if (!firstName && !lastName) return 'U'; + const first = firstName?.charAt(0) || ''; + const last = lastName?.charAt(0) || ''; + return (first + last).toUpperCase(); + }; + + const initials = getInitials(user?.firstName, user?.lastName); + const displayName = user?.firstName && user?.lastName + ? `${user.firstName} ${user.lastName}` + : user?.firstName || user?.username || 'User'; + const displayUsername = user?.username ? `@${user.username}` : ''; + + return ( +
+
+ {/* Avatar */} +
+ {initials} +
+ + {/* User info */} +
+

+ {displayName} +

+ {displayUsername && ( +

{displayUsername}

+ )} +
+
+
+ ); +}; + +export default SettingsHeader; \ No newline at end of file diff --git a/src/components/settings/components/SettingsMenuItem.tsx b/src/components/settings/components/SettingsMenuItem.tsx new file mode 100644 index 000000000..68f148804 --- /dev/null +++ b/src/components/settings/components/SettingsMenuItem.tsx @@ -0,0 +1,64 @@ +import { ReactNode } from 'react'; + +interface SettingsMenuItemProps { + icon: ReactNode; + title: string; + description?: string; + onClick: () => void; + dangerous?: boolean; + className?: string; +} + +const SettingsMenuItem = ({ + icon, + title, + description, + onClick, + dangerous = false, + className = '' +}: SettingsMenuItemProps) => { + const textColor = dangerous ? 'text-red-600' : 'text-gray-900'; + const descriptionColor = dangerous ? 'text-red-500/70' : 'text-gray-500'; + + return ( + + ); +}; + +export default SettingsMenuItem; \ No newline at end of file diff --git a/src/components/settings/components/SettingsPanelLayout.tsx b/src/components/settings/components/SettingsPanelLayout.tsx new file mode 100644 index 000000000..d29cf9558 --- /dev/null +++ b/src/components/settings/components/SettingsPanelLayout.tsx @@ -0,0 +1,27 @@ +import { ReactNode } from 'react'; +import SettingsBackButton from './SettingsBackButton'; + +interface SettingsPanelLayoutProps { + title: string; + onBack: () => void; + children: ReactNode; + className?: string; +} + +const SettingsPanelLayout = ({ + title, + onBack, + children, + className = '' +}: SettingsPanelLayoutProps) => { + return ( +
+ +
+ {children} +
+
+ ); +}; + +export default SettingsPanelLayout; \ No newline at end of file diff --git a/src/components/settings/hooks/useSettingsAnimation.ts b/src/components/settings/hooks/useSettingsAnimation.ts new file mode 100644 index 000000000..fa960caf6 --- /dev/null +++ b/src/components/settings/hooks/useSettingsAnimation.ts @@ -0,0 +1,67 @@ +import { useState, useEffect } from 'react'; + +export type AnimationState = 'entering' | 'entered' | 'exiting' | 'exited'; + +interface SettingsAnimationHook { + isVisible: boolean; + animationState: AnimationState; + startEntry: () => void; + startExit: () => void; +} + +export const useSettingsAnimation = ( + duration = 200 +): SettingsAnimationHook => { + const [animationState, setAnimationState] = useState('exited'); + + const isVisible = animationState === 'entering' || animationState === 'entered'; + + const startEntry = () => { + setAnimationState('entering'); + setTimeout(() => { + setAnimationState('entered'); + }, duration); + }; + + const startExit = () => { + setAnimationState('exiting'); + setTimeout(() => { + setAnimationState('exited'); + }, duration); + }; + + return { + isVisible, + animationState, + startEntry, + startExit + }; +}; + +// Hook for panel slide animations (slide from right) +export const usePanelAnimation = (isActive: boolean, duration = 250) => { + const [mounted, setMounted] = useState(isActive); + + useEffect(() => { + if (isActive) { + setMounted(true); + } else { + const timer = setTimeout(() => setMounted(false), duration); + return () => clearTimeout(timer); + } + }, [isActive, duration]); + + const getPanelClasses = () => { + const baseClasses = 'transition-all duration-250 ease-out'; + if (!mounted) return `${baseClasses} opacity-0`; + + return isActive + ? `${baseClasses} opacity-100 translate-x-0` + : `${baseClasses} opacity-0 translate-x-full`; + }; + + return { + mounted, + panelClasses: getPanelClasses() + }; +}; \ No newline at end of file diff --git a/src/components/settings/hooks/useSettingsNavigation.ts b/src/components/settings/hooks/useSettingsNavigation.ts new file mode 100644 index 000000000..145ae9811 --- /dev/null +++ b/src/components/settings/hooks/useSettingsNavigation.ts @@ -0,0 +1,57 @@ +import { useNavigate, useLocation } from 'react-router-dom'; +import { useCallback } from 'react'; + +export type SettingsRoute = 'home' | 'connections' | 'messaging' | 'privacy' | 'profile' | 'advanced' | 'billing'; + +interface SettingsNavigationHook { + currentRoute: SettingsRoute; + navigateToSettings: (route?: SettingsRoute) => void; + navigateBack: () => void; + closeSettings: () => void; +} + +export const useSettingsNavigation = (): SettingsNavigationHook => { + const navigate = useNavigate(); + const location = useLocation(); + + // Determine current settings route from URL + const getCurrentRoute = (): SettingsRoute => { + const path = location.pathname; + if (path.includes('/settings/connections')) return 'connections'; + if (path.includes('/settings/messaging')) return 'messaging'; + if (path.includes('/settings/privacy')) return 'privacy'; + if (path.includes('/settings/profile')) return 'profile'; + if (path.includes('/settings/advanced')) return 'advanced'; + if (path.includes('/settings/billing')) return 'billing'; + return 'home'; + }; + + const currentRoute = getCurrentRoute(); + + const navigateToSettings = useCallback((route: SettingsRoute = 'home') => { + if (route === 'home') { + navigate('/settings'); + } else { + navigate(`/settings/${route}`); + } + }, [navigate]); + + const navigateBack = useCallback(() => { + if (currentRoute === 'home') { + navigate('/home'); + } else { + navigate('/settings'); + } + }, [navigate, currentRoute]); + + const closeSettings = useCallback(() => { + navigate('/home'); + }, [navigate]); + + return { + currentRoute, + navigateToSettings, + navigateBack, + closeSettings + }; +}; \ No newline at end of file diff --git a/src/components/settings/panels/ConnectionsPanel.tsx b/src/components/settings/panels/ConnectionsPanel.tsx new file mode 100644 index 000000000..99dc4633b --- /dev/null +++ b/src/components/settings/panels/ConnectionsPanel.tsx @@ -0,0 +1,210 @@ +import { useState, useMemo } from 'react'; +import { useAppSelector } from '../../../store/hooks'; +import { selectIsAuthenticated } from '../../../store/telegramSelectors'; +import { useSettingsNavigation } from '../hooks/useSettingsNavigation'; +import SettingsPanelLayout from '../components/SettingsPanelLayout'; +import TelegramConnectionModal from '../../TelegramConnectionModal'; + +import BinanceIcon from '../../../assets/icons/binance.svg'; +import NotionIcon from '../../../assets/icons/notion.svg'; +import TelegramIcon from '../../../assets/icons/telegram.svg'; +import MetamaskIcon from '../../../assets/icons/metamask.svg'; +import GoogleIcon from '../../../assets/icons/GoogleIcon'; + +interface ConnectOption { + id: string; + name: string; + description: string; + icon: React.ReactElement; + comingSoon?: boolean; +} + +// Reused from ConnectStep.tsx - helper to check saved session +const hasSavedSession = (): boolean => { + try { + return !!localStorage.getItem('telegram_session'); + } catch { + return false; + } +}; + +const ConnectionsPanel = () => { + const { navigateBack } = useSettingsNavigation(); + const [isTelegramModalOpen, setIsTelegramModalOpen] = useState(false); + + // Redux state + const isTelegramAuthenticated = useAppSelector(selectIsAuthenticated); + const sessionString = useAppSelector((state) => state.telegram.sessionString); + + // Check if Telegram account is connected (authenticated or has saved session) + const isTelegramConnected = useMemo(() => { + return isTelegramAuthenticated || !!sessionString || hasSavedSession(); + }, [isTelegramAuthenticated, sessionString]); + + // Connection options - reused from ConnectStep.tsx + const connectOptions: ConnectOption[] = [ + { + id: 'telegram', + name: 'Telegram', + description: 'Organize chats, automate messages and get insights.', + icon: Telegram, + }, + { + id: 'google', + name: 'Google', + description: 'Manage emails, contacts and calendar events', + icon: , + comingSoon: true, + }, + { + id: 'notion', + name: 'Notion', + description: 'Manage tasks, documents and everything else in your Notion', + icon: Notion, + comingSoon: true, + }, + { + id: 'wallet', + name: 'Web3 Wallet', + description: 'Trade the trenches in a safe and secure way.', + icon: Metamask, + comingSoon: true, + }, + { + id: 'exchange', + name: 'Crypto Trading Exchanges', + description: 'Connect and make trades with deep insights.', + icon: Binance, + comingSoon: true, + }, + ]; + + // Check if an account is connected + const isAccountConnected = (accountId: string): boolean => { + if (accountId === 'telegram') { + return isTelegramConnected; + } + // Add other account checks here when implemented + return false; + }; + + const handleConnect = (provider: string) => { + if (provider === 'telegram') { + if (isTelegramConnected) { + // TODO: Show disconnect confirmation + console.log('Disconnect Telegram'); + } else { + setIsTelegramModalOpen(true); + } + return; + } + + if (connectOptions.find(opt => opt.id === provider)?.comingSoon) { + console.log(`${provider} coming soon`); + return; + } + + console.log(`Connecting to ${provider}`); + }; + + const handleTelegramComplete = () => { + setIsTelegramModalOpen(false); + }; + + const getConnectionStatus = (optionId: string) => { + const isConnected = isAccountConnected(optionId); + if (isConnected) { + return { label: 'Connected', className: 'bg-green-100 text-green-700 border-green-200' }; + } + const option = connectOptions.find(opt => opt.id === optionId); + if (option?.comingSoon) { + return { label: 'Coming Soon', className: 'bg-gray-100 text-gray-600 border-gray-200' }; + } + return { label: 'Not Connected', className: 'bg-red-50 text-red-600 border-red-200' }; + }; + + return ( + <> + +
+
+

Connected Services

+

+ Manage your connected accounts and services. Connect more accounts for better AI intelligence. +

+
+ +
+ {connectOptions.map((option) => { + const isConnected = isAccountConnected(option.id); + const status = getConnectionStatus(option.id); + + return ( +
+
+
+ {option.icon} +
+
+

{option.name}

+

{option.description}

+
+
+ +
+ {/* Status badge */} + + {status.label} + + + {/* Action button */} + {!option.comingSoon && ( + + )} +
+
+ ); + })} +
+ + {/* Security notice */} +
+
+ + + +
+

🔒 Privacy & Security

+

+ All data and credentials are stored locally with zero-data retention policy. + Your information is encrypted and never shared with third parties. +

+
+
+
+
+
+ + {/* Telegram Connection Modal */} + setIsTelegramModalOpen(false)} + onComplete={handleTelegramComplete} + /> + + ); +}; + +export default ConnectionsPanel; \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 95e767d8a..f74a2a29e 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -36,8 +36,7 @@ const Home = () => { }; const handleManageConnections = () => { - // TODO: Navigate to connections management page - console.log('Manage connections'); + navigate('/settings/connections'); }; const handleDeleteAllData = () => {