Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Port[]>([]);
Expand All @@ -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,
Expand Down Expand Up @@ -86,7 +90,7 @@ const App: React.FC = () => {
const handleAddPort = useCallback(
(newPort: Omit<Port, 'color'>) => {
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 = [
Expand All @@ -106,7 +110,7 @@ const App: React.FC = () => {
setFavoritePorts((prevPorts) => [...prevPorts, { ...newPort, color: randomColor }]);
setIsPortModalOpen(false);
},
[favoritePorts]
[favoritePorts, showError]
);

const handleRemovePort = useCallback((portNumber: number) => {
Expand Down
7 changes: 5 additions & 2 deletions src/components/AddPortModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +14,8 @@ const AddPortModal: React.FC<AddPortModalProps> = ({ isOpen, onClose, onAdd }) =
const [portNumber, setPortNumber] = useState('');
const [description, setDescription] = useState('');
const dialogRef = useRef<HTMLDialogElement>(null);
const { showError } = useAlert();


useEffect(() => {
const dialog = dialogRef.current;
Expand All @@ -29,11 +32,11 @@ const AddPortModal: React.FC<AddPortModalProps> = ({ 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 });
Expand Down
6 changes: 4 additions & 2 deletions src/components/AddSiteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +15,7 @@ const AddSiteModal: React.FC<AddSiteModalProps> = ({ isOpen, onClose, onAdd }) =
const [path, setPath] = useState('');
const [description, setDescription] = useState('');
const dialogRef = useRef<HTMLDialogElement>(null);
const { showWarning } = useAlert();

useEffect(() => {
const dialog = dialogRef.current;
Expand All @@ -29,11 +31,11 @@ const AddSiteModal: React.FC<AddSiteModalProps> = ({ 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({
Expand Down
6 changes: 4 additions & 2 deletions src/components/CustomPortSection.tsx
Original file line number Diff line number Diff line change
@@ -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}`;
Expand Down
6 changes: 4 additions & 2 deletions src/components/SearchSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>(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);
Expand Down
116 changes: 116 additions & 0 deletions src/hooks/useAlert.tsx
Original file line number Diff line number Diff line change
@@ -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<AlertProps[]>([]);

const showAlert = useCallback((alert: Omit<AlertProps, 'id'>) => {
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 (
<AlertContext.Provider value={{ showAlert, showSuccess, showError, showWarning, showInfo }}>
{children}
<div className="fixed top-4 right-4 z-50 space-y-3 max-w-md w-full pointer-events-none">
{alerts.map((alert) => (
<Alert key={alert.id} {...alert} onClose={() => removeAlert(alert.id)} />
))}
</div>
</AlertContext.Provider>
);
};

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 (
<div
className={`pointer-events-auto flex items-start gap-3 p-4 rounded-lg border shadow-lg ${currentStyle.bg} ${currentStyle.border} animate-in slide-in-from-right`}
role="alert"
>
<Icon className={`w-5 h-5 flex-shrink-0 mt-0.5 ${currentStyle.iconColor}`} />

<div className="flex-1">
{title && (
<h3 className={`font-semibold mb-1 ${currentStyle.text}`}>
{title}
</h3>
)}
<p className={`text-sm ${currentStyle.text}`}>
{message}
</p>
</div>

<button
onClick={onClose}
className={`flex-shrink-0 p-1 rounded hover:bg-black/5 dark:hover:bg-white/5 transition-colors ${currentStyle.text}`}
aria-label="Close alert"
>
<X className="w-4 h-4" />
</button>
</div>
);
};
26 changes: 26 additions & 0 deletions src/hooks/useAlertContext.ts
Original file line number Diff line number Diff line change
@@ -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<AlertProps, 'id'>) => 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<AlertContextType | undefined>(undefined);

export const useAlert = () => {
const context = useContext(AlertContext);
if (!context) {
throw new Error('useAlert must be used within AlertProvider');
}
return context;
};
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<StrictMode>
<App />
<AlertProvider>
<App />
</AlertProvider>
</StrictMode>
);