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
20 changes: 20 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -54,6 +55,25 @@ function AppContent() {
function App() {
return (
<Router>
<div className="flex flex-col min-h-screen">
<Navbar />
<LocationBanner />
<Toast />
<main className="flex-grow bg-gray-50">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/services" element={<Services />} />
<Route path="/worker/:id" element={<WorkerProfile />} />
<Route path="/profile" element={<Profile />} />
<Route path="/bookings" element={<Bookings />} />
{/* TODO: Add more routes here */}
</Routes>
</main>
<Footer />
</div>
<AppContent />
</Router>
);
Expand Down
33 changes: 27 additions & 6 deletions client/src/components/LocationBanner.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useLocation } from '../context/LocationContext';
import { useState } from 'react';
import useToast from '../hooks/useToast';

/**
* LocationBanner
Expand All @@ -10,6 +12,21 @@ import { useLocation } from '../context/LocationContext';
*/
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');
} finally {
setRetryLoading(false);
}
};

if (loading) {
return (
Expand All @@ -25,10 +42,12 @@ const LocationBanner = () => {
<div className="bg-amber-50 border-b border-amber-100 py-2 px-4 flex items-center justify-center gap-3 text-sm text-amber-800 font-medium flex-wrap">
<span>⚠️ Location access denied. Enable it in browser settings to see nearby services.</span>
<button
onClick={retry}
className="underline underline-offset-2 hover:text-amber-600 transition-colors font-semibold"
onClick={handleRetry}
disabled={retryLoading}
className="underline underline-offset-2 hover:text-amber-600 transition-colors font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
>
Retry
<span className={`btn-text ${retryLoading ? 'hidden' : ''}`}>Retry</span>
<span className={`btn-loader ${retryLoading ? '' : 'hidden'}`}>Loading...</span>
</button>
</div>
);
Expand All @@ -39,10 +58,12 @@ const LocationBanner = () => {
<div className="bg-red-50 border-b border-red-100 py-2 px-4 flex items-center justify-center gap-3 text-sm text-red-700 font-medium flex-wrap">
<span>πŸ“ {error}</span>
<button
onClick={retry}
className="underline underline-offset-2 hover:text-red-500 transition-colors font-semibold"
onClick={handleRetry}
disabled={retryLoading}
className="underline underline-offset-2 hover:text-red-500 transition-colors font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
>
Retry
<span className={`btn-text ${retryLoading ? 'hidden' : ''}`}>Retry</span>
<span className={`btn-loader ${retryLoading ? '' : 'hidden'}`}>Loading...</span>
</button>
</div>
);
Expand Down
20 changes: 20 additions & 0 deletions client/src/components/Toast.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import useToast from '../hooks/useToast';

const Toast = () => {
const { toasts } = useToast();

return (
<div className="fixed top-4 right-4 z-50 space-y-2">
{toasts.map(toast => (
<div
key={toast.id}
className={`toast toast-${toast.type} px-4 py-3 rounded-lg shadow-lg text-white max-w-sm animate-slide-in`}
>
{toast.message}
</div>
))}
</div>
);
};

export default Toast;
33 changes: 33 additions & 0 deletions client/src/context/ToastContext.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<ToastContext.Provider value={{ toasts, showToast, removeToast }}>
{children}
</ToastContext.Provider>
);
};

export default ToastContext;
12 changes: 12 additions & 0 deletions client/src/hooks/useToast.js
Original file line number Diff line number Diff line change
@@ -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 <ToastProvider>');
}
return ctx;
};

export default useToast;
44 changes: 44 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions client/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
<LocationProvider>
<ToastProvider>
<App />
</ToastProvider>
</LocationProvider>
<AuthProvider>
<LocationProvider>
<App />
Expand Down
70 changes: 70 additions & 0 deletions client/src/pages/Bookings.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import useToast from "../hooks/useToast";

const Bookings = () => {
const [bookings, setBookings] = useState([]);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("All");
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 StarRating = ({ rating, onRatingChange, size = "md" }) => {
Expand Down Expand Up @@ -91,6 +101,46 @@ const StarRating = ({ rating, onRatingChange, size = "md" }) => {
setLoading(false);
}, []);

const handleCancel = async (id) => {
setActionLoading(prev => ({ ...prev, [id]: true }));
try {
// TODO: API call to cancel booking
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
const updated = bookings.map((b) =>
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');
} finally {
setActionLoading(prev => ({ ...prev, [id]: false }));
}
};

const handleReviewSubmit = async (id) => {
if (rating === 0) return alert("Please select a rating");

setActionLoading(prev => ({ ...prev, [`review-${id}`]: true }));
try {
// TODO: API call to submit review
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
const updated = bookings.map((b) =>
b.id === id ? { ...b, review: { rating, comment } } : b
);

setBookings(updated);
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');
} finally {
setActionLoading(prev => ({ ...prev, [`review-${id}`]: false }));
}
// ---------------- SAVE BOOKINGS ----------------

useEffect(() => {
Expand Down Expand Up @@ -452,6 +502,16 @@ const StarRating = ({ rating, onRatingChange, size = "md" }) => {

</div>

{/* Actions */}
<div className="mt-3 flex gap-4">
{booking.status === "Pending" && (
<button
onClick={() => handleCancel(booking.id)}
disabled={actionLoading[booking.id]}
className="text-red-600 hover:underline text-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className={`btn-text ${actionLoading[booking.id] ? 'hidden' : ''}`}>Cancel</span>
<span className={`btn-loader ${actionLoading[booking.id] ? '' : 'hidden'}`}>Loading...</span>
<div className="bg-slate-50 rounded-2xl p-4">

<p className="text-xs text-slate-500">
Expand Down Expand Up @@ -518,6 +578,16 @@ const StarRating = ({ rating, onRatingChange, size = "md" }) => {

{/* REVIEW BOX */}

{/* Buttons */}
<div className="flex gap-3">
<button
onClick={() => handleReviewSubmit(booking.id)}
disabled={actionLoading[`review-${booking.id}`]}
className="bg-blue-600 text-white px-3 py-1 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className={`btn-text ${actionLoading[`review-${booking.id}`] ? 'hidden' : ''}`}>Submit</span>
<span className={`btn-loader ${actionLoading[`review-${booking.id}`] ? '' : 'hidden'}`}>Loading...</span>
</button>
{activeReview === b.id && (
<div className="mt-6 bg-slate-50 border border-slate-200 rounded-3xl p-5">

Expand Down
36 changes: 36 additions & 0 deletions client/src/pages/Login.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
import { useState } from "react";
import useToast from "../hooks/useToast";

const Login = () => {
const [loading, setLoading] = useState(false);
const { showToast } = useToast();

const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
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');
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
Expand Down Expand Up @@ -107,6 +124,25 @@ const Login = () => {

return (
<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 bg-white p-8 rounded-lg shadow">
<div>
<h2 className="text-center text-3xl font-extrabold text-gray-900">Sign in</h2>
</div>
{/* TODO: Add authentication logic and API connection */}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<input id="email-address" name="email" type="email" required className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" placeholder="Email address" />
</div>
<div>
<input id="password" name="password" type="password" required className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" placeholder="Password" />
</div>
</div>
<div>
<button type="submit" disabled={loading} className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
<span className={`btn-text ${loading ? 'hidden' : ''}`}>Sign in</span>
<span className={`btn-loader ${loading ? '' : 'hidden'}`}>Loading...</span>
</button>
<div className="max-w-md w-full space-y-8 bg-white p-8 rounded-2xl shadow-md">
<h2 className="text-center text-3xl font-bold text-gray-900">
Sign In
Expand Down
Loading