Skip to content

Commit 611b405

Browse files
authored
Merge pull request #31 from prisha-sh/feat-loading-states
feat: add loading states across app
2 parents 1f9e075 + 51677c3 commit 611b405

6 files changed

Lines changed: 183 additions & 9 deletions

File tree

client/src/components/LocationBanner.jsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useLocation } from '../context/LocationContext';
2+
import { useState } from 'react';
23

34
/**
45
* LocationBanner
@@ -10,6 +11,18 @@ import { useLocation } from '../context/LocationContext';
1011
*/
1112
const LocationBanner = () => {
1213
const { coords, loading, error, permissionDenied, retry } = useLocation();
14+
const [retryLoading, setRetryLoading] = useState(false);
15+
16+
const handleRetry = async () => {
17+
setRetryLoading(true);
18+
try {
19+
await retry();
20+
} catch (error) {
21+
console.error('Retry failed:', error);
22+
} finally {
23+
setRetryLoading(false);
24+
}
25+
};
1326

1427
if (loading) {
1528
return (
@@ -25,10 +38,12 @@ const LocationBanner = () => {
2538
<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">
2639
<span>⚠️ Location access denied. Enable it in browser settings to see nearby services.</span>
2740
<button
28-
onClick={retry}
29-
className="underline underline-offset-2 hover:text-amber-600 transition-colors font-semibold"
41+
onClick={handleRetry}
42+
disabled={retryLoading}
43+
className="underline underline-offset-2 hover:text-amber-600 transition-colors font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
3044
>
31-
Retry
45+
<span className={`btn-text ${retryLoading ? 'hidden' : ''}`}>Retry</span>
46+
<span className={`btn-loader ${retryLoading ? '' : 'hidden'}`}>Loading...</span>
3247
</button>
3348
</div>
3449
);
@@ -39,10 +54,12 @@ const LocationBanner = () => {
3954
<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">
4055
<span>📍 {error}</span>
4156
<button
42-
onClick={retry}
43-
className="underline underline-offset-2 hover:text-red-500 transition-colors font-semibold"
57+
onClick={handleRetry}
58+
disabled={retryLoading}
59+
className="underline underline-offset-2 hover:text-red-500 transition-colors font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
4460
>
45-
Retry
61+
<span className={`btn-text ${retryLoading ? 'hidden' : ''}`}>Retry</span>
62+
<span className={`btn-loader ${retryLoading ? '' : 'hidden'}`}>Loading...</span>
4663
</button>
4764
</div>
4865
);

client/src/pages/Bookings.jsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import { useEffect, useMemo, useState } from "react";
22
import { Link } from "react-router-dom";
33
import { useState } from "react";
44

5+
const Bookings = () => {
6+
const [bookings, setBookings] = useState([]);
7+
const [search, setSearch] = useState("");
8+
const [statusFilter, setStatusFilter] = useState("All");
9+
const [loading, setLoading] = useState(true);
10+
const [error, setError] = useState(null);
11+
const [actionLoading, setActionLoading] = useState({}); // For individual button loading
512
const StarRating = ({ rating, onRatingChange, size = "md" }) => {
613
const [hoverRating, setHoverRating] = useState(0);
714

@@ -91,6 +98,42 @@ const StarRating = ({ rating, onRatingChange, size = "md" }) => {
9198
setLoading(false);
9299
}, []);
93100

101+
const handleCancel = async (id) => {
102+
setActionLoading(prev => ({ ...prev, [id]: true }));
103+
try {
104+
// TODO: API call to cancel booking
105+
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
106+
const updated = bookings.map((b) =>
107+
b.id === id ? { ...b, status: "Cancelled" } : b
108+
);
109+
setBookings(updated);
110+
} catch (error) {
111+
console.error('Cancel failed:', error);
112+
} finally {
113+
setActionLoading(prev => ({ ...prev, [id]: false }));
114+
}
115+
};
116+
117+
const handleReviewSubmit = async (id) => {
118+
if (rating === 0) return alert("Please select a rating");
119+
120+
setActionLoading(prev => ({ ...prev, [`review-${id}`]: true }));
121+
try {
122+
// TODO: API call to submit review
123+
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
124+
const updated = bookings.map((b) =>
125+
b.id === id ? { ...b, review: { rating, comment } } : b
126+
);
127+
128+
setBookings(updated);
129+
setActiveReview(null);
130+
setRating(0);
131+
setComment("");
132+
} catch (error) {
133+
console.error('Review submit failed:', error);
134+
} finally {
135+
setActionLoading(prev => ({ ...prev, [`review-${id}`]: false }));
136+
}
94137
// ---------------- SAVE BOOKINGS ----------------
95138

96139
useEffect(() => {
@@ -452,6 +495,16 @@ const StarRating = ({ rating, onRatingChange, size = "md" }) => {
452495

453496
</div>
454497

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

457510
<p className="text-xs text-slate-500">
@@ -518,6 +571,16 @@ const StarRating = ({ rating, onRatingChange, size = "md" }) => {
518571

519572
{/* REVIEW BOX */}
520573

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

client/src/pages/Login.jsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
import { useState } from "react";
2+
3+
const Login = () => {
4+
const [loading, setLoading] = useState(false);
5+
6+
const handleSubmit = async (e) => {
7+
e.preventDefault();
8+
setLoading(true);
9+
try {
10+
// TODO: Add authentication logic and API connection
11+
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
12+
} catch (error) {
13+
console.error('Login failed:', error);
114
import { useState } from 'react';
215
import { useNavigate, Link } from 'react-router-dom';
316
import { useAuth } from '../context/AuthContext';
@@ -107,6 +120,25 @@ const Login = () => {
107120

108121
return (
109122
<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
123+
<div className="max-w-md w-full space-y-8 bg-white p-8 rounded-lg shadow">
124+
<div>
125+
<h2 className="text-center text-3xl font-extrabold text-gray-900">Sign in</h2>
126+
</div>
127+
{/* TODO: Add authentication logic and API connection */}
128+
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
129+
<div className="rounded-md shadow-sm -space-y-px">
130+
<div>
131+
<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" />
132+
</div>
133+
<div>
134+
<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" />
135+
</div>
136+
</div>
137+
<div>
138+
<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">
139+
<span className={`btn-text ${loading ? 'hidden' : ''}`}>Sign in</span>
140+
<span className={`btn-loader ${loading ? '' : 'hidden'}`}>Loading...</span>
141+
</button>
110142
<div className="max-w-md w-full space-y-8 bg-white p-8 rounded-2xl shadow-md">
111143
<h2 className="text-center text-3xl font-bold text-gray-900">
112144
Sign In

client/src/pages/Profile.jsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
1+
import { useState } from "react";
2+
13
const Profile = () => {
4+
const [loading, setLoading] = useState(false);
5+
6+
const handleSave = async () => {
7+
setLoading(true);
8+
try {
9+
// TODO: Handle API update logic
10+
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
11+
} catch (error) {
12+
console.error('Save failed:', error);
13+
} finally {
14+
setLoading(false);
15+
}
16+
};
17+
218
// TODO: (50% built) Load authenticated user's data from global state or API.
319
// Example: const { user } = useAuth();
420

@@ -26,8 +42,9 @@ const Profile = () => {
2642
</div>
2743

2844
<div className="pt-4 border-t border-gray-200">
29-
<button type="button" onClick={() => alert('TODO: Handle API update logic')} className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded shadow">
30-
Save Changes
45+
<button type="button" onClick={handleSave} disabled={loading} className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded shadow disabled:opacity-50 disabled:cursor-not-allowed">
46+
<span className={`btn-text ${loading ? 'hidden' : ''}`}>Save Changes</span>
47+
<span className={`btn-loader ${loading ? '' : 'hidden'}`}>Loading...</span>
3148
</button>
3249
</div>
3350
</form>

client/src/pages/Register.jsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
import { useState } from "react";
2+
3+
const Register = () => {
4+
const [loading, setLoading] = useState(false);
5+
6+
const handleSubmit = async (e) => {
7+
e.preventDefault();
8+
setLoading(true);
9+
try {
10+
// TODO: Add registration logic and API connection
11+
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
12+
} catch (error) {
13+
console.error('Registration failed:', error);
214
import { useNavigate, Link } from "react-router-dom";
315
import { useAuth } from "../context/AuthContext";
416

@@ -137,6 +149,9 @@ const Register = () => {
137149
Create an account
138150
</h2>
139151
</div>
152+
{/* TODO: Add authentication logic and API connection */}
153+
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
154+
<div className="rounded-md shadow-sm -space-y-px">
140155

141156
{error && (
142157
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm">
@@ -223,6 +238,12 @@ const Register = () => {
223238
)}
224239
</div>
225240
</div>
241+
<div>
242+
<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">
243+
<span className={`btn-text ${loading ? 'hidden' : ''}`}>Register</span>
244+
<span className={`btn-loader ${loading ? '' : 'hidden'}`}>Loading...</span>
245+
</button>
246+
</div>
226247

227248
<button
228249
type="submit"

client/src/pages/WorkerProfile.jsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
import { useParams, Link } from 'react-router-dom';
2+
import { useState } from "react";
3+
4+
const WorkerProfile = () => {
5+
const { id } = useParams();
6+
const [loading, setLoading] = useState(false);
7+
8+
const handleBook = async () => {
9+
setLoading(true);
10+
try {
11+
// TODO: Open booking form/modal
12+
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
13+
} catch (error) {
14+
console.error('Booking failed:', error);
15+
} finally {
16+
setLoading(false);
17+
}
18+
};
119
import { useParams, Link, useNavigate } from 'react-router-dom';
220
import { useState } from 'react';
321
import BookingConfirmationModal from '../components/BookingConfirmationModal';
@@ -99,11 +117,17 @@ const WorkerProfile = () => {
99117
</div>
100118

101119
<div className="mt-8 pt-8">
120+
{/* TODO: Implement booking modal or redirect to a booking flow */}
121+
<button
122+
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded disabled:opacity-50 disabled:cursor-not-allowed"
123+
onClick={handleBook}
124+
disabled={loading}
102125
<button
103126
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded transition"
104127
onClick={handleBooking}
105128
>
106-
Book This Service
129+
<span className={`btn-text ${loading ? 'hidden' : ''}`}>Book This Service</span>
130+
<span className={`btn-loader ${loading ? '' : 'hidden'}`}>Loading...</span>
107131
</button>
108132
</div>
109133
</div>

0 commit comments

Comments
 (0)