Skip to content

Commit e52b1d4

Browse files
committed
Add TripCard and TripList components for trip management; implement loading and error states
1 parent ba905d6 commit e52b1d4

5 files changed

Lines changed: 324 additions & 44 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
Card,
3+
CardContent,
4+
CardMedia,
5+
Typography,
6+
CardActions,
7+
Button,
8+
Chip,
9+
Box,
10+
Skeleton
11+
} from '@mui/material';
12+
import {
13+
LocationOn,
14+
DateRange,
15+
ArrowForward
16+
} from '@mui/icons-material';
17+
import { useNavigate } from 'react-router-dom';
18+
19+
const TripCard = ({ trip, loading }) => {
20+
const navigate = useNavigate();
21+
22+
if (loading) {
23+
return (
24+
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
25+
<Skeleton variant="rectangular" height={140} />
26+
<CardContent>
27+
<Skeleton variant="text" height={32} width="80%" />
28+
<Skeleton variant="text" height={24} width="60%" />
29+
<Skeleton variant="text" height={24} width="40%" />
30+
</CardContent>
31+
<CardActions>
32+
<Skeleton variant="rectangular" width={100} height={36} />
33+
</CardActions>
34+
</Card>
35+
);
36+
}
37+
38+
return (
39+
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
40+
<CardContent sx={{ flexGrow: 1 }}>
41+
<Typography gutterBottom variant="h5" component="h2">
42+
{trip.title}
43+
</Typography>
44+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
45+
<LocationOn fontSize="small" color="primary" />
46+
<Typography variant="body2" color="text.secondary" sx={{ ml: 1 }}>
47+
{trip.destination}
48+
</Typography>
49+
</Box>
50+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
51+
<DateRange fontSize="small" color="primary" />
52+
<Typography variant="body2" color="text.secondary" sx={{ ml: 1 }}>
53+
{new Date(trip.start_date).toLocaleDateString()} - {new Date(trip.end_date).toLocaleDateString()}
54+
</Typography>
55+
</Box>
56+
</CardContent>
57+
<CardActions>
58+
<Button
59+
size="small"
60+
endIcon={<ArrowForward />}
61+
onClick={() => navigate(`/trips/${trip.id}`)}
62+
>
63+
View Details
64+
</Button>
65+
</CardActions>
66+
</Card>
67+
);
68+
};
69+
70+
export default TripCard;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useState, useEffect } from 'react';
2+
import {
3+
Grid,
4+
Typography,
5+
Box,
6+
Alert,
7+
Button
8+
} from '@mui/material';
9+
import { Add as AddIcon } from '@mui/icons-material';
10+
import TripCard from './TripCard';
11+
import { useNavigate } from 'react-router-dom';
12+
import { tripService } from '../../services/tripService';
13+
14+
const TripList = ({ WelcomeMessage, ErrorState }) => {
15+
const [trips, setTrips] = useState([]);
16+
const [loading, setLoading] = useState(true);
17+
const [error, setError] = useState(null);
18+
const navigate = useNavigate();
19+
20+
useEffect(() => {
21+
const fetchTrips = async () => {
22+
try {
23+
setLoading(true);
24+
const data = await tripService.getAllTrips();
25+
console.log('TripList received data:', data); // Debug log
26+
27+
if (!data || !data.trips) {
28+
console.error('Invalid data format:', data);
29+
setError('Unexpected data format received');
30+
return;
31+
}
32+
33+
setTrips(data.trips);
34+
} catch (err) {
35+
console.error('TripList error:', err);
36+
setError(err.message);
37+
} finally {
38+
setLoading(false);
39+
}
40+
};
41+
42+
fetchTrips();
43+
}, []);
44+
45+
// Loading state with skeleton cards
46+
if (loading) {
47+
return (
48+
<Grid container spacing={3}>
49+
{[1, 2, 3].map((skeleton) => (
50+
<Grid item xs={12} sm={6} md={4} key={skeleton}>
51+
<TripCard loading={true} />
52+
</Grid>
53+
))}
54+
</Grid>
55+
);
56+
}
57+
58+
// Error state
59+
if (error) {
60+
return <ErrorState />;
61+
}
62+
63+
// Empty state
64+
if (trips.length === 0) {
65+
return <WelcomeMessage />;
66+
}
67+
68+
// Loaded state with trips
69+
return (
70+
<Grid container spacing={3}>
71+
{trips.map((trip) => (
72+
<Grid item xs={12} sm={6} md={4} key={trip.id}>
73+
<TripCard trip={trip} />
74+
</Grid>
75+
))}
76+
<Grid item xs={12} sm={6} md={4}>
77+
<Button
78+
variant="outlined"
79+
fullWidth
80+
sx={{
81+
height: '100%',
82+
minHeight: 200,
83+
display: 'flex',
84+
flexDirection: 'column',
85+
alignItems: 'center',
86+
justifyContent: 'center'
87+
}}
88+
onClick={() => navigate('/trips/new')}
89+
>
90+
<AddIcon sx={{ mb: 1 }} />
91+
<Typography>Add New Trip</Typography>
92+
</Button>
93+
</Grid>
94+
</Grid>
95+
);
96+
};
97+
98+
export default TripList;

planventure-client/src/pages/Dashboard.jsx

Lines changed: 100 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,108 @@
1-
import { Box, Typography, Grid, Paper } from '@mui/material';
1+
import { Box, Typography, Grid, Paper, Button } from '@mui/material';
2+
import { Add as AddIcon } from '@mui/icons-material';
3+
import TripList from '../components/trips/TripList';
4+
import { useAuth } from '../context/AuthContext';
5+
import travelingSvg from '../assets/undraw_traveling_yhxq.svg';
26

37
const Dashboard = () => {
8+
const { user } = useAuth();
9+
10+
const WelcomeMessage = () => (
11+
<Box
12+
sx={{
13+
textAlign: 'center',
14+
py: 6,
15+
px: 2,
16+
display: 'flex',
17+
flexDirection: 'column',
18+
alignItems: 'center',
19+
gap: 3
20+
}}
21+
>
22+
<img
23+
src={travelingSvg}
24+
alt="Start your journey"
25+
style={{
26+
maxWidth: '300px',
27+
width: '100%',
28+
height: 'auto',
29+
marginBottom: '1rem'
30+
}}
31+
/>
32+
<Typography variant="h4" component="h2" gutterBottom>
33+
Welcome to Planventure!
34+
</Typography>
35+
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: '600px', mb: 3 }}>
36+
Ready to start planning your next adventure? Create your first trip and let us help you organize everything from destinations to activities.
37+
</Typography>
38+
<Button
39+
variant="contained"
40+
size="large"
41+
startIcon={<AddIcon />}
42+
onClick={() => navigate('/trips/new')}
43+
>
44+
Plan Your First Trip
45+
</Button>
46+
</Box>
47+
);
48+
49+
const ErrorState = () => (
50+
<Box
51+
sx={{
52+
textAlign: 'center',
53+
py: 6,
54+
px: 2,
55+
display: 'flex',
56+
flexDirection: 'column',
57+
alignItems: 'center',
58+
gap: 3
59+
}}
60+
>
61+
<img
62+
src={travelingSvg}
63+
alt="Error loading trips"
64+
style={{
65+
maxWidth: '300px',
66+
width: '100%',
67+
height: 'auto',
68+
marginBottom: '1rem',
69+
opacity: 0.7
70+
}}
71+
/>
72+
<Typography variant="h5" component="h2" gutterBottom>
73+
Oops! Looks like our compass is spinning! 🧭
74+
</Typography>
75+
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: '600px', mb: 3 }}>
76+
We're having trouble loading your adventures. Don't worry, even the best travelers sometimes lose their way! Try refreshing the page or come back later.
77+
</Typography>
78+
<Button
79+
variant="contained"
80+
onClick={() => window.location.reload()}
81+
>
82+
Try Again
83+
</Button>
84+
</Box>
85+
);
86+
487
return (
5-
<Box>
88+
<Box sx={{ maxWidth: 1200, mx: 'auto', p: { xs: 2, sm: 3 } }}>
689
<Typography variant="h4" component="h1" gutterBottom>
7-
Dashboard
90+
My Trips
891
</Typography>
9-
<Grid container spacing={3}>
10-
<Grid item xs={12} md={6} lg={4}>
11-
<Paper
12-
elevation={2}
13-
sx={{
14-
p: 3,
15-
display: 'flex',
16-
flexDirection: 'column',
17-
height: 240,
18-
}}
19-
>
20-
<Typography variant="h6" gutterBottom>
21-
Upcoming Trips
22-
</Typography>
23-
{/* Add content here */}
24-
</Paper>
25-
</Grid>
26-
<Grid item xs={12} md={6} lg={4}>
27-
<Paper
28-
elevation={2}
29-
sx={{
30-
p: 3,
31-
display: 'flex',
32-
flexDirection: 'column',
33-
height: 240,
34-
}}
35-
>
36-
<Typography variant="h6" gutterBottom>
37-
Recent Activities
38-
</Typography>
39-
{/* Add content here */}
40-
</Paper>
41-
</Grid>
42-
</Grid>
92+
<Paper
93+
elevation={2}
94+
sx={{
95+
p: 3,
96+
display: 'flex',
97+
flexDirection: 'column',
98+
minHeight: '60vh'
99+
}}
100+
>
101+
<TripList
102+
WelcomeMessage={WelcomeMessage}
103+
ErrorState={ErrorState}
104+
/>
105+
</Paper>
43106
</Box>
44107
);
45108
};

planventure-client/src/services/api.jsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1-
const BASE_URL = import.meta.env.BASE_API_URL || 'http://localhost:5000';
1+
const BASE_URL = 'http://localhost:5000';
2+
3+
const getAuthHeaders = () => {
4+
const token = localStorage.getItem('token');
5+
return {
6+
'Content-Type': 'application/json',
7+
'Authorization': token ? `Bearer ${token}` : '',
8+
};
9+
};
210

311
const handleResponse = async (response) => {
12+
if (response.status === 401) {
13+
localStorage.removeItem('token');
14+
window.location.href = '/login';
15+
throw new Error('Session expired. Please login again.');
16+
}
17+
418
const data = await response.json();
519
if (!response.ok) {
620
throw new Error(data.error || 'Request failed');
@@ -9,24 +23,22 @@ const handleResponse = async (response) => {
923
};
1024

1125
export const api = {
12-
// General purpose methods
1326
get: async (endpoint) => {
14-
const response = await fetch(`${BASE_URL}${endpoint}`);
27+
const response = await fetch(`${BASE_URL}${endpoint}`, {
28+
headers: getAuthHeaders(),
29+
});
1530
return handleResponse(response);
1631
},
1732

1833
post: async (endpoint, data) => {
1934
const response = await fetch(`${BASE_URL}${endpoint}`, {
2035
method: 'POST',
21-
headers: {
22-
'Content-Type': 'application/json',
23-
},
36+
headers: getAuthHeaders(),
2437
body: JSON.stringify(data),
2538
});
2639
return handleResponse(response);
2740
},
2841

29-
// Auth specific methods
3042
auth: {
3143
login: async (credentials) => {
3244
return api.post('/auth/login', credentials);

0 commit comments

Comments
 (0)