diff --git a/src/App.tsx b/src/App.tsx index f587f34..513d02c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,8 @@ import type { Port, LocalSite } from './types'; import SearchSection from './components/SearchSection'; import ThemeSwitcher from './components/ThemeSwitcher'; import { usePortStatus } from './hooks/usePortStatus'; +import { useAlert } from './hooks/useAlertContext'; + const App: React.FC = () => { const [favoritePorts, setFavoritePorts] = useState([]); @@ -26,6 +28,8 @@ const App: React.FC = () => { const portNumbers = useMemo(() => favoritePorts.map((port) => port.number), [favoritePorts]); + const { showError } = useAlert(); + const { portStatus } = usePortStatus(portNumbers, { interval: 30000, timeout: 3000, @@ -86,7 +90,7 @@ const App: React.FC = () => { const handleAddPort = useCallback( (newPort: Omit) => { if (favoritePorts.some((p) => p.number === newPort.number)) { - alert(`Port ${newPort.number} already exists in your favorites.`); + showError(`Port ${newPort.number} already exists in your favorites.`); return; } const colors = [ @@ -106,7 +110,7 @@ const App: React.FC = () => { setFavoritePorts((prevPorts) => [...prevPorts, { ...newPort, color: randomColor }]); setIsPortModalOpen(false); }, - [favoritePorts] + [favoritePorts, showError] ); const handleRemovePort = useCallback((portNumber: number) => { diff --git a/src/components/AddPortModal.tsx b/src/components/AddPortModal.tsx index 2e3b737..99b76a1 100644 --- a/src/components/AddPortModal.tsx +++ b/src/components/AddPortModal.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { CirclePlus } from 'lucide-react'; import type { Port } from '../types'; +import { useAlert } from '../hooks/useAlertContext'; interface AddPortModalProps { isOpen: boolean; @@ -13,6 +14,8 @@ const AddPortModal: React.FC = ({ isOpen, onClose, onAdd }) = const [portNumber, setPortNumber] = useState(''); const [description, setDescription] = useState(''); const dialogRef = useRef(null); + const { showError } = useAlert(); + useEffect(() => { const dialog = dialogRef.current; @@ -29,11 +32,11 @@ const AddPortModal: React.FC = ({ isOpen, onClose, onAdd }) = e.preventDefault(); const num = parseInt(portNumber, 10); if (isNaN(num) || num < 1 || num > 65535) { - alert('Please enter a valid port number (1-65535).'); + showError('Please enter a valid port number (1-65535).'); return; } if (!description.trim()) { - alert('Please enter a description.'); + showError('Please enter a description.'); return; } onAdd({ number: num, description }); diff --git a/src/components/AddSiteModal.tsx b/src/components/AddSiteModal.tsx index 4532d73..864cf10 100644 --- a/src/components/AddSiteModal.tsx +++ b/src/components/AddSiteModal.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { CirclePlus } from 'lucide-react'; import type { LocalSite } from '../types'; +import { useAlert } from '../hooks/useAlertContext'; interface AddSiteModalProps { isOpen: boolean; @@ -14,6 +15,7 @@ const AddSiteModal: React.FC = ({ isOpen, onClose, onAdd }) = const [path, setPath] = useState(''); const [description, setDescription] = useState(''); const dialogRef = useRef(null); + const { showWarning } = useAlert(); useEffect(() => { const dialog = dialogRef.current; @@ -29,11 +31,11 @@ const AddSiteModal: React.FC = ({ isOpen, onClose, onAdd }) = const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!name.trim()) { - alert('Please enter a site name.'); + showWarning('Please enter a site name.'); return; } if (!path.trim()) { - alert('Please enter a path.'); + showWarning('Please enter a path.'); return; } onAdd({ diff --git a/src/components/CustomPortSection.tsx b/src/components/CustomPortSection.tsx index 157ae84..9e32abf 100644 --- a/src/components/CustomPortSection.tsx +++ b/src/components/CustomPortSection.tsx @@ -1,18 +1,20 @@ import React, { useState } from 'react'; import { SquareArrowOutUpRight } from 'lucide-react'; +import { useAlert } from '../hooks/useAlertContext'; const CustomPortSection: React.FC = () => { const [port, setPort] = useState(''); + const { showWarning } = useAlert(); const goToCustomPort = () => { const portNumber = parseInt(port, 10); if (!port.trim()) { - alert('Please enter a port number.'); + showWarning('Please enter a port number.'); return; } if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) { - alert('Please enter a valid port number (1-65535).'); + showWarning('Please enter a valid port number (1-65535).'); return; } const url = `http://localhost:${portNumber}`; diff --git a/src/components/SearchSection.tsx b/src/components/SearchSection.tsx index d07c2a9..002e7d5 100644 --- a/src/components/SearchSection.tsx +++ b/src/components/SearchSection.tsx @@ -3,14 +3,16 @@ import React, { useState } from 'react'; import { SEARCH_ENGINES } from '../constants'; import { Search } from 'lucide-react'; import { SearchEngine } from '../types'; +import { useAlert } from '../hooks/useAlertContext'; const SearchSection: React.FC = () => { const [query, setQuery] = useState(''); const [selectedEngine, setSelectedEngine] = useState(SearchEngine.Google); - + const { showWarning } = useAlert(); + const handleSearch = () => { if (!query.trim()) { - alert('Please enter a search term.'); + showWarning('Please enter a search term.'); return; } const engine = SEARCH_ENGINES.find((e) => e.name === selectedEngine); diff --git a/src/hooks/useAlert.tsx b/src/hooks/useAlert.tsx new file mode 100644 index 0000000..5962412 --- /dev/null +++ b/src/hooks/useAlert.tsx @@ -0,0 +1,116 @@ +import { AlertCircle, CheckCircle, Info, XCircle, X } from 'lucide-react'; +import { useState, useCallback } from 'react'; +import { AlertContext } from './useAlertContext'; +import type { AlertProps } from './useAlertContext'; +import type { ReactNode } from 'react'; + +export const AlertProvider = ({ children }: { children: ReactNode }) => { + const [alerts, setAlerts] = useState([]); + + const showAlert = useCallback((alert: Omit) => { + const id = Date.now().toString(); + setAlerts((prev) => [...prev, { ...alert, id }]); + + setTimeout(() => { + setAlerts((prev) => prev.filter((a) => a.id !== id)); + }, 5000); + }, []); + + const showSuccess = useCallback((message: string, title?: string) => { + showAlert({ type: 'success', message, title }); + }, [showAlert]); + + const showError = useCallback((message: string, title?: string) => { + showAlert({ type: 'error', message, title }); + }, [showAlert]); + + const showWarning = useCallback((message: string, title?: string) => { + showAlert({ type: 'warning', message, title }); + }, [showAlert]); + + const showInfo = useCallback((message: string, title?: string) => { + showAlert({ type: 'info', message, title }); + }, [showAlert]); + + const removeAlert = useCallback((id: string) => { + setAlerts((prev) => prev.filter((a) => a.id !== id)); + }, []); + + return ( + + {children} +
+ {alerts.map((alert) => ( + removeAlert(alert.id)} /> + ))} +
+
+ ); +}; + +interface AlertComponentProps extends AlertProps { + onClose: () => void; +} +const Alert = ({ type, title, message, onClose }: AlertComponentProps) => { + const styles = { + success: { + bg: 'bg-green-50 dark:bg-green-900/20', + border: 'border-green-200 dark:border-green-800', + text: 'text-green-800 dark:text-green-200', + icon: CheckCircle, + iconColor: 'text-green-500', + }, + error: { + bg: 'bg-red-50 dark:bg-red-900/20', + border: 'border-red-200 dark:border-red-800', + text: 'text-red-800 dark:text-red-200', + icon: XCircle, + iconColor: 'text-red-500', + }, + warning: { + bg: 'bg-yellow-50 dark:bg-yellow-900/20', + border: 'border-yellow-200 dark:border-yellow-800', + text: 'text-yellow-800 dark:text-yellow-200', + icon: AlertCircle, + iconColor: 'text-yellow-500', + }, + info: { + bg: 'bg-blue-50 dark:bg-blue-900/20', + border: 'border-blue-200 dark:border-blue-800', + text: 'text-blue-800 dark:text-blue-200', + icon: Info, + iconColor: 'text-blue-500', + }, + }; + + const currentStyle = styles[type]; + const Icon = currentStyle.icon; + + return ( +
+ + +
+ {title && ( +

+ {title} +

+ )} +

+ {message} +

+
+ + +
+ ); +}; \ No newline at end of file diff --git a/src/hooks/useAlertContext.ts b/src/hooks/useAlertContext.ts new file mode 100644 index 0000000..589b77d --- /dev/null +++ b/src/hooks/useAlertContext.ts @@ -0,0 +1,26 @@ +import { createContext, useContext } from 'react'; + +export interface AlertProps { + type: 'success' | 'error' | 'warning' | 'info'; + title?: string; + message: string; + id: string; +} + +interface AlertContextType { + showAlert: (alert: Omit) => void; + showSuccess: (message: string, title?: string) => void; + showError: (message: string, title?: string) => void; + showWarning: (message: string, title?: string) => void; + showInfo: (message: string, title?: string) => void; +} + +export const AlertContext = createContext(undefined); + +export const useAlert = () => { + const context = useContext(AlertContext); + if (!context) { + throw new Error('useAlert must be used within AlertProvider'); + } + return context; +}; diff --git a/src/index.tsx b/src/index.tsx index 90da35d..3909e8e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,9 +4,12 @@ import '@fontsource-variable/inter'; import App from './App.tsx'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { AlertProvider } from './hooks/useAlert.tsx'; createRoot(document.getElementById('root')!).render( - + + + );