auto-commit for 8a62051e-b038-4363-a62d-7047e3d6e102

This commit is contained in:
emergent-agent-e1
2026-03-16 18:22:14 +00:00
parent 706e3e2eb6
commit 6a9e343332
16 changed files with 1510 additions and 212 deletions
+123 -66
View File
@@ -13,12 +13,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
import { toast } from 'sonner';
import { destinations as initialDestinations, specials as initialSpecials } from '../mockData';
import { destinationsAPI, specialsAPI, uploadAPI } from '../services/api';
const AdminDashboard = () => {
const navigate = useNavigate();
const [destinations, setDestinations] = useState(initialDestinations);
const [specials, setSpecials] = useState(initialSpecials);
const [destinations, setDestinations] = useState([]);
const [specials, setSpecials] = useState([]);
const [loading, setLoading] = useState(true);
const [isEditMode, setIsEditMode] = useState(false);
const [editingDestination, setEditingDestination] = useState(null);
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
@@ -37,35 +38,59 @@ const AdminDashboard = () => {
const isAuthenticated = localStorage.getItem('isAdminAuthenticated');
if (!isAuthenticated) {
navigate('/admin');
} else {
fetchData();
}
}, [navigate]);
const fetchData = async () => {
try {
setLoading(true);
const [destinationsData, specialsData] = await Promise.all([
destinationsAPI.getAll(),
specialsAPI.getAll()
]);
setDestinations(destinationsData);
setSpecials(specialsData);
} catch (error) {
console.error('Error fetching data:', error);
toast.error('Failed to load data');
} finally {
setLoading(false);
}
};
const handleLogout = () => {
localStorage.removeItem('isAdminAuthenticated');
localStorage.removeItem('auth_token');
toast.success('Logged out successfully');
navigate('/admin');
};
const handleAddDestination = () => {
const newDest = {
...newDestination,
id: String(destinations.length + 1),
rating: parseFloat(newDestination.rating),
price: parseFloat(newDestination.price)
};
setDestinations([...destinations, newDest]);
setIsAddDialogOpen(false);
setNewDestination({
name: '',
location: '',
description: '',
image: '',
category: 'City',
rating: 4.5,
price: 999,
currency: 'USD'
});
toast.success('Destination added successfully!');
const handleAddDestination = async () => {
try {
const newDest = await destinationsAPI.create({
...newDestination,
rating: parseFloat(newDestination.rating),
price: parseFloat(newDestination.price)
});
setDestinations([...destinations, newDest]);
setIsAddDialogOpen(false);
setNewDestination({
name: '',
location: '',
description: '',
image: '',
category: 'City',
rating: 4.5,
price: 999,
currency: 'USD'
});
toast.success('Destination added successfully!');
} catch (error) {
console.error('Error adding destination:', error);
toast.error('Failed to add destination');
}
};
const handleEditDestination = (destination) => {
@@ -73,45 +98,71 @@ const AdminDashboard = () => {
setIsEditMode(true);
};
const handleSaveEdit = () => {
setDestinations(destinations.map(dest =>
dest.id === editingDestination.id ? editingDestination : dest
));
setIsEditMode(false);
setEditingDestination(null);
toast.success('Destination updated successfully!');
};
const handleDeleteDestination = (id) => {
setDestinations(destinations.filter(dest => dest.id !== id));
// Also remove from specials if exists
setSpecials(specials.filter(special => special.destinationId !== id));
toast.success('Destination deleted successfully!');
};
const handleToggleSpecial = (destinationId) => {
const existingSpecial = specials.find(s => s.destinationId === destinationId);
if (existingSpecial) {
setSpecials(specials.filter(s => s.destinationId !== destinationId));
toast.success('Removed from specials');
} else {
const newSpecial = {
destinationId,
discount: 20,
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
highlights: ['Special offer', 'Limited time', 'Book now']
};
setSpecials([...specials, newSpecial]);
toast.success('Added to specials!');
const handleSaveEdit = async () => {
try {
const updated = await destinationsAPI.update(editingDestination.id, editingDestination);
setDestinations(destinations.map(dest =>
dest.id === updated.id ? updated : dest
));
setIsEditMode(false);
setEditingDestination(null);
toast.success('Destination updated successfully!');
} catch (error) {
console.error('Error updating destination:', error);
toast.error('Failed to update destination');
}
};
const handleUpdateSpecial = (destinationId, field, value) => {
setSpecials(specials.map(special =>
special.destinationId === destinationId
? { ...special, [field]: field === 'discount' ? parseFloat(value) : value }
: special
));
const handleDeleteDestination = async (id) => {
try {
await destinationsAPI.delete(id);
setDestinations(destinations.filter(dest => dest.id !== id));
setSpecials(specials.filter(special => special.destination_id !== id));
toast.success('Destination deleted successfully!');
} catch (error) {
console.error('Error deleting destination:', error);
toast.error('Failed to delete destination');
}
};
const handleToggleSpecial = async (destinationId) => {
const existingSpecial = specials.find(s => s.destination_id === destinationId);
try {
if (existingSpecial) {
await specialsAPI.deleteByDestination(destinationId);
setSpecials(specials.filter(s => s.destination_id !== destinationId));
toast.success('Removed from specials');
} else {
const newSpecial = await specialsAPI.create({
destination_id: destinationId,
discount: 20,
end_date: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
highlights: ['Special offer', 'Limited time', 'Book now']
});
setSpecials([...specials, newSpecial]);
toast.success('Added to specials!');
}
} catch (error) {
console.error('Error toggling special:', error);
toast.error('Failed to update special');
}
};
const handleUpdateSpecial = async (specialId, field, value) => {
try {
const updatedSpecial = specials.find(s => s.id === specialId);
if (!updatedSpecial) return;
const updateData = { [field]: field === 'discount' ? parseFloat(value) : value };
const updated = await specialsAPI.update(specialId, updateData);
setSpecials(specials.map(special =>
special.id === specialId ? updated : special
));
} catch (error) {
console.error('Error updating special:', error);
toast.error('Failed to update special');
}
};
return (
@@ -247,8 +298,13 @@ const AdminDashboard = () => {
</Dialog>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{destinations.map((destination) => (
{loading ? (
<div className="text-center py-12">
<p className="text-gray-600">Loading destinations...</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{destinations.map((destination) => (
<Card key={destination.id} className="overflow-hidden">
<div className="relative">
<img
@@ -279,7 +335,7 @@ const AdminDashboard = () => {
<p className="text-sm text-gray-600 mb-4 line-clamp-2">{destination.description}</p>
<div className="flex items-center justify-between mb-4">
<span className="text-xl font-bold text-cyan-600">${destination.price}</span>
{specials.some(s => s.destinationId === destination.id) && (
{specials.some(s => s.destination_id === destination.id) && (
<Badge variant="outline" className="border-red-500 text-red-500">Special</Badge>
)}
</div>
@@ -307,6 +363,7 @@ const AdminDashboard = () => {
</Card>
))}
</div>
)}
</TabsContent>
{/* Specials Management */}
@@ -318,7 +375,7 @@ const AdminDashboard = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{destinations.map((destination) => {
const special = specials.find(s => s.destinationId === destination.id);
const special = specials.find(s => s.destination_id === destination.id);
return (
<Card key={destination.id}>
<CardContent className="pt-6">
@@ -342,7 +399,7 @@ const AdminDashboard = () => {
<Input
type="number"
value={special.discount}
onChange={(e) => handleUpdateSpecial(destination.id, 'discount', e.target.value)}
onChange={(e) => handleUpdateSpecial(special.id, 'discount', e.target.value)}
className="h-8"
/>
</div>
@@ -350,8 +407,8 @@ const AdminDashboard = () => {
<label className="text-xs text-gray-600">End Date</label>
<Input
type="date"
value={special.endDate}
onChange={(e) => handleUpdateSpecial(destination.id, 'endDate', e.target.value)}
value={special.end_date}
onChange={(e) => handleUpdateSpecial(special.id, 'end_date', e.target.value)}
className="h-8"
/>
</div>
+13 -6
View File
@@ -5,22 +5,29 @@ import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
import { toast } from 'sonner';
import { authAPI } from '../services/api';
const AdminLogin = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = (e) => {
const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
// Mock authentication - will be replaced with real backend
if (email === 'admin@epictravel.com' && password === 'admin123') {
try {
const response = await authAPI.login(email, password);
localStorage.setItem('auth_token', response.access_token);
localStorage.setItem('isAdminAuthenticated', 'true');
toast.success('Login successful!');
navigate('/admin/dashboard');
} else {
} catch (error) {
console.error('Login error:', error);
toast.error('Invalid credentials. Try: admin@epictravel.com / admin123');
} finally {
setLoading(false);
}
};
@@ -68,8 +75,8 @@ const AdminLogin = () => {
/>
</div>
</div>
<Button type="submit" className="w-full bg-cyan-600 hover:bg-cyan-700" size="lg">
Sign In
<Button type="submit" className="w-full bg-cyan-600 hover:bg-cyan-700" size="lg" disabled={loading}>
{loading ? 'Signing in...' : 'Sign In'}
</Button>
</form>
<div className="mt-6 p-4 bg-cyan-50 rounded-lg border border-cyan-200">
+122 -63
View File
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { MapPin, Star, Calendar, Tag, Search, Send, Mail, Phone, MessageSquare } from 'lucide-react';
import { destinations, specials, testimonials, categories } from '../mockData';
import { testimonials, categories } from '../mockData';
import { destinationsAPI, specialsAPI, contactAPI, newsletterAPI } from '../services/api';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Textarea } from '../components/ui/textarea';
@@ -13,12 +14,44 @@ const Home = () => {
const [searchQuery, setSearchQuery] = useState('');
const [contactForm, setContactForm] = useState({ name: '', email: '', message: '' });
const [newsletterEmail, setNewsletterEmail] = useState('');
const [destinations, setDestinations] = useState([]);
const [specials, setSpecials] = useState([]);
const [loading, setLoading] = useState(true);
// Fetch destinations and specials on mount
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
setLoading(true);
const [destinationsData, specialsData] = await Promise.all([
destinationsAPI.getAll(),
specialsAPI.getAll()
]);
setDestinations(destinationsData);
setSpecials(specialsData);
} catch (error) {
console.error('Error fetching data:', error);
toast.error('Failed to load destinations');
} finally {
setLoading(false);
}
};
// Get special destinations
const specialDestinations = specials.map(special => {
const dest = destinations.find(d => d.id === special.destinationId);
return { ...dest, ...special };
});
const dest = destinations.find(d => d.id === special.destination_id);
if (!dest) return null;
return {
...dest,
discount: special.discount,
endDate: special.end_date,
highlights: special.highlights,
specialId: special.id
};
}).filter(Boolean);
// Filter destinations
const filteredDestinations = destinations.filter(dest => {
@@ -28,16 +61,26 @@ const Home = () => {
return matchesCategory && matchesSearch;
});
const handleContactSubmit = (e) => {
const handleContactSubmit = async (e) => {
e.preventDefault();
toast.success('Message sent! We\'ll get back to you soon.');
setContactForm({ name: '', email: '', message: '' });
try {
await contactAPI.submit(contactForm);
toast.success('Message sent! We\'ll get back to you soon.');
setContactForm({ name: '', email: '', message: '' });
} catch (error) {
toast.error('Failed to send message. Please try again.');
}
};
const handleNewsletterSubmit = (e) => {
const handleNewsletterSubmit = async (e) => {
e.preventDefault();
toast.success('Successfully subscribed to our newsletter!');
setNewsletterEmail('');
try {
await newsletterAPI.subscribe(newsletterEmail);
toast.success('Successfully subscribed to our newsletter!');
setNewsletterEmail('');
} catch (error) {
toast.error('Failed to subscribe. Please try again.');
}
};
return (
@@ -95,59 +138,69 @@ const Home = () => {
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{specialDestinations.map((special) => (
<Card key={special.id} className="overflow-hidden hover:shadow-2xl transition-shadow duration-300 group">
<div className="relative overflow-hidden">
<img
src={special.image}
alt={special.name}
className="w-full h-64 object-cover group-hover:scale-110 transition-transform duration-500"
/>
<div className="absolute top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-full font-bold text-lg shadow-lg">
{special.discount}% OFF
{loading ? (
<div className="text-center py-12">
<p className="text-gray-600">Loading specials...</p>
</div>
) : specialDestinations.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-600">No special offers available at the moment.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{specialDestinations.map((special) => (
<Card key={special.id} className="overflow-hidden hover:shadow-2xl transition-shadow duration-300 group">
<div className="relative overflow-hidden">
<img
src={special.image}
alt={special.name}
className="w-full h-64 object-cover group-hover:scale-110 transition-transform duration-500"
/>
<div className="absolute top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-full font-bold text-lg shadow-lg">
{special.discount}% OFF
</div>
<div className="absolute bottom-4 left-4 bg-white/95 backdrop-blur-sm px-3 py-1 rounded-full flex items-center space-x-1">
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
<span className="font-semibold">{special.rating}</span>
</div>
</div>
<div className="absolute bottom-4 left-4 bg-white/95 backdrop-blur-sm px-3 py-1 rounded-full flex items-center space-x-1">
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
<span className="font-semibold">{special.rating}</span>
</div>
</div>
<CardHeader>
<CardTitle className="text-2xl">{special.name}</CardTitle>
<CardDescription className="flex items-center text-base">
<MapPin className="w-4 h-4 mr-1" />
{special.location}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-gray-600 mb-4">{special.description}</p>
<div className="space-y-2 mb-4">
{special.highlights.map((highlight, idx) => (
<div key={idx} className="flex items-start space-x-2 text-sm text-gray-700">
<Tag className="w-4 h-4 text-cyan-600 mt-0.5 flex-shrink-0" />
<span>{highlight}</span>
<CardHeader>
<CardTitle className="text-2xl">{special.name}</CardTitle>
<CardDescription className="flex items-center text-base">
<MapPin className="w-4 h-4 mr-1" />
{special.location}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-gray-600 mb-4">{special.description}</p>
<div className="space-y-2 mb-4">
{special.highlights.map((highlight, idx) => (
<div key={idx} className="flex items-start space-x-2 text-sm text-gray-700">
<Tag className="w-4 h-4 text-cyan-600 mt-0.5 flex-shrink-0" />
<span>{highlight}</span>
</div>
))}
</div>
<div className="flex items-center justify-between mb-4">
<div>
<span className="text-gray-500 line-through text-lg">${special.price}</span>
<span className="text-3xl font-bold text-cyan-600 ml-2">
${Math.round(special.price * (1 - special.discount / 100))}
</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<Calendar className="w-4 h-4 mr-1" />
Until {new Date(special.endDate).toLocaleDateString()}
</div>
))}
</div>
<div className="flex items-center justify-between mb-4">
<div>
<span className="text-gray-500 line-through text-lg">${special.price}</span>
<span className="text-3xl font-bold text-cyan-600 ml-2">
${Math.round(special.price * (1 - special.discount / 100))}
</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<Calendar className="w-4 h-4 mr-1" />
Until {new Date(special.endDate).toLocaleDateString()}
</div>
</div>
<Button className="w-full bg-cyan-600 hover:bg-cyan-700">
Book Now
</Button>
</CardContent>
</Card>
))}
</div>
<Button className="w-full bg-cyan-600 hover:bg-cyan-700">
Book Now
</Button>
</CardContent>
</Card>
))}
</div>
)}
</div>
</section>
@@ -190,8 +243,13 @@ const Home = () => {
</div>
{/* Destinations Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredDestinations.map((destination) => (
{loading ? (
<div className="text-center py-12">
<p className="text-gray-600">Loading destinations...</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredDestinations.map((destination) => (
<Card key={destination.id} className="overflow-hidden hover:shadow-xl transition-shadow duration-300 group cursor-pointer">
<div className="relative overflow-hidden">
<img
@@ -229,6 +287,7 @@ const Home = () => {
</Card>
))}
</div>
)}
</div>
</section>
+112
View File
@@ -0,0 +1,112 @@
import axios from 'axios';
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL;
const API = `${BACKEND_URL}/api`;
// Create axios instance
const apiClient = axios.create({
baseURL: API,
headers: {
'Content-Type': 'application/json',
},
});
// Add auth token to requests if available
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Auth API
export const authAPI = {
login: async (email, password) => {
const response = await apiClient.post('/auth/login', { email, password });
return response.data;
},
verify: async () => {
const response = await apiClient.post('/auth/verify');
return response.data;
},
};
// Destinations API
export const destinationsAPI = {
getAll: async (category, search) => {
const params = {};
if (category) params.category = category;
if (search) params.search = search;
const response = await apiClient.get('/destinations', { params });
return response.data;
},
getById: async (id) => {
const response = await apiClient.get(`/destinations/${id}`);
return response.data;
},
create: async (destination) => {
const response = await apiClient.post('/destinations', destination);
return response.data;
},
update: async (id, destination) => {
const response = await apiClient.put(`/destinations/${id}`, destination);
return response.data;
},
delete: async (id) => {
const response = await apiClient.delete(`/destinations/${id}`);
return response.data;
},
};
// Specials API
export const specialsAPI = {
getAll: async () => {
const response = await apiClient.get('/specials');
return response.data;
},
create: async (special) => {
const response = await apiClient.post('/specials', special);
return response.data;
},
update: async (id, special) => {
const response = await apiClient.put(`/specials/${id}`, special);
return response.data;
},
deleteByDestination: async (destinationId) => {
const response = await apiClient.delete(`/specials/destination/${destinationId}`);
return response.data;
},
};
// Contact API
export const contactAPI = {
submit: async (contact) => {
const response = await apiClient.post('/contact', contact);
return response.data;
},
};
// Newsletter API
export const newsletterAPI = {
subscribe: async (email) => {
const response = await apiClient.post('/newsletter/subscribe', { email });
return response.data;
},
};
// Image Upload API
export const uploadAPI = {
uploadImage: async (file) => {
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post('/upload/image', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
},
};
export default apiClient;