diff --git a/client/src/App.jsx b/client/src/App.jsx index 16ce59d..72744b4 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,6 +1,7 @@ import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom'; import Navbar from './components/Navbar'; import Footer from './components/Footer'; +import Toast from './components/Toast'; import LocationBanner from './components/LocationBanner'; import Home from './pages/Home'; import Login from './pages/Login'; @@ -57,6 +58,25 @@ function AppContent() { function App() { return ( +
+ + + +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* TODO: Add more routes here */} + +
+
+
); diff --git a/client/src/components/LocationBanner.jsx b/client/src/components/LocationBanner.jsx index 977cf26..2efa9af 100644 --- a/client/src/components/LocationBanner.jsx +++ b/client/src/components/LocationBanner.jsx @@ -1,5 +1,6 @@ import { useLocation } from '../context/LocationContext'; import { useState } from 'react'; +import useToast from '../hooks/useToast'; /** * LocationBanner @@ -12,11 +13,16 @@ import { useState } from 'react'; const LocationBanner = () => { const { coords, loading, error, permissionDenied, retry } = useLocation(); const [retryLoading, setRetryLoading] = useState(false); + const { showToast } = useToast(); const handleRetry = async () => { setRetryLoading(true); try { await retry(); + showToast('Location detected successfully!', 'success'); + } catch (error) { + console.error('Retry failed:', error); + showToast('Failed to detect location. Please try again.', 'error'); } catch (error) { console.error('Retry failed:', error); } finally { diff --git a/client/src/components/Toast.jsx b/client/src/components/Toast.jsx new file mode 100644 index 0000000..e92c8c3 --- /dev/null +++ b/client/src/components/Toast.jsx @@ -0,0 +1,20 @@ +import useToast from '../hooks/useToast'; + +const Toast = () => { + const { toasts } = useToast(); + + return ( +
+ {toasts.map(toast => ( +
+ {toast.message} +
+ ))} +
+ ); +}; + +export default Toast; diff --git a/client/src/context/ToastContext.jsx b/client/src/context/ToastContext.jsx new file mode 100644 index 0000000..5634443 --- /dev/null +++ b/client/src/context/ToastContext.jsx @@ -0,0 +1,33 @@ +import { createContext, useState, useCallback } from 'react'; + +const ToastContext = createContext(null); + +export const ToastProvider = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const showToast = useCallback((message, type = 'success') => { + const id = Math.random().toString(36).substr(2, 9); + const toast = { id, message, type }; + + setToasts(prev => [...prev, toast]); + + // Auto-hide after 2.5 seconds + setTimeout(() => { + setToasts(prev => prev.filter(t => t.id !== id)); + }, 2500); + + return id; + }, []); + + const removeToast = useCallback((id) => { + setToasts(prev => prev.filter(t => t.id !== id)); + }, []); + + return ( + + {children} + + ); +}; + +export default ToastContext; diff --git a/client/src/hooks/useToast.js b/client/src/hooks/useToast.js new file mode 100644 index 0000000..3e88a53 --- /dev/null +++ b/client/src/hooks/useToast.js @@ -0,0 +1,12 @@ +import { useContext } from 'react'; +import ToastContext from '../context/ToastContext'; + +const useToast = () => { + const ctx = useContext(ToastContext); + if (!ctx) { + throw new Error('useToast must be used inside '); + } + return ctx; +}; + +export default useToast; diff --git a/client/src/index.css b/client/src/index.css index d9a5583..3f42b9e 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -31,6 +31,50 @@ animation: spin 1.5s linear infinite; } +/* Toast notifications */ +@keyframes slide-in { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slide-out { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(400px); + opacity: 0; + } +} + +.animate-slide-in { + animation: slide-in 0.3s ease-out forwards; +} + +.toast { + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; +} + +.toast-success { + background-color: #10b981; + border-left: 4px solid #059669; +} + +.toast-error { + background-color: #ef4444; + border-left: 4px solid #dc2626; +} + /* Mobile responsiveness */ @media (max-width: 640px) { .grid-cols-1 { diff --git a/client/src/main.jsx b/client/src/main.jsx index 86a24e8..0f7efed 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -2,11 +2,17 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' import { LocationProvider } from './context/LocationContext.jsx' +import { ToastProvider } from './context/ToastContext.jsx' import { AuthProvider } from './context/AuthContext.jsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( + + + + + diff --git a/client/src/pages/Bookings.jsx b/client/src/pages/Bookings.jsx index efb3dc5..dba0a2e 100644 --- a/client/src/pages/Bookings.jsx +++ b/client/src/pages/Bookings.jsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; +import useToast from "../hooks/useToast"; import EmptyState from "../components/EmptyState"; const Bookings = () => { @@ -9,6 +10,7 @@ const Bookings = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [actionLoading, setActionLoading] = useState({}); // For individual button loading + const { showToast } = useToast(); import { useState } from "react"; const Bookings = () => { @@ -116,6 +118,10 @@ const StarRating = ({ rating, onRatingChange, size = "md" }) => { b.id === id ? { ...b, status: "Cancelled" } : b ); setBookings(updated); + showToast('Booking cancelled successfully.', 'success'); + } catch (error) { + console.error('Cancel failed:', error); + showToast('Failed to cancel booking. Please try again.', 'error'); } catch (error) { console.error('Cancel failed:', error); } finally { @@ -138,6 +144,10 @@ const StarRating = ({ rating, onRatingChange, size = "md" }) => { setActiveReview(null); setRating(0); setComment(""); + showToast('Review submitted successfully!', 'success'); + } catch (error) { + console.error('Review submit failed:', error); + showToast('Failed to submit review. Please try again.', 'error'); } catch (error) { console.error('Review submit failed:', error); } finally { diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx index 3fe67d5..9cf2e3c 100644 --- a/client/src/pages/Login.jsx +++ b/client/src/pages/Login.jsx @@ -1,4 +1,9 @@ import { useState } from "react"; +import useToast from "../hooks/useToast"; + +const Login = () => { + const [loading, setLoading] = useState(false); + const { showToast } = useToast(); const Login = () => { const [loading, setLoading] = useState(false); @@ -9,6 +14,10 @@ const Login = () => { try { // TODO: Add authentication logic and API connection await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call + showToast('Login successful! Welcome back.', 'success'); + } catch (error) { + console.error('Login failed:', error); + showToast('Login failed. Please try again.', 'error'); } catch (error) { console.error('Login failed:', error); import { useState } from 'react'; diff --git a/client/src/pages/Profile.jsx b/client/src/pages/Profile.jsx index ef41fb4..336a761 100644 --- a/client/src/pages/Profile.jsx +++ b/client/src/pages/Profile.jsx @@ -1,4 +1,9 @@ import { useState } from "react"; +import useToast from "../hooks/useToast"; + +const Profile = () => { + const [loading, setLoading] = useState(false); + const { showToast } = useToast(); const Profile = () => { const [loading, setLoading] = useState(false); @@ -8,6 +13,10 @@ const Profile = () => { try { // TODO: Handle API update logic await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call + showToast('Profile updated successfully!', 'success'); + } catch (error) { + console.error('Save failed:', error); + showToast('Failed to save changes. Please try again.', 'error'); } catch (error) { console.error('Save failed:', error); } finally { diff --git a/client/src/pages/Register.jsx b/client/src/pages/Register.jsx index e07c87f..842c6ee 100644 --- a/client/src/pages/Register.jsx +++ b/client/src/pages/Register.jsx @@ -1,4 +1,20 @@ import { useState } from "react"; +import useToast from "../hooks/useToast"; + +const Register = () => { + const [loading, setLoading] = useState(false); + const { showToast } = useToast(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + try { + // TODO: Add registration logic and API connection + await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call + showToast('Account created successfully!', 'success'); + } catch (error) { + console.error('Registration failed:', error); + showToast('Registration failed. Please try again.', 'error'); import { useNavigate, Link } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; import axios from "axios"; @@ -179,6 +195,9 @@ const Register = () => { Join FixNearby and get started

+ {/* TODO: Add authentication logic and API connection */} +
+
{apiError && ( @@ -285,6 +304,12 @@ const Register = () => { )}
+
+ +
{/* Submit Button */}