diff --git a/app/edit/[id]/page.tsx b/app/edit/[id]/page.tsx index 570c72c..f967fa4 100644 --- a/app/edit/[id]/page.tsx +++ b/app/edit/[id]/page.tsx @@ -4,6 +4,8 @@ import { useState, useEffect } from 'react'; import { useParams, useRouter } from 'next/navigation'; import Dropdown from '@/components/Dropdown'; import { Team } from '@/types'; +import { useToast } from '@/lib/useToast'; +import { ToastContainer } from '@/components/Toast'; type Stream = { id: number; @@ -31,10 +33,11 @@ export default function EditStream() { }); const [teams, setTeams] = useState([]); - const [message, setMessage] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [isLoading, setIsLoading] = useState(true); const [stream, setStream] = useState(null); + const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); + const { toasts, removeToast, showSuccess, showError } = useToast(); // Fetch stream data and teams useEffect(() => { @@ -71,7 +74,7 @@ export default function EditStream() { ); } catch (error) { console.error('Failed to fetch data:', error); - setMessage('Failed to load stream data'); + showError('Failed to Load Stream', 'Could not fetch stream data. Please refresh the page.'); } finally { setIsLoading(false); } @@ -85,15 +88,57 @@ export default function EditStream() { const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); + + // Clear validation error when user starts typing + if (validationErrors[name]) { + setValidationErrors(prev => ({ ...prev, [name]: '' })); + } }; const handleTeamSelect = (teamId: number) => { setFormData((prev) => ({ ...prev, team_id: teamId })); + + // Clear validation error when user selects team + if (validationErrors.team_id) { + setValidationErrors(prev => ({ ...prev, team_id: '' })); + } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setMessage(''); + + // Client-side validation + const errors: {[key: string]: string} = {}; + if (!formData.name.trim()) { + errors.name = 'Stream name is required'; + } else if (formData.name.trim().length < 2) { + errors.name = 'Stream name must be at least 2 characters'; + } + + if (!formData.obs_source_name.trim()) { + errors.obs_source_name = 'OBS source name is required'; + } + + if (!formData.url.trim()) { + errors.url = 'Stream URL is required'; + } else { + try { + new URL(formData.url); + } catch { + errors.url = 'Please enter a valid URL'; + } + } + + if (!formData.team_id) { + errors.team_id = 'Please select a team'; + } + + setValidationErrors(errors); + if (Object.keys(errors).length > 0) { + showError('Validation Error', 'Please fix the form errors'); + return; + } + setIsSubmitting(true); try { @@ -105,17 +150,17 @@ export default function EditStream() { const data = await response.json(); if (response.ok) { - setMessage('Stream updated successfully!'); + showSuccess('Stream Updated', `"${formData.name}" has been updated successfully`); // Redirect back to home after a short delay setTimeout(() => { router.push('/'); }, 1500); } else { - setMessage(data.error || 'Something went wrong.'); + showError('Failed to Update Stream', data.error || 'Unknown error occurred'); } } catch (error) { console.error('Error updating stream:', error); - setMessage('Failed to update stream.'); + showError('Failed to Update Stream', 'Network error or server unavailable'); } finally { setIsSubmitting(false); } @@ -133,17 +178,17 @@ export default function EditStream() { const data = await response.json(); if (response.ok) { - setMessage('Stream deleted successfully!'); + showSuccess('Stream Deleted', `"${stream?.name || 'Stream'}" has been deleted successfully`); // Redirect back to home after a short delay setTimeout(() => { router.push('/'); }, 1500); } else { - setMessage(data.error || 'Failed to delete stream.'); + showError('Failed to Delete Stream', data.error || 'Unknown error occurred'); } } catch (error) { console.error('Error deleting stream:', error); - setMessage('Failed to delete stream.'); + showError('Failed to Delete Stream', 'Network error or server unavailable'); } }; @@ -281,33 +326,11 @@ export default function EditStream() { - {/* Success/Error Message */} - {message && ( -
-
-
- {message.includes('successfully') ? ( - - - - ) : ( - - - - )} -
- {message} -
-
- )} + + {/* Toast Notifications */} + ); } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 8c0f60b..ccf0dcc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,6 +3,8 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import Dropdown from '@/components/Dropdown'; +import { useToast } from '@/lib/useToast'; +import { ToastContainer } from '@/components/Toast'; type Stream = { id: number; @@ -26,6 +28,7 @@ export default function Home() { }); const [isLoading, setIsLoading] = useState(true); const [openDropdown, setOpenDropdown] = useState(null); + const { toasts, removeToast, showSuccess, showError } = useToast(); useEffect(() => { const fetchData = async () => { @@ -45,6 +48,7 @@ export default function Home() { setActiveSources(activeData); } catch (error) { console.error('Error fetching data:', error); + showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.'); } finally { setIsLoading(false); } @@ -74,8 +78,11 @@ export default function Home() { if (!response.ok) { throw new Error('Failed to set active stream'); } + + showSuccess('Source Updated', `Set ${selectedStream?.name || 'stream'} as active for ${screen}`); } catch (error) { console.error('Error setting active stream:', error); + showError('Failed to Update Source', 'Could not set active stream. Please try again.'); // Revert local state on error setActiveSources((prev) => ({ ...prev, @@ -210,6 +217,9 @@ export default function Home() { )} + + {/* Toast Notifications */} + ); } \ No newline at end of file diff --git a/app/streams/page.tsx b/app/streams/page.tsx index a06b03f..24ec835 100644 --- a/app/streams/page.tsx +++ b/app/streams/page.tsx @@ -3,6 +3,8 @@ import { useState, useEffect } from 'react'; import Dropdown from '@/components/Dropdown'; import { Team } from '@/types'; +import { useToast } from '@/lib/useToast'; +import { ToastContainer } from '@/components/Toast'; interface Stream { id: number; @@ -22,8 +24,9 @@ export default function AddStream() { const [teams, setTeams] = useState<{id: number; name: string}[]>([]); const [streams, setStreams] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [message, setMessage] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); + const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); + const { toasts, removeToast, showSuccess, showError } = useToast(); // Fetch teams and streams on component mount useEffect(() => { @@ -52,6 +55,7 @@ export default function AddStream() { setStreams(streamsData); } catch (error) { console.error('Failed to fetch data:', error); + showError('Failed to Load Data', 'Could not fetch teams and streams. Please refresh the page.'); } finally { setIsLoading(false); } @@ -60,16 +64,58 @@ export default function AddStream() { const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); + + // Clear validation error when user starts typing + if (validationErrors[name]) { + setValidationErrors(prev => ({ ...prev, [name]: '' })); + } }; const handleTeamSelect = (teamId: number) => { // @ts-expect-error - team_id can be null or number in formData, but TypeScript expects only number setFormData((prev) => ({ ...prev, team_id: teamId })); + + // Clear validation error when user selects team + if (validationErrors.team_id) { + setValidationErrors(prev => ({ ...prev, team_id: '' })); + } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setMessage(''); + + // Client-side validation + const errors: {[key: string]: string} = {}; + if (!formData.name.trim()) { + errors.name = 'Stream name is required'; + } else if (formData.name.trim().length < 2) { + errors.name = 'Stream name must be at least 2 characters'; + } + + if (!formData.obs_source_name.trim()) { + errors.obs_source_name = 'OBS source name is required'; + } + + if (!formData.url.trim()) { + errors.url = 'Stream URL is required'; + } else { + try { + new URL(formData.url); + } catch { + errors.url = 'Please enter a valid URL'; + } + } + + if (!formData.team_id) { + errors.team_id = 'Please select a team'; + } + + setValidationErrors(errors); + if (Object.keys(errors).length > 0) { + showError('Validation Error', 'Please fix the form errors'); + return; + } + setIsSubmitting(true); try { @@ -81,15 +127,16 @@ export default function AddStream() { const data = await response.json(); if (response.ok) { - setMessage(data.message); - setFormData({ name: '', obs_source_name: '', url: '', team_id: null }); // Reset form - fetchData(); // Refresh the streams list + showSuccess('Stream Added', `"${formData.name}" has been added successfully`); + setFormData({ name: '', obs_source_name: '', url: '', team_id: null }); + setValidationErrors({}); + fetchData(); } else { - setMessage(data.error || 'Something went wrong.'); + showError('Failed to Add Stream', data.error || 'Unknown error occurred'); } } catch (error) { console.error('Error adding stream:', error); - setMessage('Failed to add stream.'); + showError('Failed to Add Stream', 'Network error or server unavailable'); } finally { setIsSubmitting(false); } @@ -120,9 +167,16 @@ export default function AddStream() { value={formData.name} onChange={handleInputChange} required - className="input" + className={`input ${ + validationErrors.name ? 'border-red-500/60 bg-red-500/10' : '' + }`} placeholder="Enter a display name for the stream" /> + {validationErrors.name && ( +
+ {validationErrors.name} +
+ )} {/* OBS Source Name */} @@ -136,9 +190,16 @@ export default function AddStream() { value={formData.obs_source_name} onChange={handleInputChange} required - className="input" + className={`input ${ + validationErrors.obs_source_name ? 'border-red-500/60 bg-red-500/10' : '' + }`} placeholder="Enter the exact source name from OBS" /> + {validationErrors.obs_source_name && ( +
+ {validationErrors.obs_source_name} +
+ )} {/* URL */} @@ -152,9 +213,16 @@ export default function AddStream() { value={formData.url} onChange={handleInputChange} required - className="input" + className={`input ${ + validationErrors.url ? 'border-red-500/60 bg-red-500/10' : '' + }`} placeholder="https://example.com/stream" /> + {validationErrors.url && ( +
+ {validationErrors.url} +
+ )} {/* Team Selection and Submit Button */} @@ -170,6 +238,11 @@ export default function AddStream() { onSelect={handleTeamSelect} label="Select a Team" /> + {validationErrors.team_id && ( +
+ {validationErrors.team_id} +
+ )}