diff --git a/.build/prod/nginx.Dockerfile b/.build/prod/nginx.Dockerfile new file mode 100644 index 0000000..f1153d4 --- /dev/null +++ b/.build/prod/nginx.Dockerfile @@ -0,0 +1,5 @@ +FROM nginx + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +WORKDIR /etc/nginx diff --git a/.build/prod/nginx.conf b/.build/prod/nginx.conf new file mode 100644 index 0000000..ae4aad4 --- /dev/null +++ b/.build/prod/nginx.conf @@ -0,0 +1,44 @@ +server { + listen 80; + + server_name _; + access_log /dev/stdout; + error_log /dev/stdout info; + + location /api/ { + proxy_pass http://api:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /ws/ { + proxy_pass http://api:8000/ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket timeout settings + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_connect_timeout 60s; + } + + location / { + # In development, proxy to Vite dev server + proxy_pass http://node:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/.build/prod/node.Dockerfile b/.build/prod/node.Dockerfile new file mode 100644 index 0000000..4e42054 --- /dev/null +++ b/.build/prod/node.Dockerfile @@ -0,0 +1,5 @@ +FROM node:24-bookworm-slim + +WORKDIR /var/www/html/public + +CMD npm run dev -- --host 0.0.0.0 diff --git a/.build/prod/python.Dockerfile b/.build/prod/python.Dockerfile new file mode 100644 index 0000000..74798ca --- /dev/null +++ b/.build/prod/python.Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11.4-slim-buster + +WORKDIR /var/www/html/ + +COPY requirements.txt . + +RUN pip install -r requirements.txt diff --git a/public/src/App.tsx b/public/src/App.tsx index 3224837..3a5f245 100644 --- a/public/src/App.tsx +++ b/public/src/App.tsx @@ -4,6 +4,8 @@ import { Register } from './components/auth/Register'; import { ProtectedRoute } from './components/auth/ProtectedRoute'; import { Dashboard } from './pages/Dashboard'; import { SharedMap } from './pages/SharedMap'; +import { ToastContainer } from './components/common/ToastContainer'; +import { ConfirmContainer } from './components/common/ConfirmContainer'; function App() { return ( @@ -22,6 +24,8 @@ function App() { /> } /> + + ); } diff --git a/public/src/components/auth/Login.tsx b/public/src/components/auth/Login.tsx index 1f2895e..6b0260f 100644 --- a/public/src/components/auth/Login.tsx +++ b/public/src/components/auth/Login.tsx @@ -1,11 +1,13 @@ import { useState, FormEvent } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { useAuthStore } from '../../stores/authStore'; +import { useUIStore } from '../../stores/uiStore'; export function Login() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const { login, isLoading, error, clearError } = useAuthStore(); + const { darkMode, toggleDarkMode } = useUIStore(); const navigate = useNavigate(); const handleSubmit = async (e: FormEvent) => { @@ -21,21 +23,41 @@ export function Login() { }; return ( -
-
-

- ISP Wiremap -

+
+
+ +
-
+
+
+
+ + + +
+

+ ISP Wiremap +

+

+ Sign in to your account +

+
+ + {error && ( -
+
{error}
)}
-
-
@@ -67,15 +91,25 @@ export function Login() { -
- Don't have an account? - - Register +
+ Don't have an account? + + Create one
diff --git a/public/src/components/auth/Register.tsx b/public/src/components/auth/Register.tsx index 4b6b48b..8cb08ce 100644 --- a/public/src/components/auth/Register.tsx +++ b/public/src/components/auth/Register.tsx @@ -1,26 +1,30 @@ import { useState, FormEvent } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { useAuthStore } from '../../stores/authStore'; +import { useUIStore } from '../../stores/uiStore'; export function Register() { const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [validationError, setValidationError] = useState(''); const { register, isLoading, error, clearError } = useAuthStore(); + const { darkMode, toggleDarkMode } = useUIStore(); const navigate = useNavigate(); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); clearError(); + setValidationError(''); if (password !== confirmPassword) { - alert('Passwords do not match'); + setValidationError('Passwords do not match'); return; } if (password.length < 6) { - alert('Password must be at least 6 characters'); + setValidationError('Password must be at least 6 characters'); return; } @@ -33,24 +37,41 @@ export function Register() { }; return ( -
-
-

- Create Account -

-

- Join ISP Wiremap -

+
+
+ +
+ +
+
+
+ + + +
+

+ Create Account +

+

+ Join ISP Wiremap today +

+
- {error && ( -
- {error} + {(error || validationError) && ( +
+ {error || validationError}
)}
-
-
-
-
@@ -115,15 +140,25 @@ export function Register() { -
- Already have an account? - - Login +
+ Already have an account? + + Sign in
diff --git a/public/src/components/common/ConfirmContainer.tsx b/public/src/components/common/ConfirmContainer.tsx new file mode 100644 index 0000000..6598738 --- /dev/null +++ b/public/src/components/common/ConfirmContainer.tsx @@ -0,0 +1,20 @@ +import { useUIStore } from '../../stores/uiStore'; +import { ConfirmDialog } from './ConfirmDialog'; + +export function ConfirmContainer() { + const { confirm } = useUIStore(); + + if (!confirm) return null; + + return ( + {})} + /> + ); +} diff --git a/public/src/components/common/ConfirmDialog.tsx b/public/src/components/common/ConfirmDialog.tsx new file mode 100644 index 0000000..4eef569 --- /dev/null +++ b/public/src/components/common/ConfirmDialog.tsx @@ -0,0 +1,54 @@ +interface ConfirmDialogProps { + title: string; + message: string; + confirmText?: string; + cancelText?: string; + onConfirm: () => void; + onCancel: () => void; + type?: 'danger' | 'warning' | 'info'; +} + +export function ConfirmDialog({ + title, + message, + confirmText = 'Confirm', + cancelText = 'Cancel', + onConfirm, + onCancel, + type = 'warning', +}: ConfirmDialogProps) { + const confirmColors = { + danger: 'bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800', + warning: 'bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-700 dark:hover:bg-yellow-800', + info: 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800', + }; + + return ( +
+
+
+

+ {title} +

+

+ {message} +

+
+ + +
+
+
+
+ ); +} diff --git a/public/src/components/common/Toast.tsx b/public/src/components/common/Toast.tsx new file mode 100644 index 0000000..1fceaf2 --- /dev/null +++ b/public/src/components/common/Toast.tsx @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; +import type { ToastType } from '../../stores/uiStore'; + +interface ToastProps { + message: string; + type: ToastType; + onClose: () => void; + duration?: number; +} + +export function Toast({ message, type, onClose, duration = 4000 }: ToastProps) { + useEffect(() => { + const timer = setTimeout(onClose, duration); + return () => clearTimeout(timer); + }, [duration, onClose]); + + const bgColors = { + success: 'bg-green-500 dark:bg-green-600', + error: 'bg-red-500 dark:bg-red-600', + info: 'bg-blue-500 dark:bg-blue-600', + warning: 'bg-yellow-500 dark:bg-yellow-600', + }; + + const icons = { + success: '✓', + error: '✕', + info: 'ℹ', + warning: '⚠', + }; + + return ( +
+
{icons[type]}
+
{message}
+ +
+ ); +} diff --git a/public/src/components/common/ToastContainer.tsx b/public/src/components/common/ToastContainer.tsx new file mode 100644 index 0000000..2265683 --- /dev/null +++ b/public/src/components/common/ToastContainer.tsx @@ -0,0 +1,19 @@ +import { useUIStore } from '../../stores/uiStore'; +import { Toast } from './Toast'; + +export function ToastContainer() { + const { toasts, removeToast } = useUIStore(); + + return ( +
+ {toasts.map((toast) => ( + removeToast(toast.id)} + /> + ))} +
+ ); +} diff --git a/public/src/components/layout/Layout.tsx b/public/src/components/layout/Layout.tsx index 485849b..10a5982 100644 --- a/public/src/components/layout/Layout.tsx +++ b/public/src/components/layout/Layout.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuthStore } from '../../stores/authStore'; +import { useUIStore } from '../../stores/uiStore'; interface LayoutProps { children: ReactNode; @@ -8,6 +9,7 @@ interface LayoutProps { export function Layout({ children }: LayoutProps) { const { user, logout } = useAuthStore(); + const { darkMode, toggleDarkMode, showToast } = useUIStore(); const navigate = useNavigate(); const handleLogout = () => { @@ -15,35 +17,49 @@ export function Layout({ children }: LayoutProps) { navigate('/login'); }; + const handleCopyId = () => { + if (user) { + navigator.clipboard.writeText(user.id); + showToast('User ID copied to clipboard!', 'success'); + } + }; + return ( -
-
+
+

ISP Wiremap

{user && ( -
+
{user.username} {user.is_admin && ( - + Admin )} + + + + diff --git a/public/src/components/map/ItemContextMenu.tsx b/public/src/components/map/ItemContextMenu.tsx index 5ff837c..a7ea198 100644 --- a/public/src/components/map/ItemContextMenu.tsx +++ b/public/src/components/map/ItemContextMenu.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import { mapItemService } from '../../services/mapItemService'; +import { useUIStore } from '../../stores/uiStore'; interface ItemContextMenuProps { item: any; @@ -9,15 +10,16 @@ interface ItemContextMenuProps { } export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemContextMenuProps) { + const { showToast } = useUIStore(); const [showRenameDialog, setShowRenameDialog] = useState(false); const [showNotesDialog, setShowNotesDialog] = useState(false); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showPortConfigDialog, setShowPortConfigDialog] = useState(false); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [deleteConnectedCables, setDeleteConnectedCables] = useState(false); const [newName, setNewName] = useState(item.properties.name || ''); const [notes, setNotes] = useState(item.properties.notes || ''); const [imageData, setImageData] = useState(item.properties.image || null); const [portCount, setPortCount] = useState(item.properties.port_count || 5); - const [deleteConnectedCables, setDeleteConnectedCables] = useState(false); const menuRef = useRef(null); const fileInputRef = useRef(null); @@ -72,11 +74,12 @@ export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemConte onUpdate(); onClose(); + showToast('Item deleted successfully', 'success'); console.log('Delete process completed successfully'); } catch (error) { console.error('Failed to delete item:', error); console.error('Error details:', { error, item_id: item.id, map_id: item.map_id }); - alert('Failed to delete item'); + showToast('Failed to delete item', 'error'); } }; @@ -91,9 +94,10 @@ export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemConte onUpdate(); setShowRenameDialog(false); onClose(); + showToast('Item renamed successfully', 'success'); } catch (error) { console.error('Failed to rename item:', error); - alert('Failed to rename item'); + showToast('Failed to rename item', 'error'); } }; @@ -103,13 +107,13 @@ export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemConte // Check file size (max 2MB) if (file.size > 2 * 1024 * 1024) { - alert('Image too large. Please use an image smaller than 2MB.'); + showToast('Image too large. Please use an image smaller than 2MB.', 'error'); return; } // Check file type if (!file.type.startsWith('image/')) { - alert('Please select an image file.'); + showToast('Please select an image file.', 'error'); return; } @@ -139,9 +143,10 @@ export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemConte onUpdate(); setShowNotesDialog(false); onClose(); + showToast('Notes saved successfully', 'success'); } catch (error) { console.error('Failed to save notes:', error); - alert('Failed to save notes'); + showToast('Failed to save notes', 'error'); } }; @@ -156,9 +161,10 @@ export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemConte onUpdate(); setShowPortConfigDialog(false); onClose(); + showToast('Port configuration saved', 'success'); } catch (error) { console.error('Failed to save port configuration:', error); - alert('Failed to save port configuration'); + showToast('Failed to save port configuration', 'error'); } }; @@ -166,11 +172,11 @@ export function ItemContextMenu({ item, position, onClose, onUpdate }: ItemConte return (
-

Configure Ports

-