Initial commit from remix
This commit is contained in:
574
src/pages/Checkout.tsx
Normal file
574
src/pages/Checkout.tsx
Normal file
@@ -0,0 +1,574 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCart } from '@/contexts/CartContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ArrowLeft, CreditCard, Smartphone, Building, Truck, Shield, Check, X } from 'lucide-react';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface CustomerInfo {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
city: string;
|
||||
country: string;
|
||||
zipCode: string;
|
||||
}
|
||||
|
||||
interface PaymentInfo {
|
||||
method: 'credit_card' | 'paypal' | 'bank_transfer' | 'cash';
|
||||
cardNumber: string;
|
||||
expiryDate: string;
|
||||
cvv: string;
|
||||
cardName: string;
|
||||
}
|
||||
|
||||
const Checkout = () => {
|
||||
const navigate = useNavigate();
|
||||
const { items, getTotalPrice, clearCart } = useCart();
|
||||
const { toast } = useToast();
|
||||
const [step, setStep] = useState(1);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const [customerInfo, setCustomerInfo] = useState<CustomerInfo>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
city: '',
|
||||
country: 'Dominican Republic',
|
||||
zipCode: ''
|
||||
});
|
||||
|
||||
const [paymentInfo, setPaymentInfo] = useState<PaymentInfo>({
|
||||
method: 'credit_card',
|
||||
cardNumber: '',
|
||||
expiryDate: '',
|
||||
cvv: '',
|
||||
cardName: ''
|
||||
});
|
||||
|
||||
const [specialRequests, setSpecialRequests] = useState('');
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-12">
|
||||
<div className="text-center py-12">
|
||||
<h1 className="text-2xl font-bold mb-4">Tu carrito está vacío</h1>
|
||||
<p className="text-gray-600 mb-6">Agrega algunas ofertas a tu carrito para continuar.</p>
|
||||
<Button onClick={() => navigate('/explore')}>
|
||||
Explorar Ofertas
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalPrice = getTotalPrice();
|
||||
const taxes = totalPrice * 0.18; // 18% ITBIS (impuesto dominicano)
|
||||
const serviceFee = totalPrice * 0.05; // 5% fee de servicio
|
||||
const finalTotal = totalPrice + taxes + serviceFee;
|
||||
|
||||
const paymentMethods = [
|
||||
{ id: 'credit_card', name: 'Tarjeta de Crédito/Débito', icon: CreditCard },
|
||||
{ id: 'paypal', name: 'PayPal', icon: Smartphone },
|
||||
{ id: 'bank_transfer', name: 'Transferencia Bancaria', icon: Building },
|
||||
{ id: 'cash', name: 'Pago en Efectivo', icon: Truck }
|
||||
];
|
||||
|
||||
const handleCustomerInfoChange = (field: keyof CustomerInfo, value: string) => {
|
||||
setCustomerInfo(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handlePaymentInfoChange = (field: keyof PaymentInfo, value: string) => {
|
||||
setPaymentInfo(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const saveOrderToJSON = (orderData: any) => {
|
||||
const orders = JSON.parse(localStorage.getItem('karibeo_orders') || '[]');
|
||||
orders.push(orderData);
|
||||
localStorage.setItem('karibeo_orders', JSON.stringify(orders));
|
||||
};
|
||||
|
||||
const processPayment = async () => {
|
||||
setIsProcessing(true);
|
||||
|
||||
// Simulate payment processing
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
const orderData = {
|
||||
id: Date.now().toString(),
|
||||
date: new Date().toISOString(),
|
||||
items: items,
|
||||
customerInfo,
|
||||
paymentInfo: {
|
||||
method: paymentInfo.method,
|
||||
// Don't save sensitive payment data in real implementation
|
||||
cardLast4: paymentInfo.cardNumber.slice(-4)
|
||||
},
|
||||
specialRequests,
|
||||
pricing: {
|
||||
subtotal: totalPrice,
|
||||
taxes,
|
||||
serviceFee,
|
||||
total: finalTotal
|
||||
},
|
||||
status: 'confirmed'
|
||||
};
|
||||
|
||||
saveOrderToJSON(orderData);
|
||||
clearCart();
|
||||
|
||||
toast({
|
||||
title: "¡Pago procesado exitosamente!",
|
||||
description: "Tu reserva ha sido confirmada. Recibirás un email de confirmación.",
|
||||
});
|
||||
|
||||
navigate('/order-confirmation', { state: { orderData } });
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
const validateStep1 = () => {
|
||||
return customerInfo.firstName && customerInfo.lastName && customerInfo.email && customerInfo.phone;
|
||||
};
|
||||
|
||||
const validateStep2 = () => {
|
||||
if (paymentInfo.method === 'credit_card') {
|
||||
return paymentInfo.cardNumber && paymentInfo.expiryDate && paymentInfo.cvv && paymentInfo.cardName;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-12">
|
||||
<div className="mb-6">
|
||||
<Button variant="ghost" onClick={() => navigate(-1)} className="mb-4">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver
|
||||
</Button>
|
||||
<h1 className="text-3xl font-bold">Checkout</h1>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="flex items-center mt-6 mb-8">
|
||||
<div className={`flex items-center ${step >= 1 ? 'text-primary' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
|
||||
step >= 1 ? 'border-primary bg-primary text-white' : 'border-gray-300'
|
||||
}`}>
|
||||
{step > 1 ? <Check className="w-4 h-4" /> : '1'}
|
||||
</div>
|
||||
<span className="ml-2 font-medium">Información Personal</span>
|
||||
</div>
|
||||
<div className={`flex-1 h-px mx-4 ${step > 1 ? 'bg-primary' : 'bg-gray-300'}`} />
|
||||
<div className={`flex items-center ${step >= 2 ? 'text-primary' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
|
||||
step >= 2 ? 'border-primary bg-primary text-white' : 'border-gray-300'
|
||||
}`}>
|
||||
{step > 2 ? <Check className="w-4 h-4" /> : '2'}
|
||||
</div>
|
||||
<span className="ml-2 font-medium">Pago</span>
|
||||
</div>
|
||||
<div className={`flex-1 h-px mx-4 ${step > 2 ? 'bg-primary' : 'bg-gray-300'}`} />
|
||||
<div className={`flex items-center ${step >= 3 ? 'text-primary' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
|
||||
step >= 3 ? 'border-primary bg-primary text-white' : 'border-gray-300'
|
||||
}`}>
|
||||
3
|
||||
</div>
|
||||
<span className="ml-2 font-medium">Confirmación</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2">
|
||||
{step === 1 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Información Personal</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="firstName">Nombre *</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={customerInfo.firstName}
|
||||
onChange={(e) => handleCustomerInfoChange('firstName', e.target.value)}
|
||||
placeholder="Tu nombre"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="lastName">Apellido *</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={customerInfo.lastName}
|
||||
onChange={(e) => handleCustomerInfoChange('lastName', e.target.value)}
|
||||
placeholder="Tu apellido"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">Email *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={customerInfo.email}
|
||||
onChange={(e) => handleCustomerInfoChange('email', e.target.value)}
|
||||
placeholder="tu@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="phone">Teléfono *</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={customerInfo.phone}
|
||||
onChange={(e) => handleCustomerInfoChange('phone', e.target.value)}
|
||||
placeholder="+1 (809) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="address">Dirección</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={customerInfo.address}
|
||||
onChange={(e) => handleCustomerInfoChange('address', e.target.value)}
|
||||
placeholder="Calle, número, sector"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="city">Ciudad</Label>
|
||||
<Input
|
||||
id="city"
|
||||
value={customerInfo.city}
|
||||
onChange={(e) => handleCustomerInfoChange('city', e.target.value)}
|
||||
placeholder="Santo Domingo"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="country">País</Label>
|
||||
<Select
|
||||
value={customerInfo.country}
|
||||
onValueChange={(value) => handleCustomerInfoChange('country', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Dominican Republic">República Dominicana</SelectItem>
|
||||
<SelectItem value="United States">Estados Unidos</SelectItem>
|
||||
<SelectItem value="Canada">Canadá</SelectItem>
|
||||
<SelectItem value="Other">Otro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="zipCode">Código Postal</Label>
|
||||
<Input
|
||||
id="zipCode"
|
||||
value={customerInfo.zipCode}
|
||||
onChange={(e) => handleCustomerInfoChange('zipCode', e.target.value)}
|
||||
placeholder="10001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="requests">Solicitudes Especiales</Label>
|
||||
<Textarea
|
||||
id="requests"
|
||||
value={specialRequests}
|
||||
onChange={(e) => setSpecialRequests(e.target.value)}
|
||||
placeholder="Algún requerimiento especial para tu reserva..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => setStep(2)}
|
||||
disabled={!validateStep1()}
|
||||
>
|
||||
Continuar al Pago
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Método de Pago</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Payment Method Selection */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{paymentMethods.map((method) => {
|
||||
const IconComponent = method.icon;
|
||||
return (
|
||||
<button
|
||||
key={method.id}
|
||||
onClick={() => handlePaymentInfoChange('method', method.id as any)}
|
||||
className={`p-4 border-2 rounded-lg flex items-center space-x-3 transition-colors ${
|
||||
paymentInfo.method === method.id
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
<span className="font-medium">{method.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Credit Card Form */}
|
||||
{paymentInfo.method === 'credit_card' && (
|
||||
<div className="space-y-4 border-t pt-6">
|
||||
<div>
|
||||
<Label htmlFor="cardName">Nombre en la Tarjeta *</Label>
|
||||
<Input
|
||||
id="cardName"
|
||||
value={paymentInfo.cardName}
|
||||
onChange={(e) => handlePaymentInfoChange('cardName', e.target.value)}
|
||||
placeholder="Como aparece en la tarjeta"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="cardNumber">Número de Tarjeta *</Label>
|
||||
<Input
|
||||
id="cardNumber"
|
||||
value={paymentInfo.cardNumber}
|
||||
onChange={(e) => handlePaymentInfoChange('cardNumber', e.target.value)}
|
||||
placeholder="1234 5678 9012 3456"
|
||||
maxLength={19}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="expiryDate">Fecha de Vencimiento *</Label>
|
||||
<Input
|
||||
id="expiryDate"
|
||||
value={paymentInfo.expiryDate}
|
||||
onChange={(e) => handlePaymentInfoChange('expiryDate', e.target.value)}
|
||||
placeholder="MM/YY"
|
||||
maxLength={5}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="cvv">CVV *</Label>
|
||||
<Input
|
||||
id="cvv"
|
||||
value={paymentInfo.cvv}
|
||||
onChange={(e) => handlePaymentInfoChange('cvv', e.target.value)}
|
||||
placeholder="123"
|
||||
maxLength={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other Payment Methods Info */}
|
||||
{paymentInfo.method === 'paypal' && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg border-t">
|
||||
<p className="text-sm text-blue-700">
|
||||
Serás redirigido a PayPal para completar tu pago de forma segura.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{paymentInfo.method === 'bank_transfer' && (
|
||||
<div className="bg-green-50 p-4 rounded-lg border-t">
|
||||
<p className="text-sm text-green-700 mb-2">
|
||||
Te enviaremos los detalles de la cuenta bancaria para realizar la transferencia.
|
||||
</p>
|
||||
<p className="text-xs text-green-600">
|
||||
Tu reserva se confirmará una vez recibamos el comprobante de pago.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{paymentInfo.method === 'cash' && (
|
||||
<div className="bg-orange-50 p-4 rounded-lg border-t">
|
||||
<p className="text-sm text-orange-700 mb-2">
|
||||
Podrás pagar en efectivo al momento del check-in o al llegar al lugar.
|
||||
</p>
|
||||
<p className="text-xs text-orange-600">
|
||||
Tu reserva quedará pendiente hasta el pago.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => setStep(1)}>
|
||||
Volver
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => setStep(3)}
|
||||
disabled={!validateStep2()}
|
||||
>
|
||||
Revisar Orden
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Confirmar Reserva</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">Información de Contacto</h3>
|
||||
<p className="text-sm">{customerInfo.firstName} {customerInfo.lastName}</p>
|
||||
<p className="text-sm">{customerInfo.email}</p>
|
||||
<p className="text-sm">{customerInfo.phone}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">Método de Pago</h3>
|
||||
<p className="text-sm">
|
||||
{paymentMethods.find(m => m.id === paymentInfo.method)?.name}
|
||||
{paymentInfo.method === 'credit_card' && paymentInfo.cardNumber &&
|
||||
` terminada en ${paymentInfo.cardNumber.slice(-4)}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{specialRequests && (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">Solicitudes Especiales</h3>
|
||||
<p className="text-sm">{specialRequests}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center p-4 bg-blue-50 rounded-lg">
|
||||
<Shield className="w-5 h-5 text-blue-600 mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-900">Pago Seguro</p>
|
||||
<p className="text-xs text-blue-700">
|
||||
Tu información está protegida con encriptación SSL
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => setStep(2)}>
|
||||
Volver
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={processPayment}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing ? 'Procesando...' : `Confirmar y Pagar $${finalTotal.toFixed(2)}`}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Order Summary Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-24">
|
||||
<CardHeader>
|
||||
<CardTitle>Resumen de Orden</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Cart Items */}
|
||||
<div className="space-y-3">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="flex space-x-3">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
className="w-16 h-16 object-cover rounded"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-sm truncate">{item.title}</h4>
|
||||
<p className="text-xs text-gray-500">{item.location}</p>
|
||||
{item.selectedDate && (
|
||||
<p className="text-xs text-gray-600">Fecha: {item.selectedDate}</p>
|
||||
)}
|
||||
{item.guests && (
|
||||
<p className="text-xs text-gray-600">Personas: {item.guests}</p>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<span className="text-xs text-gray-500">Qty: {item.quantity}</span>
|
||||
<span className="text-sm font-semibold">${(item.price * item.quantity).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Pricing Breakdown */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Subtotal</span>
|
||||
<span>${totalPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>ITBIS (18%)</span>
|
||||
<span>${taxes.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Fee de servicio (5%)</span>
|
||||
<span>${serviceFee.toFixed(2)}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span>${finalTotal.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 mt-4">
|
||||
<p>• Cancelación gratuita hasta 24 horas antes</p>
|
||||
<p>• Recibirás confirmación por email</p>
|
||||
<p>• Soporte al cliente 24/7</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkout;
|
||||
390
src/pages/Explore.tsx
Normal file
390
src/pages/Explore.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Star, MapPin, Phone, Grid, List, Search, Heart, Filter, X, Map } from 'lucide-react';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import MapView from '@/components/MapView';
|
||||
import { mockApi } from '@/services/mockApi';
|
||||
|
||||
export default function Explore() {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [listings, setListings] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedLocation, setSelectedLocation] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list' | 'map'>('grid');
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [priceRange, setPriceRange] = useState([0, 500]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [sortBy, setSortBy] = useState('latest');
|
||||
|
||||
const categories = [
|
||||
{ name: 'hotel', count: 3 },
|
||||
{ name: 'restaurant', count: 2 },
|
||||
{ name: 'tour', count: 1 },
|
||||
{ name: 'apartment', count: 2 },
|
||||
{ name: 'events/arts', count: 1 },
|
||||
{ name: 'shops', count: 3 },
|
||||
{ name: 'museum', count: 2 },
|
||||
{ name: 'gymnasiums', count: 1 }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize from URL parameters
|
||||
const searchParam = searchParams.get('search');
|
||||
const locationParam = searchParams.get('location');
|
||||
const categoryParam = params.category;
|
||||
|
||||
if (searchParam) setSearchQuery(searchParam);
|
||||
if (locationParam) setSelectedLocation(locationParam);
|
||||
if (categoryParam) {
|
||||
setSelectedCategories([categoryParam]);
|
||||
setSelectedCategory(categoryParam);
|
||||
}
|
||||
}, [searchParams, params]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchListings = async () => {
|
||||
try {
|
||||
let data = await mockApi.getListings() as any[];
|
||||
|
||||
// Filter by location if specified
|
||||
if (selectedLocation) {
|
||||
data = data.filter((listing: any) =>
|
||||
listing.location?.address?.toLowerCase().includes(selectedLocation.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
setListings(data as any[]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching listings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchListings();
|
||||
}, [selectedLocation]);
|
||||
|
||||
const handleCategoryToggle = (categoryName: string) => {
|
||||
setSelectedCategories(prev =>
|
||||
prev.includes(categoryName)
|
||||
? prev.filter(cat => cat !== categoryName)
|
||||
: [...prev, categoryName]
|
||||
);
|
||||
};
|
||||
|
||||
const handleListingClick = (listing: any) => {
|
||||
navigate(`/offer/${listing.id}`);
|
||||
};
|
||||
|
||||
const filteredListings = listings.filter((listing) => {
|
||||
const matchesSearch = !searchQuery ||
|
||||
listing.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
listing.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
listing.location?.address?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesCategory = selectedCategories.length === 0 ||
|
||||
selectedCategories.includes(listing.category.toLowerCase()) ||
|
||||
selectedCategories.includes(listing.category);
|
||||
const matchesPrice = listing.price >= priceRange[0] && listing.price <= priceRange[1];
|
||||
const matchesLocation = !selectedLocation ||
|
||||
listing.location?.address?.toLowerCase().includes(selectedLocation.toLowerCase());
|
||||
return matchesSearch && matchesCategory && matchesPrice && matchesLocation;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="map-wrapper flex">
|
||||
{/* Sidebar Filters */}
|
||||
<div className={`sidebar-filters bg-white border-r transition-transform duration-300 ${
|
||||
showFilters ? 'translate-x-0' : '-translate-x-full'
|
||||
} fixed md:relative md:translate-x-0 w-80 h-screen z-40 overflow-y-auto`}>
|
||||
{/* Filter Header - Mobile Only */}
|
||||
<div className="border-b flex justify-between items-center p-3 md:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowFilters(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-lg font-semibold">Filters</span>
|
||||
<Button variant="ghost" className="text-primary font-medium">
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{/* Price Filter */}
|
||||
<div className="mb-6 border-b pb-4">
|
||||
<div className="mb-3">
|
||||
<h4 className="text-lg font-semibold mb-1">Price Filter</h4>
|
||||
<p className="text-sm text-muted-foreground">Select min and max price range</p>
|
||||
</div>
|
||||
<div className="px-2">
|
||||
<Slider
|
||||
defaultValue={priceRange}
|
||||
max={500}
|
||||
min={0}
|
||||
step={10}
|
||||
onValueChange={setPriceRange}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-muted-foreground mt-2">
|
||||
<span>${priceRange[0]}</span>
|
||||
<span>${priceRange[1]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-6 border-b pb-4">
|
||||
<div className="mb-3">
|
||||
<h4 className="text-lg font-semibold mb-2">Categories</h4>
|
||||
<p className="text-sm text-muted-foreground">Select categories to filter listings</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{categories.map((category) => (
|
||||
<div key={category.name} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={category.name}
|
||||
checked={selectedCategories.includes(category.name)}
|
||||
onCheckedChange={() => handleCategoryToggle(category.name)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={category.name}
|
||||
className="text-sm font-medium cursor-pointer flex-1 capitalize"
|
||||
>
|
||||
{category.name}
|
||||
<span className="text-muted-foreground ml-1">({category.count})</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort By */}
|
||||
<div className="mb-6 border-b pb-4">
|
||||
<div className="mb-3">
|
||||
<h4 className="text-lg font-semibold mb-1">Order by</h4>
|
||||
<p className="text-sm text-muted-foreground">Sort listings by preference</p>
|
||||
</div>
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="latest">Latest</SelectItem>
|
||||
<SelectItem value="nearby">Nearby</SelectItem>
|
||||
<SelectItem value="top-rated">Top rated</SelectItem>
|
||||
<SelectItem value="random">Random</SelectItem>
|
||||
<SelectItem value="a-z">A-Z</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Apply Filters Button */}
|
||||
<Button className="w-full mb-2">Apply filters</Button>
|
||||
<Button variant="ghost" className="w-full text-center">
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="items-content flex-1 bg-gray-50 min-h-screen py-3 px-4">
|
||||
{/* Search Header */}
|
||||
{(searchQuery || selectedLocation || selectedCategory) && (
|
||||
<div className="mb-4 p-4 bg-white rounded-lg border">
|
||||
<h2 className="text-lg font-semibold mb-2">Search Results</h2>
|
||||
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
|
||||
{searchQuery && <span>Search: "{searchQuery}"</span>}
|
||||
{selectedLocation && <span>Location: {selectedLocation}</span>}
|
||||
{selectedCategory && <span>Category: {selectedCategory}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center mb-3 gap-2">
|
||||
<div className="flex-1 text-lg">
|
||||
All <span className="font-bold text-primary">{filteredListings.length}</span> listing{filteredListings.length !== 1 ? 's' : ''} found
|
||||
</div>
|
||||
|
||||
{/* Filter Toggle & View Mode */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="md:hidden"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
>
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="bg-white border rounded-lg p-1 shadow-sm">
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('grid')}
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('list')}
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'map' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('map')}
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<Map className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<p className="mt-4 text-muted-foreground">Loading listings...</p>
|
||||
</div>
|
||||
) : viewMode === 'map' ? (
|
||||
<MapView
|
||||
offers={filteredListings}
|
||||
onOfferClick={handleListingClick}
|
||||
/>
|
||||
) : (
|
||||
<div className={`grid gap-4 mb-5 ${
|
||||
viewMode === 'grid'
|
||||
? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'
|
||||
: 'grid-cols-1'
|
||||
}`}>
|
||||
{filteredListings.map((listing) => (
|
||||
<Card
|
||||
key={listing.id}
|
||||
className="bg-white rounded-lg border-0 shadow-sm hover:shadow-md transition-shadow cursor-pointer overflow-hidden"
|
||||
onClick={() => handleListingClick(listing)}
|
||||
>
|
||||
{/* Card Image */}
|
||||
<div className="relative overflow-hidden">
|
||||
<img
|
||||
src={listing.images[0]}
|
||||
alt={listing.title}
|
||||
className="w-full h-48 object-cover hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div className="absolute top-3 left-3 flex flex-col gap-1">
|
||||
<Badge className="bg-black/50 text-white backdrop-blur-sm">
|
||||
<Star className="h-3 w-3 mr-1" />
|
||||
Featured
|
||||
</Badge>
|
||||
<Badge className="bg-black/50 text-white backdrop-blur-sm">
|
||||
<span className="text-xs">${listing.price}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-3 right-3 bg-black/20 text-white hover:bg-black/30 backdrop-blur-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Handle bookmark
|
||||
}}
|
||||
>
|
||||
<Heart className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-3 relative">
|
||||
{/* Category Icon */}
|
||||
<div className="absolute -top-4 left-3 bg-primary text-white rounded-full w-8 h-8 flex items-center justify-center">
|
||||
<Star className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
{/* Rating */}
|
||||
<div className="flex items-center gap-2 text-primary mb-1 mt-2">
|
||||
<div className="flex">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`h-4 w-4 ${
|
||||
i < Math.floor(listing.rating || 4.5) ? 'fill-current' : 'text-muted'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="font-medium text-primary">
|
||||
<span className="font-semibold">({listing.rating || 4.5})</span>
|
||||
<span className="text-sm ml-1">2,391 reviews</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h4 className="font-semibold text-lg mb-0 flex items-center gap-2">
|
||||
{listing.title}
|
||||
<Badge variant="outline" className="text-green-600 border-green-600">
|
||||
✓
|
||||
</Badge>
|
||||
</h4>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray-600 text-sm mt-2 mb-2">{listing.description}</p>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="flex flex-wrap gap-3 mt-2">
|
||||
<a
|
||||
href="tel:(123) 456-7890"
|
||||
className="flex items-center gap-2 text-sm font-semibold text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
<span>(123) 456-7890</span>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center gap-2 text-sm font-semibold text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MapPin className="h-4 w-4" />
|
||||
<span>Directions</span>
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlay for mobile filters */}
|
||||
{showFilters && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-30 md:hidden"
|
||||
onClick={() => setShowFilters(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
299
src/pages/Index.tsx
Normal file
299
src/pages/Index.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ExploreSection from "@/components/ExploreSection";
|
||||
import PlacesSection from "@/components/PlacesSection";
|
||||
import BlogSection from "@/components/BlogSection";
|
||||
import Footer from "@/components/Footer";
|
||||
import { AIFloatingAssistant } from "@/components/AIFloatingAssistant";
|
||||
|
||||
const Index = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedLocation, setSelectedLocation] = useState("");
|
||||
|
||||
const handleSearch = () => {
|
||||
const params = new URLSearchParams();
|
||||
if (searchQuery) params.append('search', searchQuery);
|
||||
if (selectedLocation) params.append('location', selectedLocation);
|
||||
navigate(`/explore?${params.toString()}`);
|
||||
};
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
navigate(`/explore/${categoryName.toLowerCase()}`);
|
||||
};
|
||||
|
||||
const categories = [
|
||||
{ icon: "🏢", title: "Apartment", count: "99+" },
|
||||
{ icon: "🍽️", title: "Restaurant", count: "55+" },
|
||||
{ icon: "🎭", title: "Events/Arts", count: "55+" },
|
||||
{ icon: "🛍️", title: "Shops", count: "80+" },
|
||||
{ icon: "🏛️", title: "Museum", count: "96+" },
|
||||
{ icon: "💪", title: "Gymnasiums", count: "21+" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Custom Styles */}
|
||||
<style>{`
|
||||
.font-caveat {
|
||||
font-family: 'Caveat', cursive;
|
||||
}
|
||||
.text-span {
|
||||
text-decoration: underline;
|
||||
color: #fe8303;
|
||||
}
|
||||
.bg-blur {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.hero-header-classic {
|
||||
min-height: 100vh;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Navbar */}
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 bg-transparent">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<a className="flex-shrink-0" href="/">
|
||||
<img
|
||||
className="h-8"
|
||||
src="https://karibeo.com/desktop/assets/images/logo.png"
|
||||
alt="Karibeo"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div className="hidden lg:flex items-center space-x-8">
|
||||
<a className="text-white hover:text-orange-400 transition-colors" href="/">Home</a>
|
||||
<a className="text-white hover:text-orange-400 transition-colors" href="/explore">Explore</a>
|
||||
<a className="text-white hover:text-orange-400 transition-colors" href="/about">About</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<a href="/sign-in" className="text-white hover:text-orange-400 transition-colors">
|
||||
❤️
|
||||
</a>
|
||||
<a href="/sign-in" className="text-white hover:text-orange-400 transition-colors">
|
||||
👤
|
||||
</a>
|
||||
<a
|
||||
href="/dashboard/add-listing"
|
||||
className="hidden sm:flex items-center space-x-2 bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded-full transition-colors"
|
||||
>
|
||||
<span>➕</span>
|
||||
<span>Add Listing</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Header */}
|
||||
<div className="hero-header-classic flex items-center bg-gray-900 overflow-hidden relative pt-20">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{backgroundImage: 'url(https://karibeo.com/desktop/assets/images/8825.jpg)'}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50" />
|
||||
<div className="container mx-auto px-4 relative z-10 py-20">
|
||||
<div className="text-center text-white uppercase mb-3 text-sm font-medium">
|
||||
WE ARE #1 ON THE MARKET
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-white text-center mb-5">
|
||||
We're Here to Help You
|
||||
<br className="hidden lg:block" />
|
||||
<span className="font-caveat text-span">Navigate</span> While Traveling
|
||||
</h1>
|
||||
<div className="text-lg mb-8 text-center text-white max-w-2xl mx-auto">
|
||||
You'll get comprehensive results based on the provided location.
|
||||
</div>
|
||||
|
||||
{/* Search Form */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="w-full max-w-4xl">
|
||||
<div className="bg-blur rounded-2xl p-6 flex flex-col md:flex-row gap-4">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="text-white mr-3">🔍</div>
|
||||
<input
|
||||
type="text"
|
||||
className="bg-transparent text-white placeholder-white border-none outline-none flex-1"
|
||||
placeholder="What are you looking for?"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-px bg-white bg-opacity-20 hidden md:block" />
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="text-white mr-3">📍</div>
|
||||
<select
|
||||
className="bg-transparent text-white border-none outline-none flex-1"
|
||||
value={selectedLocation}
|
||||
onChange={(e) => setSelectedLocation(e.target.value)}
|
||||
>
|
||||
<option value="" className="text-gray-800">Location</option>
|
||||
<option value="florence" className="text-gray-800">Florence, Italy</option>
|
||||
<option value="rome" className="text-gray-800">Rome, Italy</option>
|
||||
<option value="milan" className="text-gray-800">Milan, Italy</option>
|
||||
<option value="santo-domingo" className="text-gray-800">Santo Domingo, DR</option>
|
||||
<option value="punta-cana" className="text-gray-800">Punta Cana, DR</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white px-6 py-3 rounded-full transition-colors"
|
||||
onClick={handleSearch}
|
||||
>
|
||||
Search places
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Cards */}
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-6xl">
|
||||
<div className="flex overflow-x-auto space-x-4 p-4">
|
||||
{categories.map((category, index) => (
|
||||
<div key={index} className="flex-shrink-0 w-72">
|
||||
<div className="bg-blur rounded-3xl p-4 flex items-center shadow-lg hover:bg-white hover:bg-opacity-20 transition-all duration-300">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="bg-black bg-opacity-50 rounded-full w-12 h-12 flex items-center justify-center text-2xl">
|
||||
{category.icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
<button
|
||||
onClick={() => handleCategoryClick(category.title)}
|
||||
className="hover:text-orange-400 transition-colors text-left"
|
||||
>
|
||||
{category.title}
|
||||
</button>
|
||||
</h3>
|
||||
<p className="text-sm text-gray-300">{category.count} listings</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Process Section */}
|
||||
<div className="py-20 border-t border-gray-200 relative overflow-hidden bg-white">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="max-w-4xl text-center">
|
||||
<div className="mb-12">
|
||||
<div className="font-caveat text-4xl font-medium text-orange-500 mb-4">Best Way</div>
|
||||
<h2 className="text-4xl md:text-5xl font-semibold mb-6">
|
||||
Find Your Dream Place The Best Way
|
||||
</h2>
|
||||
<div className="text-lg text-gray-600">
|
||||
Discover exciting categories.
|
||||
<span className="text-orange-500 font-semibold"> Find what you're looking for.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 bg-orange-500 text-white font-caveat text-4xl flex items-center justify-center mx-auto mb-6 rounded-full">
|
||||
1
|
||||
</div>
|
||||
<h4 className="text-xl font-semibold mb-4">
|
||||
Input your location to start looking for landmarks.
|
||||
</h4>
|
||||
<p className="text-gray-600">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pharetra vitae quam integer semper.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 bg-orange-500 text-white font-caveat text-4xl flex items-center justify-center mx-auto mb-6 rounded-full">
|
||||
2
|
||||
</div>
|
||||
<h4 className="text-xl font-semibold mb-4">
|
||||
Make an appointment at the place you want to visit.
|
||||
</h4>
|
||||
<p className="text-gray-600">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pharetra vitae quam integer.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 bg-orange-500 text-white font-caveat text-4xl flex items-center justify-center mx-auto mb-6 rounded-full">
|
||||
3
|
||||
</div>
|
||||
<h4 className="text-xl font-semibold mb-4">
|
||||
Visit the place and enjoy the experience.
|
||||
</h4>
|
||||
<p className="text-gray-600">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pharetra vitae quam integer aenean.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Use the new ExploreSection component */}
|
||||
<ExploreSection />
|
||||
|
||||
{/* Add the new PlacesSection component */}
|
||||
<PlacesSection />
|
||||
|
||||
{/* Blog Section */}
|
||||
<BlogSection />
|
||||
|
||||
{/* AI Section */}
|
||||
<div className="py-20 bg-orange-500 text-white relative overflow-hidden">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h2 className="text-4xl md:text-5xl font-semibold mb-8">
|
||||
Discover Your Ideal Vacation with
|
||||
<br />
|
||||
<span className="font-caveat text-yellow-300">AI-Powered Precision</span>
|
||||
</h2>
|
||||
<img
|
||||
src="https://img.freepik.com/foto-gratis/vista-superior-mano-sosteniendo-telefono-inteligente_23-2149617681.jpg"
|
||||
alt="AI Technology"
|
||||
className="w-full h-96 rounded-2xl object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<p className="text-xl mb-6">
|
||||
Ready for an unforgettable getaway? Our AI tool tailors your perfect trip by matching you with top destinations, exclusive offers, and personalized discounts based on your interests.
|
||||
</p>
|
||||
<ul className="space-y-4 text-lg mb-8">
|
||||
<li>• Uncover trending businesses, hidden gems, and iconic landmarks near you.</li>
|
||||
<li>• Get custom vacation spot recommendations that suit your unique preferences.</li>
|
||||
<li>• Explore a variety of locations, from must-see attractions to local hotspots.</li>
|
||||
<li>• Unlock special deals and diverse options to plan your dream trip effortlessly.</li>
|
||||
</ul>
|
||||
<a
|
||||
href="/contact"
|
||||
className="bg-white text-orange-500 px-6 py-3 rounded-lg inline-flex items-center hover:bg-gray-100 transition-colors w-fit"
|
||||
>
|
||||
🚀 Start Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
|
||||
{/* AI Floating Assistant */}
|
||||
<AIFloatingAssistant />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
285
src/pages/ListingDetails.tsx
Normal file
285
src/pages/ListingDetails.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
Star,
|
||||
Heart,
|
||||
Phone,
|
||||
MapPin,
|
||||
ExternalLink,
|
||||
Expand,
|
||||
Calendar,
|
||||
User,
|
||||
Video,
|
||||
Fan,
|
||||
Waves,
|
||||
Wifi,
|
||||
Car,
|
||||
Utensils,
|
||||
Shield,
|
||||
Coffee
|
||||
} from 'lucide-react';
|
||||
|
||||
export function ListingDetails() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { listing } = location.state || {};
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
|
||||
if (!listing) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Listing not found</h1>
|
||||
<Button onClick={() => navigate('/explore')}>
|
||||
Back to Explore
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const amenities = [
|
||||
{ icon: Video, name: "Security cameras" },
|
||||
{ icon: Fan, name: "Garden" },
|
||||
{ icon: Waves, name: "Jacuzzi" },
|
||||
{ icon: Wifi, name: "Free WiFi" },
|
||||
{ icon: Car, name: "Parking" },
|
||||
{ icon: Utensils, name: "Restaurant" },
|
||||
{ icon: Shield, name: "24/7 Security" },
|
||||
{ icon: Coffee, name: "Breakfast" }
|
||||
];
|
||||
|
||||
const galleryImages = [
|
||||
"https://themes.easital.com/html/liston/v2.3/assets/images/listing-details/gallery/08.jpg",
|
||||
"https://themes.easital.com/html/liston/v2.3/assets/images/listing-details/gallery/09.jpg",
|
||||
"https://themes.easital.com/html/liston/v2.3/assets/images/listing-details/gallery/10.jpg"
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
{/* Details Header */}
|
||||
<div className="py-6 bg-white border-b">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-3xl font-semibold mb-2">
|
||||
{listing.title || 'Chuijhal Hotel And Restaurant'}
|
||||
</h1>
|
||||
<ul className="flex flex-wrap items-center gap-4 mb-2 text-sm">
|
||||
<li>
|
||||
<a href="#" className="font-medium text-primary flex items-center gap-1">
|
||||
{listing.location || 'Chuijhal'}
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex text-primary">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`h-4 w-4 ${
|
||||
i < Math.floor(listing.rating || 4.5) ? 'fill-current' : 'text-muted'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="font-medium text-primary">
|
||||
<span className="text-base font-semibold">({listing.rating || 4.5})</span>
|
||||
<small className="ml-1">{listing.reviews || '2,391'} reviews</small>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground font-medium">
|
||||
<li>Posted 7 hours ago</li>
|
||||
<li>1123 Fictional St, San Francisco</li>
|
||||
<li>Full time</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="lg:text-right">
|
||||
<div className="mb-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isBookmarked}
|
||||
onChange={(e) => setIsBookmarked(e.target.checked)}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant={isBookmarked ? "default" : "outline"}
|
||||
className="gap-2"
|
||||
>
|
||||
<Heart className={`h-4 w-4 ${isBookmarked ? 'fill-current' : ''}`} />
|
||||
{isBookmarked ? 'Saved' : 'Save this listing'}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
46 people bookmarked this place
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gallery */}
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 h-96">
|
||||
<div className="md:col-span-3 relative">
|
||||
<img
|
||||
src={listing.image || galleryImages[0]}
|
||||
alt="Main gallery image"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="absolute bottom-3 right-3 md:hidden gap-2"
|
||||
>
|
||||
<Expand className="h-4 w-4" />
|
||||
View photos
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hidden md:flex md:flex-col gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<img
|
||||
src={galleryImages[1]}
|
||||
alt="Gallery image"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<img
|
||||
src={galleryImages[2]}
|
||||
alt="Gallery image"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="absolute bottom-3 right-3 gap-2"
|
||||
>
|
||||
<Expand className="h-4 w-4" />
|
||||
View photos
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-2">
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold text-gray-800">Published:</span>
|
||||
<span className="ml-2 text-muted-foreground">November 21, 2023</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2">
|
||||
{/* Description */}
|
||||
<div className="mb-8">
|
||||
<h4 className="text-2xl font-semibold mb-4">
|
||||
Latest Property <span className="text-primary">Reviews</span>
|
||||
</h4>
|
||||
<div className="prose max-w-none">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
|
||||
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
|
||||
when an unknown printer took a galley of type and scrambled it to make a type
|
||||
specimen book. It is a long established fact that a reader will be distracted
|
||||
by the readable content of a page when looking at its layout.
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
It has survived not only five centuries, but also the leap into electronic
|
||||
typesetting, remaining essentially unchanged. It was popularised in the 1960s
|
||||
with the release of Letraset sheets containing Lorem Ipsum passages, and more
|
||||
recently with desktop publishing software like Aldus PageMaker including
|
||||
versions of Lorem Ipsum.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amenities */}
|
||||
<div className="mb-8">
|
||||
<h4 className="text-2xl font-semibold mb-4">
|
||||
Amenities <span className="text-primary">Available</span>
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{amenities.map((amenity, index) => (
|
||||
<div key={index} className="flex items-center gap-3 text-gray-800">
|
||||
<div className="flex-shrink-0">
|
||||
<amenity.icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div className="font-medium">{amenity.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-4">
|
||||
<CardContent className="p-6">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-3xl font-bold text-primary mb-2">
|
||||
${listing.priceRange?.min || 12} - ${listing.priceRange?.max || 40}
|
||||
</div>
|
||||
<p className="text-muted-foreground">per night</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<Button className="w-full" size="lg">
|
||||
<Calendar className="h-4 w-4 mr-2" />
|
||||
Book Now
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" size="lg">
|
||||
<Phone className="h-4 w-4 mr-2" />
|
||||
Call Now
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" size="lg">
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
Contact Owner
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Response rate:</span>
|
||||
<span className="font-medium">100%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Response time:</span>
|
||||
<span className="font-medium">Within an hour</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Last seen:</span>
|
||||
<span className="font-medium">Online now</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/pages/NotFound.tsx
Normal file
27
src/pages/NotFound.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const NotFound = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
console.error(
|
||||
"404 Error: User attempted to access non-existent route:",
|
||||
location.pathname
|
||||
);
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">404</h1>
|
||||
<p className="text-xl text-gray-600 mb-4">Oops! Page not found</p>
|
||||
<a href="/" className="text-blue-500 hover:text-blue-700 underline">
|
||||
Return to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
184
src/pages/OfferDetails.tsx
Normal file
184
src/pages/OfferDetails.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { mockListings } from '@/services/mockApi';
|
||||
import { Star, Heart, MapPin, Shield, Users, Wifi, Car } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import { useCart } from '@/contexts/CartContext';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import CartSidebar from '@/components/CartSidebar';
|
||||
import BookingSidebar from '@/components/BookingSidebar';
|
||||
|
||||
const OfferDetails = () => {
|
||||
const { slug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { addToCart } = useCart();
|
||||
const { toast } = useToast();
|
||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||
|
||||
// Find offer by slug
|
||||
const offer = mockListings.find(listing => listing.id === slug);
|
||||
|
||||
if (!offer) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Oferta no encontrada</h1>
|
||||
<Button onClick={() => navigate('/explore')}>Volver a explorar</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const amenities = [
|
||||
{ icon: <Wifi className="w-5 h-5" />, name: "WiFi Gratis" },
|
||||
{ icon: <Car className="w-5 h-5" />, name: "Estacionamiento" },
|
||||
{ icon: <Users className="w-5 h-5" />, name: "Accesible" },
|
||||
{ icon: <Shield className="w-5 h-5" />, name: "Seguro" },
|
||||
];
|
||||
|
||||
const galleryImages = [
|
||||
offer.images[0],
|
||||
"https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=400",
|
||||
"https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=400",
|
||||
"https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=400",
|
||||
];
|
||||
|
||||
const handleAddToCart = (date: Date | undefined, guests: number, timeSlot?: string) => {
|
||||
const cartItem = {
|
||||
id: offer.id,
|
||||
title: offer.title,
|
||||
price: offer.price,
|
||||
image: offer.images[0],
|
||||
category: offer.category,
|
||||
location: offer.location.address,
|
||||
selectedDate: date?.toISOString().split('T')[0] || '',
|
||||
guests: guests || undefined,
|
||||
timeSlot
|
||||
};
|
||||
|
||||
addToCart(cartItem);
|
||||
toast({
|
||||
title: "¡Agregado al carrito!",
|
||||
description: `${offer.title} se ha agregado a tu carrito.`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleBookNow = (date: Date | undefined, guests: number, timeSlot?: string) => {
|
||||
handleAddToCart(date, guests, timeSlot);
|
||||
navigate('/checkout');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-12">
|
||||
{/* Header with Cart */}
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{offer.title}</h1>
|
||||
<div className="flex items-center text-gray-600 mb-2">
|
||||
<MapPin className="w-4 h-4 mr-1" />
|
||||
<span>{offer.location.address}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center">
|
||||
<Star className="w-5 h-5 fill-yellow-400 text-yellow-400 mr-1" />
|
||||
<span className="font-semibold">{offer.rating}</span>
|
||||
<span className="text-gray-600 ml-1">(124 reviews)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 md:mt-0 flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsBookmarked(!isBookmarked)}
|
||||
>
|
||||
<Heart className={`w-4 h-4 mr-2 ${isBookmarked ? 'fill-red-500 text-red-500' : ''}`} />
|
||||
Guardar
|
||||
</Button>
|
||||
<CartSidebar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gallery */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<div className="md:col-span-2">
|
||||
<img
|
||||
src={galleryImages[0]}
|
||||
alt={offer.title}
|
||||
className="w-full h-96 object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 md:col-span-2">
|
||||
{galleryImages.slice(1).map((image, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={image}
|
||||
alt={`${offer.title} ${index + 2}`}
|
||||
className="w-full h-32 md:h-[188px] object-cover rounded-lg"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2">
|
||||
{/* Description */}
|
||||
<div className="bg-white rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Descripción</h2>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
{offer.description}. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
|
||||
minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
|
||||
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit
|
||||
esse cillum dolore eu fugiat nulla pariatur.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Amenities */}
|
||||
<div className="bg-white rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Servicios disponibles</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{amenities.map((amenity, index) => (
|
||||
<div key={index} className="flex items-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-primary mr-3">{amenity.icon}</div>
|
||||
<span className="text-sm font-medium">{amenity.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div className="bg-white rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Ubicación</h2>
|
||||
<div className="bg-gray-200 h-64 rounded-lg flex items-center justify-center">
|
||||
<p className="text-gray-600">Mapa interactivo - {offer.location.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Booking Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<BookingSidebar
|
||||
offer={offer}
|
||||
onBookNow={handleBookNow}
|
||||
onAddToCart={handleAddToCart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OfferDetails;
|
||||
257
src/pages/OrderConfirmation.tsx
Normal file
257
src/pages/OrderConfirmation.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { CheckCircle, Download, Mail, Calendar, MapPin, Users } from 'lucide-react';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
const OrderConfirmation = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const orderData = location.state?.orderData;
|
||||
|
||||
if (!orderData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-12">
|
||||
<div className="text-center py-12">
|
||||
<h1 className="text-2xl font-bold mb-4">Orden no encontrada</h1>
|
||||
<Button onClick={() => navigate('/explore')}>
|
||||
Volver a Explorar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const downloadReceipt = () => {
|
||||
const receiptData = {
|
||||
...orderData,
|
||||
downloadDate: new Date().toISOString()
|
||||
};
|
||||
|
||||
const dataStr = JSON.stringify(receiptData, null, 2);
|
||||
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||||
|
||||
const exportFileDefaultName = `karibeo-receipt-${orderData.id}.json`;
|
||||
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', dataUri);
|
||||
linkElement.setAttribute('download', exportFileDefaultName);
|
||||
linkElement.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-12">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Success Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
¡Reserva Confirmada!
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Tu reserva ha sido procesada exitosamente. Recibirás un email de confirmación en breve.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Order Details */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5" />
|
||||
Detalles de la Reserva
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<span className="text-sm text-gray-600">Número de Orden:</span>
|
||||
<p className="font-semibold">#{orderData.id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-600">Fecha de Reserva:</span>
|
||||
<p className="font-semibold">
|
||||
{new Date(orderData.date).toLocaleDateString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-600">Estado:</span>
|
||||
<Badge className="ml-2 bg-green-100 text-green-800">
|
||||
{orderData.status === 'confirmed' ? 'Confirmada' : orderData.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Mail className="w-5 h-5" />
|
||||
Información de Contacto
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<span className="text-sm text-gray-600">Nombre:</span>
|
||||
<p className="font-semibold">
|
||||
{orderData.customerInfo.firstName} {orderData.customerInfo.lastName}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-600">Email:</span>
|
||||
<p className="font-semibold">{orderData.customerInfo.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-600">Teléfono:</span>
|
||||
<p className="font-semibold">{orderData.customerInfo.phone}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Reserved Items */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Reservas Confirmadas</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{orderData.items.map((item: any, index: number) => (
|
||||
<div key={index} className="flex space-x-4 p-4 bg-gray-50 rounded-lg">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
className="w-20 h-20 object-cover rounded"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold">{item.title}</h4>
|
||||
<div className="flex items-center text-gray-600 text-sm mt-1">
|
||||
<MapPin className="w-4 h-4 mr-1" />
|
||||
{item.location}
|
||||
</div>
|
||||
{item.selectedDate && (
|
||||
<div className="flex items-center text-gray-600 text-sm mt-1">
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
Fecha: {item.selectedDate}
|
||||
</div>
|
||||
)}
|
||||
{item.guests && (
|
||||
<div className="flex items-center text-gray-600 text-sm mt-1">
|
||||
<Users className="w-4 h-4 mr-1" />
|
||||
{item.guests} personas
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
Cantidad: {item.quantity}
|
||||
</span>
|
||||
<span className="font-semibold text-primary">
|
||||
${(item.price * item.quantity).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Payment Summary */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Resumen de Pago</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal:</span>
|
||||
<span>${orderData.pricing.subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>ITBIS (18%):</span>
|
||||
<span>${orderData.pricing.taxes.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Fee de servicio (5%):</span>
|
||||
<span>${orderData.pricing.serviceFee.toFixed(2)}</span>
|
||||
</div>
|
||||
<hr className="my-2" />
|
||||
<div className="flex justify-between font-semibold text-lg">
|
||||
<span>Total Pagado:</span>
|
||||
<span className="text-primary">${orderData.pricing.total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center text-blue-800">
|
||||
<CheckCircle className="w-5 h-5 mr-2" />
|
||||
<span className="font-medium">
|
||||
Pago procesado via {orderData.paymentInfo.method === 'credit_card' ? 'Tarjeta de Crédito' : orderData.paymentInfo.method}
|
||||
</span>
|
||||
</div>
|
||||
{orderData.paymentInfo.cardLast4 && (
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
Tarjeta terminada en {orderData.paymentInfo.cardLast4}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Special Requests */}
|
||||
{orderData.specialRequests && (
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Solicitudes Especiales</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-700">{orderData.specialRequests}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button onClick={downloadReceipt} variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Descargar Recibo
|
||||
</Button>
|
||||
<Button onClick={() => navigate('/explore')}>
|
||||
Explorar Más Ofertas
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Important Information */}
|
||||
<div className="mt-8 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<h3 className="font-semibold text-yellow-800 mb-2">Información Importante:</h3>
|
||||
<ul className="text-sm text-yellow-700 space-y-1">
|
||||
<li>• Recibirás un email de confirmación en los próximos 5 minutos</li>
|
||||
<li>• Política de cancelación: Gratuita hasta 24 horas antes</li>
|
||||
<li>• Para cambios o consultas, contacta al +1 (809) 123-4567</li>
|
||||
<li>• Guarda este número de orden para futuras referencias: #{orderData.id}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderConfirmation;
|
||||
201
src/pages/SignIn.tsx
Normal file
201
src/pages/SignIn.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
import { Apple, Eye, EyeOff } from 'lucide-react';
|
||||
import { FaGoogle } from 'react-icons/fa';
|
||||
|
||||
const SignIn = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { login, user } = useAuth();
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
// Decide destination based on role
|
||||
const cached = localStorage.getItem('karibeo-user');
|
||||
const u = user ?? (cached ? JSON.parse(cached) : null);
|
||||
const role = u?.role;
|
||||
if (role === 'super_admin' || role === 'admin') {
|
||||
navigate('/dashboard/admin');
|
||||
} else {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSocialLogin = (provider: string) => {
|
||||
// Simulate social login
|
||||
console.log(`Login with ${provider}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex">
|
||||
{/* Left Side - Form */}
|
||||
<div className="flex-1 flex items-center justify-center p-8 bg-white">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
{/* Header */}
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{t('welcomeBack')} <span className="text-primary italic">{t('signIn')}</span> {t('toContinue')}
|
||||
</h1>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{t('unlockContent')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Social Login Buttons */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSocialLogin('apple')}
|
||||
className="w-full h-12 bg-gray-900 text-white hover:bg-gray-800 border-gray-900"
|
||||
>
|
||||
<Apple className="w-5 h-5 mr-3" />
|
||||
{t('signUpWithApple')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSocialLogin('google')}
|
||||
className="w-full h-12 bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
<FaGoogle className="w-5 h-5 mr-3" />
|
||||
{t('signUpWithGoogle')}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
{t('privacyNotice')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-4 bg-white text-gray-500">Or</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-red-50 text-red-500 p-3 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('enterEmail')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={t('enterValidEmail')}
|
||||
className="h-12"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('password')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('enterPassword')}
|
||||
className="h-12 pr-12"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="remember"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary"
|
||||
/>
|
||||
<label htmlFor="remember" className="ml-2 text-sm text-gray-700">
|
||||
{t('rememberMe')} next time
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full h-12 bg-primary hover:bg-primary-dark text-white font-semibold"
|
||||
>
|
||||
{isLoading ? t('loading') : t('signIn')}
|
||||
</Button>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-sm text-gray-600">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/sign-up" className="text-primary hover:underline font-medium">
|
||||
{t('signUp')}
|
||||
</Link>
|
||||
</p>
|
||||
<Link to="/forgot-password" className="text-sm text-primary hover:underline">
|
||||
Remind {t('password')}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Image & Content */}
|
||||
<div className="hidden lg:flex flex-1 bg-gray-100 items-center justify-center p-8">
|
||||
<div className="text-center max-w-md">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4 leading-tight">
|
||||
Effortlessly organize your workspace with ease.
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
|
||||
</p>
|
||||
<div className="bg-gray-300 rounded-lg h-64 flex items-center justify-center">
|
||||
<span className="text-6xl font-bold text-gray-500">793x552</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
280
src/pages/SignUp.tsx
Normal file
280
src/pages/SignUp.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
import { Apple, Eye, EyeOff } from 'lucide-react';
|
||||
import { FaGoogle } from 'react-icons/fa';
|
||||
|
||||
const SignUp = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
userType: 'tourist' as 'tourist' | 'business'
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { register } = useAuth();
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError('Las contraseñas no coinciden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!agreeToTerms) {
|
||||
setError('Debes aceptar los términos de servicio');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await register({
|
||||
name: formData.fullName,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
type: formData.userType,
|
||||
location: { lat: 18.4861, lng: -69.9312 }, // Default to Santo Domingo
|
||||
preferences: { language: 'es' }
|
||||
});
|
||||
navigate('/dashboard');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSocialLogin = (provider: string) => {
|
||||
console.log(`Register with ${provider}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex">
|
||||
{/* Left Side - Form */}
|
||||
<div className="flex-1 flex items-center justify-center p-8 bg-white">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
{/* Header */}
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{t('welcomeSignUp')} <span className="text-primary italic">{t('signUp')}</span> {t('toContinue')}
|
||||
</h1>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
{t('unlockContent')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Social Login Buttons */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSocialLogin('apple')}
|
||||
className="w-full h-12 bg-gray-900 text-white hover:bg-gray-800 border-gray-900"
|
||||
>
|
||||
<Apple className="w-5 h-5 mr-3" />
|
||||
{t('signUpWithApple')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleSocialLogin('google')}
|
||||
className="w-full h-12 bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
<FaGoogle className="w-5 h-5 mr-3" />
|
||||
{t('signUpWithGoogle')}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
{t('privacyNotice')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-4 bg-white text-gray-500">Or</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Registration Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-red-50 text-red-500 p-3 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('fullName')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="fullName"
|
||||
value={formData.fullName}
|
||||
onChange={handleChange}
|
||||
placeholder="Ingresa tu nombre completo"
|
||||
className="h-12"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Tipo de Usuario <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
name="userType"
|
||||
value={formData.userType}
|
||||
onChange={handleChange}
|
||||
className="w-full h-12 px-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
|
||||
required
|
||||
>
|
||||
<option value="tourist">Turista</option>
|
||||
<option value="business">Comercio</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('enterEmail')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
placeholder={t('enterValidEmail')}
|
||||
className="h-12"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('password')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
placeholder={t('enterPassword')}
|
||||
className="h-12 pr-12"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('confirmPassword')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
placeholder="Confirma tu contraseña"
|
||||
className="h-12 pr-12"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="terms"
|
||||
checked={agreeToTerms}
|
||||
onChange={(e) => setAgreeToTerms(e.target.checked)}
|
||||
className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary mt-1"
|
||||
/>
|
||||
<label htmlFor="terms" className="ml-2 text-sm text-gray-700">
|
||||
By signing up, you agree to the{' '}
|
||||
<Link to="/terms" className="text-primary hover:underline">
|
||||
terms of service
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full h-12 bg-primary hover:bg-primary-dark text-white font-semibold"
|
||||
>
|
||||
{isLoading ? t('loading') : t('signUp')}
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
Already have an account?{' '}
|
||||
<Link to="/sign-in" className="text-primary hover:underline font-medium">
|
||||
{t('signIn')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Image & Content */}
|
||||
<div className="hidden lg:flex flex-1 bg-gray-100 items-center justify-center p-8">
|
||||
<div className="text-center max-w-md">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4 leading-tight">
|
||||
Effortlessly organize your workspace with ease.
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-8">
|
||||
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
|
||||
</p>
|
||||
<div className="bg-gray-300 rounded-lg h-64 flex items-center justify-center">
|
||||
<span className="text-6xl font-bold text-gray-500">698x609</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUp;
|
||||
324
src/pages/dashboard/AddListing.tsx
Normal file
324
src/pages/dashboard/AddListing.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { mockApi } from '@/services/mockApi';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { ImagePlus, MapPin, DollarSign, Tag } from 'lucide-react';
|
||||
|
||||
const AddListing = () => {
|
||||
const { user } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
category: 'hotel',
|
||||
description: '',
|
||||
price: '',
|
||||
location: '',
|
||||
amenities: '',
|
||||
image: ''
|
||||
});
|
||||
|
||||
const categories = [
|
||||
{ value: 'hotel', label: 'Hotel' },
|
||||
{ value: 'restaurant', label: 'Restaurant' },
|
||||
{ value: 'tour', label: 'Tour Guide' },
|
||||
{ value: 'shop', label: 'Shop' },
|
||||
{ value: 'museum', label: 'Museum' },
|
||||
{ value: 'transport', label: 'Transport' }
|
||||
];
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const amenitiesArray = formData.amenities.split(',').map(a => a.trim()).filter(a => a);
|
||||
|
||||
await mockApi.createListing({
|
||||
...formData,
|
||||
price: parseFloat(formData.price),
|
||||
amenities: amenitiesArray,
|
||||
location: {
|
||||
lat: 18.4861 + (Math.random() - 0.5) * 0.01,
|
||||
lng: -69.9312 + (Math.random() - 0.5) * 0.01,
|
||||
address: formData.location
|
||||
},
|
||||
ownerId: user?.id
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
title: '',
|
||||
category: 'hotel',
|
||||
description: '',
|
||||
price: '',
|
||||
location: '',
|
||||
amenities: '',
|
||||
image: ''
|
||||
});
|
||||
|
||||
alert('Listing created successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error creating listing:', error);
|
||||
alert('Error creating listing');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
{/* Page Header */}
|
||||
<div className="mb-8">
|
||||
<div className="text-center mb-4">
|
||||
<div className="text-2xl font-caveat text-primary mb-2">Add New</div>
|
||||
<h2 className="text-4xl font-bold text-foreground mb-4">Create Listing</h2>
|
||||
<p className="text-muted-foreground">Fill in the information below to create your new listing</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-4 gap-6 max-w-7xl mx-auto">
|
||||
{/* Main Form */}
|
||||
<div className="xl:col-span-3">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Basic Information Card */}
|
||||
<Card className="backdrop-blur-sm bg-card/50 border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Tag size={20} />
|
||||
Basic Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">
|
||||
Listing Title <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter listing title"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">
|
||||
Category <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<select
|
||||
name="category"
|
||||
value={formData.category}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border border-border rounded-md bg-background text-foreground"
|
||||
required
|
||||
>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat.value} value={cat.value}>
|
||||
{cat.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">
|
||||
Description <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Describe your listing in detail..."
|
||||
rows={4}
|
||||
className="w-full p-2 border border-border rounded-md bg-background text-foreground resize-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Location & Pricing Card */}
|
||||
<Card className="backdrop-blur-sm bg-card/50 border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MapPin size={20} />
|
||||
Location & Pricing
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">
|
||||
Location <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="location"
|
||||
value={formData.location}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter full address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">
|
||||
<DollarSign className="inline mr-1" size={16} />
|
||||
Price (USD) <span className="text-destructive">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
name="price"
|
||||
value={formData.price}
|
||||
onChange={handleChange}
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">
|
||||
Amenities & Features
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="amenities"
|
||||
value={formData.amenities}
|
||||
onChange={handleChange}
|
||||
placeholder="WiFi, Parking, Pool, Restaurant, Air Conditioning..."
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Separate multiple amenities with commas</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Image Upload Card */}
|
||||
<Card className="backdrop-blur-sm bg-card/50 border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ImagePlus size={20} />
|
||||
Images
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border-2 border-dashed border-primary rounded-lg p-8 text-center bg-primary/5">
|
||||
<ImagePlus size={48} className="mx-auto text-muted-foreground mb-3" />
|
||||
<h6 className="mb-2 font-medium">Upload Images</h6>
|
||||
<p className="text-muted-foreground mb-3">Drag & drop images here or paste URL below</p>
|
||||
<Input
|
||||
type="url"
|
||||
name="image"
|
||||
value={formData.image}
|
||||
onChange={handleChange}
|
||||
placeholder="Paste image URL here"
|
||||
className="max-w-md mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></span>
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Create Listing
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="xl:col-span-1 space-y-6">
|
||||
<Card className="backdrop-blur-sm bg-card/50 border-border">
|
||||
<CardHeader>
|
||||
<CardTitle>Publishing Tips</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-primary rounded-full p-2 flex items-center justify-center min-w-[32px] h-8">
|
||||
<ImagePlus size={14} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="font-medium mb-1">High-Quality Images</h6>
|
||||
<p className="text-sm text-muted-foreground">Upload clear, high-resolution images to attract more customers</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-primary rounded-full p-2 flex items-center justify-center min-w-[32px] h-8">
|
||||
<Tag size={14} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="font-medium mb-1">Detailed Description</h6>
|
||||
<p className="text-sm text-muted-foreground">Provide comprehensive details about your services and amenities</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-primary rounded-full p-2 flex items-center justify-center min-w-[32px] h-8">
|
||||
<MapPin size={14} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="font-medium mb-1">Accurate Location</h6>
|
||||
<p className="text-sm text-muted-foreground">Include precise address for better discoverability</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="backdrop-blur-sm bg-card/50 border-border">
|
||||
<CardContent className="p-6 text-center">
|
||||
<img
|
||||
src="https://img.freepik.com/foto-gratis/vista-superior-mano-sosteniendo-telefono-inteligente_23-2149617681.jpg"
|
||||
alt="Mobile app"
|
||||
className="w-full h-48 object-cover rounded-lg mb-3"
|
||||
/>
|
||||
<h6 className="font-medium mb-2">Need Help?</h6>
|
||||
<p className="text-sm text-muted-foreground mb-3">Contact our support team for assistance with your listing</p>
|
||||
<Button variant="outline" size="sm">
|
||||
Get Support
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddListing;
|
||||
233
src/pages/dashboard/AdminDashboard.tsx
Normal file
233
src/pages/dashboard/AdminDashboard.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useAdminData } from '@/hooks/useAdminData';
|
||||
import {
|
||||
Users,
|
||||
DollarSign,
|
||||
Calendar,
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Shield,
|
||||
Settings,
|
||||
BarChart3,
|
||||
FileText,
|
||||
CreditCard,
|
||||
MapPin,
|
||||
Phone,
|
||||
Bell,
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
Eye,
|
||||
Edit,
|
||||
Trash2,
|
||||
Check,
|
||||
X,
|
||||
Plus,
|
||||
Building,
|
||||
Navigation,
|
||||
HeadphonesIcon,
|
||||
Globe,
|
||||
Wifi,
|
||||
MessageSquare,
|
||||
TrendingUp,
|
||||
Star,
|
||||
Zap,
|
||||
UserCheck,
|
||||
Clock,
|
||||
RefreshCw,
|
||||
Send,
|
||||
Target,
|
||||
PieChart,
|
||||
BarChart,
|
||||
Hotel,
|
||||
UtensilsCrossed,
|
||||
Car,
|
||||
Store
|
||||
} from 'lucide-react';
|
||||
|
||||
// Import tab components
|
||||
import OverviewTab from '@/components/admin/OverviewTab';
|
||||
import UsersTab from '@/components/admin/UsersTab';
|
||||
import ServicesTab from '@/components/admin/ServicesTab';
|
||||
import FinancialTab from '@/components/admin/FinancialTab';
|
||||
import ContentTab from '@/components/admin/ContentTab';
|
||||
import EmergencyTab from '@/components/admin/EmergencyTab';
|
||||
import SupportTab from '@/components/admin/SupportTab';
|
||||
import ConfigTab from '@/components/admin/ConfigTab';
|
||||
import GeolocationTab from '@/components/admin/GeolocationTab';
|
||||
|
||||
const AdminDashboard = () => {
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
stats,
|
||||
users,
|
||||
destinations,
|
||||
places,
|
||||
establishments,
|
||||
incidents,
|
||||
reviews,
|
||||
loading,
|
||||
error,
|
||||
isAdmin,
|
||||
isSuperAdmin,
|
||||
refreshData,
|
||||
loadUsers,
|
||||
loadDestinations,
|
||||
loadPlaces,
|
||||
loadEstablishments,
|
||||
loadIncidents,
|
||||
loadReviews,
|
||||
createDestination,
|
||||
updateDestination,
|
||||
deleteDestination,
|
||||
createPlace,
|
||||
updatePlace,
|
||||
deletePlace
|
||||
} = useAdminData();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const activeTab = searchParams.get('tab') || 'overview';
|
||||
|
||||
|
||||
// Listen to global refresh from layout
|
||||
useEffect(() => {
|
||||
const handler = () => refreshData();
|
||||
window.addEventListener('admin:refresh', handler);
|
||||
return () => window.removeEventListener('admin:refresh', handler);
|
||||
}, [refreshData]);
|
||||
|
||||
// Check permissions
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Acceso Denegado</h2>
|
||||
<p className="text-gray-600">No tienes permisos para acceder al panel de administración.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-orange-500 mx-auto mb-4"></div>
|
||||
<div className="text-lg">Cargando panel de administración...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<div className="text-red-500 text-xl mb-4">{error}</div>
|
||||
<button
|
||||
onClick={refreshData}
|
||||
className="bg-orange-500 text-white px-4 py-2 rounded-lg hover:bg-orange-600"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// No mostrar tabs duplicados ya que están en el sidebar
|
||||
const tabs: any[] = [];
|
||||
|
||||
// Filter tabs based on permissions
|
||||
const availableTabs = tabs.filter(tab => !tab.superAdminOnly || isSuperAdmin);
|
||||
|
||||
const renderTabContent = () => {
|
||||
const tabProps = {
|
||||
stats,
|
||||
users,
|
||||
destinations,
|
||||
places,
|
||||
establishments,
|
||||
incidents,
|
||||
reviews,
|
||||
isAdmin,
|
||||
isSuperAdmin,
|
||||
refreshData,
|
||||
loadUsers,
|
||||
loadDestinations,
|
||||
loadPlaces,
|
||||
loadEstablishments,
|
||||
loadIncidents,
|
||||
loadReviews,
|
||||
onCreateDestination: createDestination,
|
||||
onUpdateDestination: updateDestination,
|
||||
onDeleteDestination: deleteDestination,
|
||||
onCreatePlace: createPlace,
|
||||
onUpdatePlace: updatePlace,
|
||||
onDeletePlace: deletePlace,
|
||||
onRefreshData: refreshData
|
||||
};
|
||||
|
||||
switch (activeTab) {
|
||||
case 'overview':
|
||||
return <OverviewTab {...tabProps} />;
|
||||
case 'users':
|
||||
return <UsersTab {...tabProps} />;
|
||||
case 'services':
|
||||
return <ServicesTab {...tabProps} />;
|
||||
case 'financial':
|
||||
return <FinancialTab {...tabProps} />;
|
||||
case 'content':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="destinations" />;
|
||||
case 'content-destinations':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="destinations" />;
|
||||
case 'content-places':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="places" />;
|
||||
case 'content-guides':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="guides" />;
|
||||
case 'content-taxis':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="taxis" />;
|
||||
case 'content-geolocation':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="geolocation" />;
|
||||
case 'content-promotional':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="promotional" />;
|
||||
case 'content-ai-guides':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="ai-guides" />;
|
||||
case 'content-ar':
|
||||
return <ContentTab isSuperAdmin={isSuperAdmin} activeSubTab="ar-content" />;
|
||||
case 'geofences':
|
||||
return <GeolocationTab activeSubTab="geofences" />;
|
||||
case 'analytics':
|
||||
return <GeolocationTab activeSubTab="analytics" />;
|
||||
case 'testing':
|
||||
return <GeolocationTab activeSubTab="testing" />;
|
||||
case 'emergency-geo':
|
||||
return <GeolocationTab activeSubTab="emergency" />;
|
||||
case 'navigation':
|
||||
return <GeolocationTab activeSubTab="navigation" />;
|
||||
case 'emergency':
|
||||
return <EmergencyTab {...tabProps} />;
|
||||
case 'support':
|
||||
return <SupportTab {...tabProps} />;
|
||||
case 'config':
|
||||
return <ConfigTab {...tabProps} />;
|
||||
default:
|
||||
return <OverviewTab {...tabProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 admin-dashboard">
|
||||
<div className="px-4 md:px-6 py-4 md:py-8 mobile-content">
|
||||
<div className="mobile-card md:bg-transparent md:shadow-none bg-white rounded-lg p-4 md:p-0">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminDashboard;
|
||||
489
src/pages/dashboard/Bookings.tsx
Normal file
489
src/pages/dashboard/Bookings.tsx
Normal file
@@ -0,0 +1,489 @@
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Eye,
|
||||
Edit,
|
||||
Trash2,
|
||||
Calendar,
|
||||
Users,
|
||||
MapPin
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { BookingForm } from '@/components/dashboard/BookingForm';
|
||||
|
||||
// CreateReservationDto interface based on user requirements
|
||||
interface CreateReservationDto {
|
||||
establishmentId: string;
|
||||
userId: string;
|
||||
type: 'hotel' | 'restaurant' | 'tour' | 'activity';
|
||||
referenceId?: string;
|
||||
checkInDate: string;
|
||||
checkOutDate?: string;
|
||||
checkInTime?: string;
|
||||
guestsCount: number;
|
||||
specialRequests?: string;
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
interface Booking extends CreateReservationDto {
|
||||
id: string;
|
||||
sl: string;
|
||||
logo: string;
|
||||
customerName: string;
|
||||
establishmentName: string;
|
||||
clientName: string;
|
||||
clientEmail: string;
|
||||
clientPhone: string;
|
||||
status: 'Approved' | 'Pending' | 'Canceled';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const Bookings = () => {
|
||||
const { user } = useAuth();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
const [editingBooking, setEditingBooking] = useState<Booking | null>(null);
|
||||
|
||||
// Sample bookings data matching CreateReservationDto structure
|
||||
const [bookings, setBookings] = useState<Booking[]>([
|
||||
{
|
||||
id: '1',
|
||||
sl: '01',
|
||||
logo: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=100&h=100&fit=crop&crop=center',
|
||||
establishmentId: 'est_001',
|
||||
establishmentName: 'Chuijhal Hotel And Restaurant',
|
||||
userId: 'user_001',
|
||||
customerName: 'Ethan Blackwood',
|
||||
clientName: 'Ethan Blackwood',
|
||||
clientEmail: 'ethan@example.com',
|
||||
clientPhone: '123-456-789',
|
||||
type: 'hotel',
|
||||
referenceId: 'REF001',
|
||||
checkInDate: '2024-08-20',
|
||||
checkOutDate: '2024-08-24',
|
||||
checkInTime: '15:00',
|
||||
guestsCount: 2,
|
||||
specialRequests: 'Late check-in requested',
|
||||
totalAmount: 147,
|
||||
status: 'Approved',
|
||||
createdAt: '2024-08-15'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
sl: '02',
|
||||
logo: 'https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=100&h=100&fit=crop&crop=center',
|
||||
establishmentId: 'est_002',
|
||||
establishmentName: 'Green Mart Apartment',
|
||||
userId: 'user_002',
|
||||
customerName: 'Alexander Kaminski',
|
||||
clientName: 'Alexander Kaminski',
|
||||
clientEmail: 'alexander@example.com',
|
||||
clientPhone: '123-456-789',
|
||||
type: 'hotel',
|
||||
referenceId: 'REF002',
|
||||
checkInDate: '2024-08-20',
|
||||
checkOutDate: '2024-08-24',
|
||||
checkInTime: '14:00',
|
||||
guestsCount: 2,
|
||||
specialRequests: '',
|
||||
totalAmount: 147,
|
||||
status: 'Pending',
|
||||
createdAt: '2024-08-16'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
sl: '03',
|
||||
logo: 'https://images.unsplash.com/photo-1542314831-068cd1dbfeeb?w=100&h=100&fit=crop&crop=center',
|
||||
establishmentId: 'est_003',
|
||||
establishmentName: 'Northridge University',
|
||||
userId: 'user_003',
|
||||
customerName: 'Gabriel North',
|
||||
clientName: 'Gabriel North',
|
||||
clientEmail: 'gabriel@liston.com',
|
||||
clientPhone: '123-456-789',
|
||||
type: 'restaurant',
|
||||
referenceId: 'REF003',
|
||||
checkInDate: '2024-08-20',
|
||||
checkInTime: '19:00',
|
||||
guestsCount: 2,
|
||||
specialRequests: 'Window table preferred',
|
||||
totalAmount: 147,
|
||||
status: 'Canceled',
|
||||
createdAt: '2024-08-17'
|
||||
}
|
||||
]);
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusVariants = {
|
||||
'Approved': 'default',
|
||||
'Pending': 'secondary',
|
||||
'Canceled': 'destructive'
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<Badge variant={statusVariants[status as keyof typeof statusVariants]}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredBookings = bookings.filter(booking =>
|
||||
booking.establishmentName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
booking.clientName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
booking.clientEmail.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const handleCreateBooking = (bookingData: CreateReservationDto) => {
|
||||
const newBooking: Booking = {
|
||||
...bookingData,
|
||||
id: Date.now().toString(),
|
||||
sl: String(bookings.length + 1).padStart(2, '0'),
|
||||
logo: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=100&h=100&fit=crop&crop=center',
|
||||
establishmentName: `Establishment ${bookingData.establishmentId}`,
|
||||
customerName: `Customer ${bookingData.userId}`,
|
||||
clientName: `Customer ${bookingData.userId}`,
|
||||
clientEmail: 'customer@example.com',
|
||||
clientPhone: '123-456-789',
|
||||
status: 'Pending',
|
||||
createdAt: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
|
||||
setBookings([...bookings, newBooking]);
|
||||
setIsFormOpen(false);
|
||||
};
|
||||
|
||||
const handleEditBooking = (bookingData: CreateReservationDto) => {
|
||||
if (!editingBooking) return;
|
||||
|
||||
const updatedBooking: Booking = {
|
||||
...editingBooking,
|
||||
...bookingData
|
||||
};
|
||||
|
||||
setBookings(bookings.map(booking =>
|
||||
booking.id === editingBooking.id ? updatedBooking : booking
|
||||
));
|
||||
setEditingBooking(null);
|
||||
setIsFormOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteBooking = (id: string) => {
|
||||
setBookings(bookings.filter(booking => booking.id !== id));
|
||||
};
|
||||
|
||||
const handleApproveBooking = (id: string) => {
|
||||
setBookings(bookings.map(booking =>
|
||||
booking.id === id ? { ...booking, status: 'Approved' as const } : booking
|
||||
));
|
||||
};
|
||||
|
||||
const openEditForm = (booking: Booking) => {
|
||||
setEditingBooking(booking);
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
const openCreateForm = () => {
|
||||
setEditingBooking(null);
|
||||
setIsFormOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mobile-safe-top space-y-4 lg:space-y-6 p-4 lg:p-6">
|
||||
{/* Mobile Header */}
|
||||
<div className="mobile-header">
|
||||
<h1 className="mobile-title">Booking Requests</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Manage your booking requests and reservations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mobile Action Button */}
|
||||
<div className="lg:hidden">
|
||||
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={openCreateForm} className="mobile-btn w-full">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create New Booking
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[95vw] lg:max-w-2xl h-[90vh] lg:h-auto overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingBooking ? 'Edit Booking' : 'Create New Booking'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<BookingForm
|
||||
booking={editingBooking}
|
||||
onSubmit={editingBooking ? handleEditBooking : handleCreateBooking}
|
||||
onCancel={() => setIsFormOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="mobile-card">
|
||||
{/* Desktop Header */}
|
||||
<div className="hidden lg:flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-foreground">Booking Requests</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Duis vulputate metus fringilla, aliquet ex sed, pulvinar justo.
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={openCreateForm} className="bg-primary hover:bg-primary/90">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create New Booking
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingBooking ? 'Edit Booking' : 'Create New Booking'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<BookingForm
|
||||
booking={editingBooking}
|
||||
onSubmit={editingBooking ? handleEditBooking : handleCreateBooking}
|
||||
onCancel={() => setIsFormOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between mb-6 space-y-4 lg:space-y-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">Show</span>
|
||||
<Select value={itemsPerPage.toString()} onValueChange={(value) => setItemsPerPage(Number(value))}>
|
||||
<SelectTrigger className="w-20">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="25">25</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-muted-foreground">entries</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground hidden lg:block">Search:</span>
|
||||
<div className="relative flex-1 lg:flex-none">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search bookings..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="mobile-input pl-10 lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<div className="lg:hidden space-y-4">
|
||||
{filteredBookings.map((booking) => (
|
||||
<div key={booking.id} className="mobile-card border border-border bg-card">
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={booking.logo} alt="Logo" />
|
||||
<AvatarFallback>{booking.establishmentName.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h3 className="font-medium">{booking.establishmentName}</h3>
|
||||
<p className="text-sm text-muted-foreground">#{booking.sl}</p>
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge(booking.status)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<div className="flex items-center space-x-1 text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Date</span>
|
||||
</div>
|
||||
<p className="font-medium">{booking.checkInDate}</p>
|
||||
{booking.checkOutDate && <p className="text-xs text-muted-foreground">to {booking.checkOutDate}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-1 text-muted-foreground">
|
||||
<Users className="h-4 w-4" />
|
||||
<span>Guests</span>
|
||||
</div>
|
||||
<p className="font-medium">{booking.guestsCount} {booking.guestsCount === 1 ? 'Guest' : 'Guests'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-muted-foreground">Client</span>
|
||||
<span className="text-lg font-bold text-primary">${booking.totalAmount}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<p className="font-medium">{booking.clientName}</p>
|
||||
<p className="text-muted-foreground">{booking.clientEmail}</p>
|
||||
<p className="text-muted-foreground">{booking.clientPhone}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => openEditForm(booking)}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
{booking.status === 'Pending' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-green-600 hover:text-green-700 hover:bg-green-50"
|
||||
onClick={() => handleApproveBooking(booking.id)}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<div className="hidden lg:block border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-16">SL</TableHead>
|
||||
<TableHead className="w-16">Logo</TableHead>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead>Booking Date</TableHead>
|
||||
<TableHead>Persons</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="w-32">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredBookings.map((booking) => (
|
||||
<TableRow key={booking.id} className="hover:bg-muted/30">
|
||||
<TableCell className="font-medium">{booking.sl}</TableCell>
|
||||
<TableCell>
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={booking.logo} alt="Logo" />
|
||||
<AvatarFallback>{booking.establishmentName.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{booking.establishmentName}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{booking.checkInDate}</span>
|
||||
{booking.checkOutDate && <span> - {booking.checkOutDate}</span>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{booking.guestsCount} {booking.guestsCount === 1 ? 'Guest' : 'Guests'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-semibold text-primary">${booking.totalAmount}</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">{booking.clientName}</div>
|
||||
<div className="text-sm text-muted-foreground">{booking.clientEmail}</div>
|
||||
<div className="text-sm text-muted-foreground">{booking.clientPhone}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(booking.status)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button variant="outline" size="sm" className="h-8 w-8 p-0">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => openEditForm(booking)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
{booking.status === 'Pending' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-green-600 hover:text-green-700 hover:bg-green-50"
|
||||
onClick={() => handleApproveBooking(booking.id)}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => handleDeleteBooking(booking.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between mt-6 space-y-4 lg:space-y-0">
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Showing 1 to {filteredBookings.length} of {filteredBookings.length} entries
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="bg-primary text-primary-foreground">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bookings;
|
||||
300
src/pages/dashboard/Bookmarks.tsx
Normal file
300
src/pages/dashboard/Bookmarks.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useBookmarks } from '@/hooks/useBookmarks';
|
||||
import {
|
||||
Star,
|
||||
Phone,
|
||||
MapPin,
|
||||
Eye,
|
||||
Heart,
|
||||
ExternalLink,
|
||||
Trash,
|
||||
CheckCircle,
|
||||
ArrowUpRight
|
||||
} from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const Bookmarks = () => {
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
bookmarks,
|
||||
loading,
|
||||
error,
|
||||
removeBookmark,
|
||||
loadBookmarks,
|
||||
getBookmarksCount
|
||||
} = useBookmarks();
|
||||
|
||||
const handleRemoveBookmark = async (bookmarkId: string) => {
|
||||
const success = await removeBookmark(bookmarkId);
|
||||
if (success) {
|
||||
toast.success('Bookmark removed successfully');
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
const stars = [];
|
||||
const fullStars = Math.floor(rating);
|
||||
|
||||
for (let i = 0; i < fullStars; i++) {
|
||||
stars.push(
|
||||
<Star
|
||||
key={i}
|
||||
size={14}
|
||||
className="text-primary fill-current"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const remainingStars = 5 - fullStars;
|
||||
for (let i = 0; i < remainingStars; i++) {
|
||||
stars.push(
|
||||
<Star
|
||||
key={`empty-${i}`}
|
||||
size={14}
|
||||
className="text-muted"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return stars;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="body-content">
|
||||
<div className="container-xxl">
|
||||
<div className="flex items-center justify-center min-h-96">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-4 text-muted-foreground">Loading bookmarks...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="body-content">
|
||||
<div className="container-xxl">
|
||||
<div className="flex items-center justify-center min-h-96">
|
||||
<div className="text-center">
|
||||
<p className="text-destructive mb-4">{error}</p>
|
||||
<Button onClick={loadBookmarks}>Try Again</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="body-content">
|
||||
<div className="decoration blur-2"></div>
|
||||
<div className="decoration blur-3"></div>
|
||||
<div className="container-xxl">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-end justify-between mb-4" data-aos="fade-down">
|
||||
<div className="col">
|
||||
<div className="section-header">
|
||||
<div className="font-caveat text-2xl font-bold text-primary capitalize text-center xl:text-left mb-2">
|
||||
Bookmarked
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-0 capitalize text-center xl:text-left">
|
||||
Bookmarked Listings
|
||||
</h2>
|
||||
<div className="text-base text-center xl:text-left mt-2">
|
||||
Discover exciting categories.
|
||||
<span className="text-primary font-semibold"> Find what you're looking for.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-xl-auto">
|
||||
<Link
|
||||
to="/explore"
|
||||
className="flex items-center text-sm font-bold gap-2 justify-center xl:justify-end tracking-wide text-primary uppercase hover:underline"
|
||||
>
|
||||
See All
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bookmarks List */}
|
||||
{bookmarks.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Heart className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold mb-2">No bookmarks yet</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Start exploring and save your favorite places to see them here.
|
||||
</p>
|
||||
<Link to="/explore">
|
||||
<Button>
|
||||
Explore Now
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{bookmarks.map((bookmark) => (
|
||||
<div
|
||||
key={bookmark.id}
|
||||
className="border-0 bg-background card card-hover flex-fill overflow-hidden rounded-3 shadow-sm w-100 card-hover-bg hover:shadow-md transition-shadow"
|
||||
>
|
||||
<Link
|
||||
to={`/listing-details/${bookmark.id}`}
|
||||
className="stretched-link absolute inset-0 z-0"
|
||||
aria-label={`View details for ${bookmark.title}`}
|
||||
/>
|
||||
<div className="card-body p-0">
|
||||
<div className="flex h-full">
|
||||
{/* Image Section */}
|
||||
<div className="lg:w-1/4 md:w-2/5 sm:w-1/3 xl:w-1/6 relative">
|
||||
<div className="card-image-hover dark-overlay h-full overflow-hidden relative">
|
||||
<img
|
||||
src={bookmark.image}
|
||||
alt={bookmark.title}
|
||||
className="h-full w-full object-cover"
|
||||
style={{ height: '200px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="lg:w-3/4 md:w-3/5 sm:w-2/3 xl:w-5/6 p-3 lg:p-4 md:p-3 sm:p-4">
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Remove Bookmark Button */}
|
||||
<div className="flex gap-2 absolute top-3 right-3 z-10">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveBookmark(bookmark.id);
|
||||
}}
|
||||
className="flex items-center bg-background/90 backdrop-blur-sm p-2 rounded-full text-destructive hover:bg-destructive hover:text-destructive-foreground transition-colors"
|
||||
data-tooltip="Remove Bookmark"
|
||||
aria-label="Remove from bookmarks"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Rating */}
|
||||
<div className="flex items-center flex-wrap gap-1 text-primary mb-2">
|
||||
<div className="flex">
|
||||
{renderStars(bookmark.rating)}
|
||||
</div>
|
||||
<span className="font-medium text-primary">
|
||||
<span className="text-base font-semibold mr-1">
|
||||
({bookmark.rating})
|
||||
</span>
|
||||
{bookmark.reviewCount.toLocaleString()} reviews
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h4 className="text-lg font-semibold mb-0 flex items-center gap-2">
|
||||
<span>{bookmark.title}</span>
|
||||
{bookmark.isVerified && (
|
||||
<CheckCircle className="h-5 w-5 text-success" />
|
||||
)}
|
||||
</h4>
|
||||
|
||||
{/* Address */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{bookmark.address}
|
||||
</p>
|
||||
|
||||
{/* Category & Price */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{bookmark.category}
|
||||
</Badge>
|
||||
{bookmark.price && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{bookmark.price}
|
||||
</Badge>
|
||||
)}
|
||||
{bookmark.isFeatured && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Featured
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Amenities */}
|
||||
{bookmark.amenities && bookmark.amenities.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{bookmark.amenities.slice(0, 3).map((amenity, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="text-xs bg-muted text-muted-foreground px-2 py-1 rounded"
|
||||
>
|
||||
{amenity}
|
||||
</span>
|
||||
))}
|
||||
{bookmark.amenities.length > 3 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
+{bookmark.amenities.length - 3} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contact Actions */}
|
||||
<div className="flex flex-wrap gap-3 lg:gap-2 xl:gap-3 mt-auto z-10">
|
||||
<a
|
||||
href={`tel:${bookmark.phone}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex gap-2 items-center text-sm font-semibold hover:text-primary transition-colors"
|
||||
>
|
||||
<Phone className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{bookmark.phone}</span>
|
||||
</a>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Open directions in Google Maps
|
||||
const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(bookmark.address)}`;
|
||||
window.open(mapsUrl, '_blank');
|
||||
}}
|
||||
className="flex gap-2 items-center text-sm font-semibold hover:text-primary transition-colors"
|
||||
>
|
||||
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Directions</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Load More / Footer */}
|
||||
{bookmarks.length > 0 && (
|
||||
<div className="text-center mt-8">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Showing {bookmarks.length} of {getBookmarksCount()} bookmarked items
|
||||
</p>
|
||||
<Link to="/explore">
|
||||
<Button variant="outline" size="lg">
|
||||
Explore More Places
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bookmarks;
|
||||
266
src/pages/dashboard/Dashboard.tsx
Normal file
266
src/pages/dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
import { mockApi } from '@/services/mockApi';
|
||||
import CounterCard from '@/components/dashboard/CounterCard';
|
||||
import ApexChart from '@/components/dashboard/ApexChart';
|
||||
import EnhancedDataTable from '@/components/dashboard/EnhancedDataTable';
|
||||
import { useDashboardFeatures } from '@/hooks/useDashboardFeatures';
|
||||
import {
|
||||
Users,
|
||||
TrendingUp,
|
||||
DollarSign,
|
||||
Eye,
|
||||
Calendar,
|
||||
MapPin,
|
||||
Star,
|
||||
CreditCard,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
User,
|
||||
Plus
|
||||
} from 'lucide-react';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useLanguage();
|
||||
const { isDarkMode, toggleDarkMode, initializeTooltips } = useDashboardFeatures();
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// If user is admin/super_admin, redirect to Admin dashboard
|
||||
useEffect(() => {
|
||||
if (user?.role === 'admin' || user?.role === 'super_admin') {
|
||||
navigate('/dashboard/admin', { replace: true });
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const dashboardStats = await mockApi.getDashboardStats(user?.id || '1');
|
||||
setStats(dashboardStats);
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard stats:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
initializeTooltips();
|
||||
}, [user, initializeTooltips]);
|
||||
|
||||
// Sample data for charts
|
||||
const monthlyData = [10, 20, 15, 30, 35, 30, 45, 59, 30, 35, 25, 29, 15];
|
||||
const monthlyLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
// Sample data for table
|
||||
const tableColumns = [
|
||||
{ key: 'guest', label: 'Guest', sortable: true },
|
||||
{ key: 'listing', label: 'Listing', sortable: true },
|
||||
{ key: 'date', label: 'Date', sortable: true },
|
||||
{ key: 'payment', label: 'Payment', sortable: true },
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
render: (value: string) => (
|
||||
<span className={`badge ${
|
||||
value === 'Confirmed' ? 'bg-success' :
|
||||
value === 'Pending' ? 'bg-warning' : 'bg-secondary'
|
||||
}`}>
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
label: 'Action',
|
||||
render: () => (
|
||||
<button className="btn btn-outline-primary btn-sm">View</button>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const sampleBookings = [
|
||||
{
|
||||
guest: 'John Doe',
|
||||
listing: 'Ocean View Villa',
|
||||
date: '2024-01-15',
|
||||
payment: '$299',
|
||||
status: 'Confirmed'
|
||||
},
|
||||
{
|
||||
guest: 'Jane Smith',
|
||||
listing: 'Mountain Cabin',
|
||||
date: '2024-01-16',
|
||||
payment: '$199',
|
||||
status: 'Pending'
|
||||
},
|
||||
{
|
||||
guest: 'Mike Johnson',
|
||||
listing: 'City Apartment',
|
||||
date: '2024-01-17',
|
||||
payment: '$149',
|
||||
status: 'Confirmed'
|
||||
}
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="d-flex align-items-center justify-content-center" style={{ height: '400px' }}>
|
||||
<div className="text-center">
|
||||
<div className="spinner-border text-primary mb-3" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div className="text-muted">Loading dashboard...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mobile-safe-top lg:p-6 p-4 space-y-4 lg:space-y-6">
|
||||
{/* Header Banner */}
|
||||
<div className="mobile-card bg-gradient-to-br from-primary to-primary-glow rounded-lg p-4 lg:p-6 text-white relative overflow-hidden">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex-1">
|
||||
<h1 className="mobile-title lg:text-3xl font-bold mb-2 lg:mb-3">
|
||||
¡Welcome back, {user?.name || 'User'}!
|
||||
</h1>
|
||||
<p className="text-white/80 text-sm lg:text-base mb-4">
|
||||
Manage your listings, track bookings, and grow your business with our comprehensive dashboard.
|
||||
</p>
|
||||
<button className="mobile-btn bg-white text-primary hover:bg-white/90 transition-colors">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add New Listing
|
||||
</button>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<img
|
||||
src="https://img.freepik.com/foto-gratis/vista-superior-mano-sosteniendo-telefono-inteligente_23-2149617681.jpg"
|
||||
alt="Dashboard"
|
||||
className="w-48 h-auto opacity-80"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="mobile-grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="mobile-card">
|
||||
<CounterCard
|
||||
icon={<Users size={20} />}
|
||||
title="Times Bookmarked"
|
||||
value={stats?.bookmarks || 847}
|
||||
suffix=""
|
||||
trend={12}
|
||||
color="#F84525"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-card">
|
||||
<CounterCard
|
||||
icon={<TrendingUp size={20} />}
|
||||
title="Progress"
|
||||
value={stats?.progress || 75}
|
||||
suffix="%"
|
||||
trend={-3}
|
||||
color="#28a745"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-card">
|
||||
<CounterCard
|
||||
icon={<DollarSign size={20} />}
|
||||
title="Revenue"
|
||||
value={stats?.revenue || 12850}
|
||||
suffix=""
|
||||
trend={18}
|
||||
color="#ffc107"
|
||||
/>
|
||||
</div>
|
||||
<div className="mobile-card">
|
||||
<CounterCard
|
||||
icon={<Eye size={20} />}
|
||||
title="Visitors"
|
||||
value={stats?.visitors || 2847}
|
||||
suffix=""
|
||||
trend={7}
|
||||
color="#17a2b8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<div className="mobile-card h-full">
|
||||
<ApexChart
|
||||
type="area"
|
||||
data={monthlyData}
|
||||
labels={monthlyLabels}
|
||||
title="Monthly Statistics"
|
||||
height={300}
|
||||
color="#F84525"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-card">
|
||||
<div className="p-4 lg:p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Performance Overview</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Booking Rate</span>
|
||||
<span className="text-primary font-bold">78%</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div className="bg-primary h-2 rounded-full" style={{ width: '78%' }}></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Customer Satisfaction</span>
|
||||
<span className="text-green-600 font-bold">92%</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div className="bg-green-600 h-2 rounded-full" style={{ width: '92%' }}></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Response Time</span>
|
||||
<span className="text-yellow-600 font-bold">65%</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div className="bg-yellow-600 h-2 rounded-full" style={{ width: '65%' }}></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Revenue Growth</span>
|
||||
<span className="text-blue-600 font-bold">84%</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div className="bg-blue-600 h-2 rounded-full" style={{ width: '84%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Bookings Table */}
|
||||
<div className="mobile-card">
|
||||
<div className="overflow-x-auto">
|
||||
<EnhancedDataTable
|
||||
data={sampleBookings}
|
||||
columns={tableColumns}
|
||||
itemsPerPage={5}
|
||||
searchable={true}
|
||||
exportable={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
266
src/pages/dashboard/InvoiceDetail.tsx
Normal file
266
src/pages/dashboard/InvoiceDetail.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import React from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Download,
|
||||
Send,
|
||||
DollarSign,
|
||||
Calendar,
|
||||
Phone,
|
||||
Mail,
|
||||
MapPin,
|
||||
CreditCard
|
||||
} from 'lucide-react';
|
||||
|
||||
const InvoiceDetail = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Mock invoice data - in real app, fetch by ID
|
||||
const invoice = {
|
||||
id: id,
|
||||
invoiceNumber: 'INV-0044777',
|
||||
company: {
|
||||
name: 'ListOn',
|
||||
address: '1355 Market Street, Suite 900',
|
||||
city: 'San Francisco, CA 94103',
|
||||
phone: '(123) 456-7890',
|
||||
email: 'billing@liston.com'
|
||||
},
|
||||
customer: {
|
||||
name: 'Alexander Kamin',
|
||||
email: 'first.last@example.com',
|
||||
address: '1355 Market Street, Suite 900',
|
||||
city: 'San Francisco, CA 94103',
|
||||
phone: '(123) 456-7890'
|
||||
},
|
||||
issueDate: 'March 19th, 2017',
|
||||
dueDate: 'April 21th, 2017',
|
||||
status: 'paid',
|
||||
items: [
|
||||
{
|
||||
description: 'Lorem Ipsum is simply dummy text',
|
||||
details: 'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots',
|
||||
quantity: 1,
|
||||
unitPrice: 39.00,
|
||||
tax: 71.98,
|
||||
totalPrice: 27.98
|
||||
},
|
||||
{
|
||||
description: 'It is a long established fact that a reader will be',
|
||||
details: 'There are many variations of passages of Lorem Ipsum available, but the majority',
|
||||
quantity: 2,
|
||||
unitPrice: 57.00,
|
||||
tax: 56.80,
|
||||
totalPrice: 112.80
|
||||
},
|
||||
{
|
||||
description: 'The standard chunk of Lorem Ipsum used since',
|
||||
details: 'It has survived not only five centuries, but also the leap into electronic.',
|
||||
quantity: 3,
|
||||
unitPrice: 645.00,
|
||||
tax: 321.20,
|
||||
totalPrice: 1286.20
|
||||
},
|
||||
{
|
||||
description: 'The standard chunk of Lorem Ipsum used since',
|
||||
details: 'It has survived not only five centuries, but also the leap into electronic.',
|
||||
quantity: 3,
|
||||
unitPrice: 486.00,
|
||||
tax: 524.20,
|
||||
totalPrice: 789.20
|
||||
}
|
||||
],
|
||||
subtotal: 920.05,
|
||||
discount: 12.9,
|
||||
vat: 0,
|
||||
grandTotal: 1248.9,
|
||||
paymentNote: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.',
|
||||
thankNote: 'Thank you very much for choosing us. It was a pleasure to have worked with you.'
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'paid':
|
||||
return 'bg-green-100 text-green-800 border-green-200';
|
||||
case 'pending':
|
||||
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
|
||||
case 'overdue':
|
||||
return 'bg-red-100 text-red-800 border-red-200';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate('/dashboard/invoices')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to Invoices
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`capitalize ${getStatusColor(invoice.status)}`}
|
||||
>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download PDF
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Send Email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Content */}
|
||||
<Card className="p-8">
|
||||
<CardContent className="p-0">
|
||||
{/* Invoice Header */}
|
||||
<div className="flex justify-between items-start mb-12">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-red-500 mb-4">
|
||||
List<span className="text-black">On</span>.
|
||||
</h1>
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<p className="font-medium">{invoice.company.name}</p>
|
||||
<p>{invoice.company.address}</p>
|
||||
<p>{invoice.company.city}</p>
|
||||
<p>P: {invoice.company.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<h2 className="text-2xl font-bold mb-2">Invoice #{invoice.invoiceNumber}</h2>
|
||||
<div className="space-y-1 text-sm">
|
||||
<p><span className="font-medium">Issued</span> {invoice.issueDate}</p>
|
||||
<p className="text-red-500"><span className="font-medium">Payment due</span> {invoice.dueDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bill To Section */}
|
||||
<div className="flex justify-between mb-8">
|
||||
<div>
|
||||
<h3 className="font-bold text-red-500 mb-3">Full Name</h3>
|
||||
<p className="text-red-500 font-medium">{invoice.customer.email}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="space-y-1 text-sm">
|
||||
<p className="font-medium">{invoice.company.name}</p>
|
||||
<p>{invoice.company.address}</p>
|
||||
<p>{invoice.company.city}</p>
|
||||
<p>P: {invoice.company.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items Table */}
|
||||
<div className="mb-8">
|
||||
<div className="bg-gray-50 rounded-t-lg">
|
||||
<div className="grid grid-cols-12 gap-4 p-4 font-medium text-sm">
|
||||
<div className="col-span-5">ITEM LIST</div>
|
||||
<div className="col-span-2 text-center">QUANTITY</div>
|
||||
<div className="col-span-2 text-center">UNIT PRICE</div>
|
||||
<div className="col-span-1 text-center">TAX</div>
|
||||
<div className="col-span-2 text-center">TOTAL PRICE</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{invoice.items.map((item, index) => (
|
||||
<div key={index} className="grid grid-cols-12 gap-4 p-4 border-b border-gray-200">
|
||||
<div className="col-span-5">
|
||||
<h4 className="font-medium mb-1">{item.description}</h4>
|
||||
<p className="text-sm text-muted-foreground">{item.details}</p>
|
||||
</div>
|
||||
<div className="col-span-2 text-center">{item.quantity}</div>
|
||||
<div className="col-span-2 text-center">${item.unitPrice.toFixed(2)}</div>
|
||||
<div className="col-span-1 text-center">${item.tax.toFixed(2)}</div>
|
||||
<div className="col-span-2 text-center font-medium">${item.totalPrice.toFixed(2)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Payment Note */}
|
||||
<div className="mb-8">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{invoice.paymentNote}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Totals */}
|
||||
<div className="flex justify-end mb-8">
|
||||
<div className="w-80 space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span>Sub - Total amount:</span>
|
||||
<span className="font-medium">${invoice.subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Discount:</span>
|
||||
<span className="font-medium">{invoice.discount}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>VAT:</span>
|
||||
<span className="font-medium">---</span>
|
||||
</div>
|
||||
<div className="border-t pt-3">
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Grand Total:</span>
|
||||
<span>${invoice.grandTotal.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thank You Note */}
|
||||
<div className="mb-8">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{invoice.thankNote}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Payment Methods */}
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/placeholder.svg?text=PayPal" alt="PayPal" className="h-8 w-16 bg-blue-600 rounded" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/placeholder.svg?text=Visa" alt="Visa" className="h-8 w-12 bg-blue-800 rounded" />
|
||||
<img src="/placeholder.svg?text=MC" alt="MasterCard" className="h-8 w-12 bg-red-600 rounded" />
|
||||
<img src="/placeholder.svg?text=Maestro" alt="Maestro" className="h-8 w-12 bg-blue-500 rounded" />
|
||||
<img src="/placeholder.svg?text=Amex" alt="American Express" className="h-8 w-12 bg-blue-400 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Button size="sm" className="bg-blue-500 hover:bg-blue-600">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
<Button size="sm" className="bg-green-500 hover:bg-green-600">
|
||||
<DollarSign className="w-4 h-4 mr-2" />
|
||||
Make A Payment
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvoiceDetail;
|
||||
340
src/pages/dashboard/Invoices.tsx
Normal file
340
src/pages/dashboard/Invoices.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Eye,
|
||||
Download,
|
||||
Filter,
|
||||
Calendar,
|
||||
DollarSign
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface Invoice {
|
||||
id: string;
|
||||
invoiceNumber: string;
|
||||
customer: string;
|
||||
customerEmail: string;
|
||||
amount: number;
|
||||
status: 'paid' | 'pending' | 'overdue' | 'cancelled';
|
||||
issueDate: string;
|
||||
dueDate: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const mockInvoices: Invoice[] = [
|
||||
{
|
||||
id: '1',
|
||||
invoiceNumber: 'INV-0044777',
|
||||
customer: 'Alexander Kamin',
|
||||
customerEmail: 'alex@example.com',
|
||||
amount: 1247.80,
|
||||
status: 'paid',
|
||||
issueDate: '2024-03-19',
|
||||
dueDate: '2024-04-21',
|
||||
description: 'Hotel booking services'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
invoiceNumber: 'INV-0044778',
|
||||
customer: 'Sarah Johnson',
|
||||
customerEmail: 'sarah@example.com',
|
||||
amount: 899.50,
|
||||
status: 'pending',
|
||||
issueDate: '2024-03-18',
|
||||
dueDate: '2024-04-18',
|
||||
description: 'Restaurant reservation'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
invoiceNumber: 'INV-0044779',
|
||||
customer: 'Michael Brown',
|
||||
customerEmail: 'michael@example.com',
|
||||
amount: 2150.00,
|
||||
status: 'overdue',
|
||||
issueDate: '2024-03-10',
|
||||
dueDate: '2024-04-10',
|
||||
description: 'Tour package booking'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
invoiceNumber: 'INV-0044780',
|
||||
customer: 'Emma Wilson',
|
||||
customerEmail: 'emma@example.com',
|
||||
amount: 567.25,
|
||||
status: 'cancelled',
|
||||
issueDate: '2024-03-15',
|
||||
dueDate: '2024-04-15',
|
||||
description: 'Activity booking'
|
||||
},
|
||||
];
|
||||
|
||||
const Invoices = () => {
|
||||
const navigate = useNavigate();
|
||||
const [invoices] = useState<Invoice[]>(mockInvoices);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [entriesPerPage, setEntriesPerPage] = useState('10');
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'paid':
|
||||
return 'bg-green-100 text-green-800 border-green-200';
|
||||
case 'pending':
|
||||
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
|
||||
case 'overdue':
|
||||
return 'bg-red-100 text-red-800 border-red-200';
|
||||
case 'cancelled':
|
||||
return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800 border-gray-200';
|
||||
}
|
||||
};
|
||||
|
||||
const filteredInvoices = invoices.filter(invoice => {
|
||||
const matchesSearch = invoice.customer.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
invoice.invoiceNumber.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'all' || invoice.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const totalAmount = filteredInvoices.reduce((sum, invoice) => sum + invoice.amount, 0);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-red-500 flex items-center">
|
||||
<div className="w-1 h-8 bg-red-500 mr-3"></div>
|
||||
Invoice Requests
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage and track all your invoices and payment requests.
|
||||
</p>
|
||||
</div>
|
||||
<Button className="bg-red-500 hover:bg-red-600">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create New Invoice
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Invoices</p>
|
||||
<p className="text-2xl font-bold">{filteredInvoices.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Calendar className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Amount</p>
|
||||
<p className="text-2xl font-bold">${totalAmount.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<DollarSign className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Paid</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
{filteredInvoices.filter(inv => inv.status === 'paid').length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<div className="w-5 h-5 bg-green-600 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Pending</p>
|
||||
<p className="text-2xl font-bold text-yellow-600">
|
||||
{filteredInvoices.filter(inv => inv.status === 'pending').length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center">
|
||||
<div className="w-5 h-5 bg-yellow-600 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="mb-6">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center justify-between">
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">Show</span>
|
||||
<Select value={entriesPerPage} onValueChange={setEntriesPerPage}>
|
||||
<SelectTrigger className="w-20">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="25">25</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm">entries</span>
|
||||
</div>
|
||||
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="All Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="paid">Paid</SelectItem>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="overdue">Overdue</SelectItem>
|
||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full lg:w-auto lg:min-w-[300px]">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||
<Input
|
||||
placeholder="Search invoices..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoices Table */}
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th className="text-left p-4 font-medium text-sm">INVOICE #</th>
|
||||
<th className="text-left p-4 font-medium text-sm">CUSTOMER</th>
|
||||
<th className="text-left p-4 font-medium text-sm">ISSUE DATE</th>
|
||||
<th className="text-left p-4 font-medium text-sm">DUE DATE</th>
|
||||
<th className="text-left p-4 font-medium text-sm">AMOUNT</th>
|
||||
<th className="text-left p-4 font-medium text-sm">STATUS</th>
|
||||
<th className="text-left p-4 font-medium text-sm">ACTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredInvoices.map((invoice) => (
|
||||
<tr key={invoice.id} className="border-b hover:bg-gray-50">
|
||||
<td className="p-4">
|
||||
<span className="font-medium text-blue-600">{invoice.invoiceNumber}</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<div className="font-medium">{invoice.customer}</div>
|
||||
<div className="text-sm text-muted-foreground">{invoice.customerEmail}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-sm">{invoice.issueDate}</td>
|
||||
<td className="p-4 text-sm">{invoice.dueDate}</td>
|
||||
<td className="p-4">
|
||||
<span className="font-medium">${invoice.amount.toLocaleString()}</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`capitalize ${getStatusColor(invoice.status)}`}
|
||||
>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/dashboard/invoice/${invoice.id}`)}
|
||||
className="text-green-600 border-green-200 hover:bg-green-50"
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-blue-600 border-blue-200 hover:bg-blue-50"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{filteredInvoices.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No invoices found matching your criteria.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="p-4 border-t bg-gray-50 flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing 1 to {Math.min(parseInt(entriesPerPage), filteredInvoices.length)} of {filteredInvoices.length} entries
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="bg-red-500 text-white">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Invoices;
|
||||
931
src/pages/dashboard/Messages.tsx
Normal file
931
src/pages/dashboard/Messages.tsx
Normal file
@@ -0,0 +1,931 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
MessageCircle,
|
||||
Users,
|
||||
UserCheck,
|
||||
Bell,
|
||||
Phone,
|
||||
Video,
|
||||
Paperclip,
|
||||
Smile,
|
||||
Send,
|
||||
Search,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
X,
|
||||
Settings,
|
||||
UserPlus,
|
||||
Circle,
|
||||
Palette,
|
||||
Edit3
|
||||
} from 'lucide-react';
|
||||
|
||||
const Messages = () => {
|
||||
const { user } = useAuth();
|
||||
const [activeTab, setActiveTab] = useState('chats');
|
||||
const [selectedChatId, setSelectedChatId] = useState<string | null>(null);
|
||||
const [messageText, setMessageText] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||
const [showUserSearch, setShowUserSearch] = useState(false);
|
||||
const [showChatDetails, setShowChatDetails] = useState(true);
|
||||
const [selectedColor, setSelectedColor] = useState('green');
|
||||
const [isAutoBot, setIsAutoBot] = useState(true);
|
||||
const [newNickname, setNewNickname] = useState('');
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
chats,
|
||||
messages,
|
||||
onlineUsers,
|
||||
loading,
|
||||
error,
|
||||
loadMessages,
|
||||
sendMessage,
|
||||
searchUsers,
|
||||
createChat,
|
||||
getChatById,
|
||||
clearError
|
||||
} = useChat();
|
||||
|
||||
const selectedChat = selectedChatId ? getChatById(selectedChatId) : chats[0];
|
||||
|
||||
// Auto-scroll to bottom of messages
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
// Load messages when chat is selected
|
||||
useEffect(() => {
|
||||
if (selectedChatId) {
|
||||
loadMessages(selectedChatId);
|
||||
}
|
||||
}, [selectedChatId, loadMessages]);
|
||||
|
||||
// Auto-select first chat
|
||||
useEffect(() => {
|
||||
if (chats.length > 0 && !selectedChatId) {
|
||||
setSelectedChatId(chats[0].id);
|
||||
}
|
||||
}, [chats, selectedChatId]);
|
||||
|
||||
// Handle search
|
||||
const handleSearch = async (query: string) => {
|
||||
setSearchQuery(query);
|
||||
if (query.trim()) {
|
||||
const results = await searchUsers(query);
|
||||
setSearchResults(results);
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle send message
|
||||
const handleSendMessage = async () => {
|
||||
if (!messageText.trim() || !selectedChatId) return;
|
||||
|
||||
try {
|
||||
await sendMessage(selectedChatId, messageText);
|
||||
setMessageText('');
|
||||
} catch (err) {
|
||||
toast.error('Failed to send message');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle create chat with user
|
||||
const handleCreateChat = async (userId: string) => {
|
||||
try {
|
||||
const newChat = await createChat([userId]);
|
||||
setSelectedChatId(newChat.id);
|
||||
setShowUserSearch(false);
|
||||
setSearchQuery('');
|
||||
setSearchResults([]);
|
||||
toast.success('Chat created successfully');
|
||||
} catch (err) {
|
||||
toast.error('Failed to create chat');
|
||||
}
|
||||
};
|
||||
|
||||
// Get notifications count
|
||||
const getNotificationsCount = () => {
|
||||
return chats.reduce((total, chat) => total + chat.unreadCount, 0);
|
||||
};
|
||||
|
||||
// Format time
|
||||
const formatTime = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
if (diffInHours < 24) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (diffInHours < 168) {
|
||||
return date.toLocaleDateString([], { weekday: 'short' });
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background overflow-hidden">
|
||||
{/* Chat Container */}
|
||||
<div className="chat-container m-0 overflow-hidden relative rounded-lg flex w-full">
|
||||
{/* Chat List Sidebar */}
|
||||
<div className="chat-list__sidebar p-0 w-80 bg-background border-r border-border flex flex-col">
|
||||
{/* Search Section */}
|
||||
<div className="chat-list__search relative p-4 border-b border-border">
|
||||
<form className="relative">
|
||||
<Input
|
||||
type="search"
|
||||
className="pl-4 pr-16"
|
||||
placeholder="People, Groups and Messages"
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-12 top-1/2 transform -translate-y-1/2 p-1 hover:bg-accent rounded"
|
||||
>
|
||||
<Search className="h-5 w-5 text-muted-foreground" />
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 p-2 hover:bg-accent rounded"
|
||||
onClick={() => setShowUserSearch(!showUserSearch)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar Tabs */}
|
||||
<ul className="chat-list__sidebar-tabs flex border-b border-border bg-muted/30" role="tablist">
|
||||
<li className="flex-1" role="presentation">
|
||||
<button
|
||||
className={`w-full p-3 text-center border-0 bg-transparent transition-colors ${
|
||||
activeTab === 'chats'
|
||||
? 'bg-background border-b-2 border-primary text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
onClick={() => setActiveTab('chats')}
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'chats'}
|
||||
>
|
||||
<div className="relative inline-block">
|
||||
<MessageCircle className="h-6 w-6 mx-auto mb-1" />
|
||||
{chats.length > 0 && (
|
||||
<div className="absolute -top-1 -right-1 bg-orange-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
|
||||
{chats.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="block text-xs font-semibold">Chats</span>
|
||||
</button>
|
||||
</li>
|
||||
<li className="flex-1" role="presentation">
|
||||
<button
|
||||
className={`w-full p-3 text-center border-0 bg-transparent transition-colors ${
|
||||
activeTab === 'online'
|
||||
? 'bg-background border-b-2 border-primary text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
onClick={() => setActiveTab('online')}
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'online'}
|
||||
>
|
||||
<Users className="h-6 w-6 mx-auto mb-1" />
|
||||
<span className="block text-xs font-semibold">Online users</span>
|
||||
</button>
|
||||
</li>
|
||||
<li className="flex-1" role="presentation">
|
||||
<button
|
||||
className={`w-full p-3 text-center border-0 bg-transparent transition-colors ${
|
||||
activeTab === 'contacts'
|
||||
? 'bg-background border-b-2 border-primary text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
onClick={() => setActiveTab('contacts')}
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'contacts'}
|
||||
>
|
||||
<UserCheck className="h-6 w-6 mx-auto mb-1" />
|
||||
<span className="block text-xs font-semibold">Contacts</span>
|
||||
</button>
|
||||
</li>
|
||||
<li className="flex-1" role="presentation">
|
||||
<button
|
||||
className={`w-full p-3 text-center border-0 bg-transparent transition-colors ${
|
||||
activeTab === 'notifications'
|
||||
? 'bg-background border-b-2 border-primary text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
onClick={() => setActiveTab('notifications')}
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'notifications'}
|
||||
>
|
||||
<div className="relative inline-block">
|
||||
<Bell className="h-6 w-6 mx-auto mb-1" />
|
||||
{getNotificationsCount() > 0 && (
|
||||
<div className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
|
||||
{getNotificationsCount()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="block text-xs font-semibold">Notifications</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* Search Results */}
|
||||
{showUserSearch && searchResults.length > 0 && (
|
||||
<div className="p-3 border-b border-border">
|
||||
<h3 className="text-sm font-medium mb-2">Search Results</h3>
|
||||
{searchResults.map((user) => (
|
||||
<div
|
||||
key={user.id}
|
||||
className="flex items-center p-2 hover:bg-accent rounded-lg cursor-pointer"
|
||||
onClick={() => handleCreateChat(user.id)}
|
||||
>
|
||||
<Avatar className="h-8 w-8 mr-3">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>{user.name?.charAt(0) || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{user.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{user.email}</p>
|
||||
</div>
|
||||
<Plus className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chats Tab */}
|
||||
{activeTab === 'chats' && (
|
||||
<div className="chat-list__in relative">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-semibold mb-4">Recent Chat</h2>
|
||||
<div className="nav chat-list space-y-1">
|
||||
{chats.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
className={`item-list item-list__chat flex items-start p-3 rounded-lg cursor-pointer transition-colors ${
|
||||
selectedChatId === chat.id
|
||||
? 'bg-primary/10 border-l-4 border-primary'
|
||||
: 'hover:bg-accent unseen'
|
||||
} ${chat.unreadCount > 0 ? 'unseen' : 'seen'}`}
|
||||
onClick={() => setSelectedChatId(chat.id)}
|
||||
>
|
||||
<div className="avatar relative mr-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={chat.avatar} />
|
||||
<AvatarFallback>{chat.name?.charAt(0) || 'C'}</AvatarFallback>
|
||||
</Avatar>
|
||||
{chat.online ? (
|
||||
<div className="status online absolute bottom-0 right-0 h-3 w-3 bg-green-500 rounded-full border-2 border-background" />
|
||||
) : (
|
||||
<div className="status offline absolute bottom-0 right-0 h-3 w-3 bg-gray-400 rounded-full border-2 border-background" />
|
||||
)}
|
||||
{chat.unreadCount > 0 && (
|
||||
<div className="new absolute -top-1 -left-1 h-6 w-6 bg-yellow-500 text-white text-xs rounded-full flex items-center justify-center">
|
||||
{chat.unreadCount > 99 ? '99+' : chat.unreadCount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="info-text flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h5 className="font-semibold text-sm truncate">{chat.name}</h5>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{chat.lastMessage ? formatTime(chat.lastMessage.timestamp) : ''}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{chat.lastMessage?.content || 'No messages yet'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Online Users Tab */}
|
||||
{activeTab === 'online' && (
|
||||
<div className="chat-list__in relative">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-semibold mb-4">Online Users</h2>
|
||||
<div className="online-visitor space-y-2">
|
||||
{onlineUsers.map((user) => (
|
||||
<div
|
||||
key={user.id}
|
||||
className="visitor-history flex items-center p-3 hover:bg-accent rounded-lg cursor-pointer"
|
||||
onClick={() => handleCreateChat(user.id)}
|
||||
>
|
||||
<div className="relative mr-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>{user.name?.charAt(0) || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="absolute -bottom-1 -right-1 h-3 w-3 bg-green-500 rounded-full border-2 border-background" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{user.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{user.email}</div>
|
||||
</div>
|
||||
<MessageCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contacts Tab */}
|
||||
{activeTab === 'contacts' && (
|
||||
<div className="chat-list__in relative">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-semibold mb-4">Contacts</h2>
|
||||
<div className="nav contact-list space-y-1">
|
||||
{chats.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
className="item-list item-list__contact flex items-center p-3 hover:bg-accent rounded-lg cursor-pointer"
|
||||
onClick={() => setSelectedChatId(chat.id)}
|
||||
>
|
||||
<div className="avatar relative mr-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={chat.avatar} />
|
||||
<AvatarFallback>{chat.name?.charAt(0) || 'C'}</AvatarFallback>
|
||||
</Avatar>
|
||||
{chat.online ? (
|
||||
<div className="status online absolute bottom-0 right-0 h-3 w-3 bg-green-500 rounded-full border-2 border-background" />
|
||||
) : (
|
||||
<div className="status offline absolute bottom-0 right-0 h-3 w-3 bg-gray-400 rounded-full border-2 border-background" />
|
||||
)}
|
||||
</div>
|
||||
<div className="info-text flex-1">
|
||||
<h5 className="font-semibold text-sm">{chat.name}</h5>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{chat.participants[0]?.email || 'No email'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="person-add">
|
||||
<UserCheck className="h-5 w-5 text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notifications Tab */}
|
||||
{activeTab === 'notifications' && (
|
||||
<div className="chat-list__in relative">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4">
|
||||
<h2 className="text-lg font-semibold mb-4">Notifications</h2>
|
||||
<div className="nav notification-list space-y-1">
|
||||
{chats.filter(chat => chat.unreadCount > 0).map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
className="item-list item-list__contact flex items-center p-3 hover:bg-accent rounded-lg cursor-pointer"
|
||||
onClick={() => setSelectedChatId(chat.id)}
|
||||
>
|
||||
<div className="avatar mr-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={chat.avatar} />
|
||||
<AvatarFallback>{chat.name?.charAt(0) || 'C'}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="info-text flex-1">
|
||||
<h5 className="font-semibold text-sm">
|
||||
{chat.name} sent you a message
|
||||
</h5>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{chat.lastMessage ? formatTime(chat.lastMessage.timestamp) : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Area */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{selectedChat ? (
|
||||
<>
|
||||
{/* Chat Header */}
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
<AvatarImage src={selectedChat.avatar} />
|
||||
<AvatarFallback>{selectedChat.name?.charAt(0) || 'C'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h2 className="font-semibold">{selectedChat.name || 'Unknown'}</h2>
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<Circle className="h-2 w-2 fill-green-500 text-green-500 mr-1" />
|
||||
{selectedChat.online ? 'Online' : 'Last seen 12 hour ago'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button size="sm" variant="outline">
|
||||
<Phone className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
<Video className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="message-content message-content-scroll bg-background">
|
||||
<div className="relative">
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-8">
|
||||
<div className="w-32 h-32 bg-orange-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<MessageCircle className="h-16 w-16 text-orange-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">This chat is empty.</h3>
|
||||
<p className="text-muted-foreground">Be the first one to start it.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Date Divider */}
|
||||
<div className="date flex items-center my-6">
|
||||
<hr className="flex-1 border-border" />
|
||||
<span className="px-4 text-sm text-muted-foreground bg-background">Yesterday</span>
|
||||
<hr className="flex-1 border-border" />
|
||||
</div>
|
||||
|
||||
{/* Sample Messages */}
|
||||
<div className="message flex items-start mb-4 px-4">
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
<AvatarImage src={selectedChat.avatar} />
|
||||
<AvatarFallback>{selectedChat.name?.charAt(0) || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-main flex-1">
|
||||
<span className="time-ago text-xs text-muted-foreground mb-2 block">09:46 AM</span>
|
||||
<div className="text-group">
|
||||
<div className="text bg-muted p-3 rounded-lg max-w-md">
|
||||
<p className="text-sm">It is a long established fact that a reader will be.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="message me flex items-start mb-4 px-4 justify-end">
|
||||
<div className="text-main flex-1 flex flex-col items-end">
|
||||
<span className="time-ago text-xs text-muted-foreground mb-2 block">11:32 AM</span>
|
||||
<div className="text-group me">
|
||||
<div className="text me bg-primary text-primary-foreground p-3 rounded-lg max-w-md">
|
||||
<p className="text-sm">By the readable content of a page when looking at its?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="message flex items-start mb-4 px-4">
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
<AvatarImage src={selectedChat.avatar} />
|
||||
<AvatarFallback>{selectedChat.name?.charAt(0) || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-main flex-1">
|
||||
<span className="time-ago text-xs text-muted-foreground mb-2 block">02:56 PM</span>
|
||||
<div className="text-group">
|
||||
<div className="text bg-muted p-3 rounded-lg max-w-md">
|
||||
<p className="text-sm">The point of using Lorem Ipsum is that it has a more-or-less normal distribution.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="message me flex items-start mb-4 px-4 justify-end">
|
||||
<div className="text-main flex-1 flex flex-col items-end">
|
||||
<span className="time-ago text-xs text-muted-foreground mb-2 block">10:21 PM</span>
|
||||
<div className="text-group me space-y-2">
|
||||
<div className="text me bg-primary text-primary-foreground p-3 rounded-lg max-w-md">
|
||||
<p className="text-sm">Roger that boss!</p>
|
||||
</div>
|
||||
<div className="text me bg-primary text-primary-foreground p-3 rounded-lg max-w-md">
|
||||
<p className="text-sm">Many desktop publishing packages and web page editors now use Lorem Ipsum as their!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="message flex items-start mb-4 px-4">
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
<AvatarImage src={selectedChat.avatar} />
|
||||
<AvatarFallback>{selectedChat.name?.charAt(0) || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-main flex-1">
|
||||
<span className="time-ago text-xs text-muted-foreground mb-2 block">11:07 PM</span>
|
||||
<div className="text-group">
|
||||
<div className="text bg-muted p-3 rounded-lg max-w-md">
|
||||
<p className="text-sm">Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Today Date Divider */}
|
||||
<div className="date flex items-center my-6">
|
||||
<hr className="flex-1 border-border" />
|
||||
<span className="px-4 text-sm text-muted-foreground bg-background">Today</span>
|
||||
<hr className="flex-1 border-border" />
|
||||
</div>
|
||||
|
||||
{/* File Attachment Message */}
|
||||
<div className="message flex items-start mb-4 px-4">
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
<AvatarImage src={selectedChat.avatar} />
|
||||
<AvatarFallback>{selectedChat.name?.charAt(0) || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-main flex-1">
|
||||
<span className="time-ago text-xs text-muted-foreground mb-2 block">11:07 PM</span>
|
||||
<div className="text-group">
|
||||
<div className="text bg-muted p-3 rounded-lg max-w-md">
|
||||
<div className="attachment flex items-center gap-3">
|
||||
<Button size="sm" className="p-2 h-auto">
|
||||
<Paperclip className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="file">
|
||||
<h5 className="text-sm font-medium">
|
||||
<a href="#" className="text-primary hover:underline">Documentations.pdf</a>
|
||||
</h5>
|
||||
<span className="text-xs text-muted-foreground">21kb Document</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* My Message with Read Receipt */}
|
||||
<div className="message me flex items-start mb-4 px-4 justify-end">
|
||||
<div className="text-main flex-1 flex flex-col items-end">
|
||||
<span className="time-ago text-xs text-muted-foreground mb-2 flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-check2-all" viewBox="0 0 16 16">
|
||||
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"></path>
|
||||
<path d="m5.354 7.146.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708"></path>
|
||||
</svg>
|
||||
10:21 PM
|
||||
</span>
|
||||
<div className="text-group me">
|
||||
<div className="text me bg-primary text-primary-foreground p-3 rounded-lg max-w-md">
|
||||
<p className="text-sm">If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Typing Indicator */}
|
||||
<div className="message flex items-start mb-4 px-4">
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
<AvatarImage src={selectedChat.avatar} />
|
||||
<AvatarFallback>{selectedChat.name?.charAt(0) || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-main flex-1">
|
||||
<div className="text-group">
|
||||
<div className="text typing bg-muted p-3 rounded-lg w-16">
|
||||
<div className="wave flex items-center justify-center space-x-1">
|
||||
<span className="dot w-2 h-2 bg-muted-foreground rounded-full animate-bounce [animation-delay:-0.3s]"></span>
|
||||
<span className="dot w-2 h-2 bg-muted-foreground rounded-full animate-bounce [animation-delay:-0.15s]"></span>
|
||||
<span className="dot w-2 h-2 bg-muted-foreground rounded-full animate-bounce"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Render dynamic messages */}
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`message flex items-start mb-4 px-4 ${message.isOwn ? 'me justify-end' : ''}`}
|
||||
>
|
||||
{!message.isOwn && (
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
<AvatarImage src={message.senderAvatar} />
|
||||
<AvatarFallback>{message.senderName?.charAt(0) || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<div className={`text-main flex-1 ${message.isOwn ? 'flex flex-col items-end' : ''}`}>
|
||||
<span className="time-ago text-xs text-muted-foreground mb-2 block">{message.timestamp}</span>
|
||||
<div className={`text-group ${message.isOwn ? 'me' : ''}`}>
|
||||
<div className={`text p-3 rounded-lg max-w-md ${
|
||||
message.isOwn
|
||||
? 'me bg-primary text-primary-foreground'
|
||||
: 'bg-muted'
|
||||
}`}>
|
||||
<p className="text-sm">{message.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Message Input */}
|
||||
<div className="p-4 border-t border-border">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button size="sm" variant="outline">
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Type a message here..."
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">
|
||||
<Smile className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!messageText.trim()}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<MessageCircle className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">Select a conversation</h3>
|
||||
<p className="text-muted-foreground">Choose from your existing conversations or start a new one</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chat Details Sidebar - Right */}
|
||||
{selectedChat && showChatDetails && (
|
||||
<div className="chat-list__sidebar--right w-80 border-l border-border bg-background">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4">
|
||||
{/* User Info */}
|
||||
<div className="chat-user__info flex items-center mb-6">
|
||||
<div className="avatar relative mr-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={selectedChat.avatar} />
|
||||
<AvatarFallback>{selectedChat.name?.charAt(0) || 'C'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="status online absolute bottom-0 right-0 h-3 w-3 bg-green-500 rounded-full border-2 border-background" />
|
||||
</div>
|
||||
<div className="info-text flex-1">
|
||||
<h5 className="text-lg font-semibold m-0">{selectedChat.name || 'Unknown'}</h5>
|
||||
<p className="text-sm text-muted-foreground writing">{selectedChat.name} typing a message</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Bot/Manual Toggle */}
|
||||
<div className="chatting_indicate bg-muted/50 p-4 rounded-lg mb-4">
|
||||
<h5 className="text-base font-semibold mb-2">Conversation With Auto bot or manual</h5>
|
||||
<p className="text-sm text-muted-foreground mb-4">Everyone in this conversation will see this.</p>
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<Label
|
||||
className={`cursor-pointer px-3 py-1 rounded ${isAutoBot ? 'bg-primary text-primary-foreground' : 'text-muted-foreground'}`}
|
||||
htmlFor="autobot"
|
||||
>
|
||||
Auto bot
|
||||
</Label>
|
||||
<div className="toggle">
|
||||
<Switch
|
||||
id="switcher"
|
||||
checked={!isAutoBot}
|
||||
onCheckedChange={(checked) => setIsAutoBot(!checked)}
|
||||
/>
|
||||
</div>
|
||||
<Label
|
||||
className={`cursor-pointer px-3 py-1 rounded ${!isAutoBot ? 'bg-primary text-primary-foreground' : 'text-muted-foreground'}`}
|
||||
htmlFor="manual"
|
||||
>
|
||||
Manual
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Accordion */}
|
||||
<Accordion type="single" collapsible defaultValue="change-color">
|
||||
{/* User Details */}
|
||||
<AccordionItem value="user-details">
|
||||
<AccordionTrigger className="text-left">
|
||||
<div className="flex items-center">
|
||||
<UserCheck className="h-5 w-5 mr-2" />
|
||||
User Details
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="user-info">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border border-border rounded">
|
||||
<tbody>
|
||||
<tr className="border-b border-border">
|
||||
<td className="user-info-first p-3 font-medium bg-muted/50">Name</td>
|
||||
<td className="p-3">{selectedChat.name || 'Unknown'}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="user-info-first p-3 font-medium bg-muted/50">ID</td>
|
||||
<td className="p-3">{selectedChat.id}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="user-info-first p-3 font-medium bg-muted/50">E-mail</td>
|
||||
<td className="p-3">{selectedChat.participants[0]?.email || 'example@email.com'}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="user-info-first p-3 font-medium bg-muted/50">URL</td>
|
||||
<td className="p-3">
|
||||
<a href="#" className="text-primary hover:underline">https://easital.com/</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="user-info-first p-3 font-medium bg-muted/50">Browser</td>
|
||||
<td className="p-3">Chrome</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Edit Name */}
|
||||
<AccordionItem value="edit-name">
|
||||
<AccordionTrigger className="text-left">
|
||||
<div className="flex items-center">
|
||||
<Edit3 className="h-5 w-5 mr-2" />
|
||||
Edit name
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4">
|
||||
<h5 className="font-semibold">Edit Nickname for {selectedChat.name}</h5>
|
||||
<p className="text-sm text-muted-foreground">Everyone in this conversation will see this.</p>
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={selectedChat.name || 'Enter nickname'}
|
||||
value={newNickname}
|
||||
onChange={(e) => setNewNickname(e.target.value)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm" variant="default">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Change Color */}
|
||||
<AccordionItem value="change-color">
|
||||
<AccordionTrigger className="text-left">
|
||||
<div className="flex items-center">
|
||||
<Palette className="h-5 w-5 mr-2" />
|
||||
Change color
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4">
|
||||
<h5 className="font-semibold">Pick a color for this conversation</h5>
|
||||
<p className="text-sm text-muted-foreground">Everyone in this conversation will see this.</p>
|
||||
<div className="radio-list change-bg-color grid grid-cols-5 gap-3">
|
||||
{[
|
||||
{ id: 'red', color: 'bg-red-500' },
|
||||
{ id: 'green', color: 'bg-green-500' },
|
||||
{ id: 'yellow', color: 'bg-yellow-500' },
|
||||
{ id: 'orange', color: 'bg-orange-500' },
|
||||
{ id: 'teal', color: 'bg-teal-500' },
|
||||
{ id: 'blue', color: 'bg-blue-500' },
|
||||
{ id: 'violet', color: 'bg-violet-500' },
|
||||
{ id: 'purple', color: 'bg-purple-500' },
|
||||
{ id: 'pink', color: 'bg-pink-500' },
|
||||
{ id: 'gray', color: 'bg-gray-500' }
|
||||
].map((colorOption) => (
|
||||
<label
|
||||
key={colorOption.id}
|
||||
className="cursor-pointer"
|
||||
htmlFor={colorOption.id}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="color"
|
||||
id={colorOption.id}
|
||||
value={colorOption.id}
|
||||
checked={selectedColor === colorOption.id}
|
||||
onChange={(e) => setSelectedColor(e.target.value)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<span
|
||||
className={`block w-8 h-8 rounded-full ${colorOption.color} ${
|
||||
selectedColor === colorOption.id ? 'ring-2 ring-offset-2 ring-primary' : ''
|
||||
}`}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Notifications */}
|
||||
<AccordionItem value="notifications">
|
||||
<AccordionTrigger className="text-left">
|
||||
<div className="flex items-center">
|
||||
<Bell className="h-5 w-5 mr-2" />
|
||||
Notifications
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4">
|
||||
<h5 className="font-semibold">Conversation Notifications</h5>
|
||||
<p className="text-sm text-muted-foreground">Everyone in this conversation will see this.</p>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="notifications1" />
|
||||
<Label htmlFor="notifications1" className="text-sm">
|
||||
Receive notifications for new messages
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="notifications2" />
|
||||
<Label htmlFor="notifications2" className="text-sm">
|
||||
Receive notifications for reactions
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm" variant="default">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Messages;
|
||||
209
src/pages/dashboard/MyListings.tsx
Normal file
209
src/pages/dashboard/MyListings.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useLanguage } from '@/contexts/LanguageContext';
|
||||
import { mockApi } from '@/services/mockApi';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Edit, Trash2, Eye, Star, MapPin, Plus } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const MyListings = () => {
|
||||
const { user } = useAuth();
|
||||
const { t } = useLanguage();
|
||||
const [listings, setListings] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMyListings = async () => {
|
||||
if (user) {
|
||||
try {
|
||||
const data = await mockApi.getUserListings(user.id) as any;
|
||||
setListings(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user listings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchMyListings();
|
||||
}, [user]);
|
||||
|
||||
const handleDeleteListing = (listingId: string) => {
|
||||
if (confirm('Are you sure you want to delete this listing?')) {
|
||||
setListings(listings.filter(listing => listing.id !== listingId));
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-lg">{t('loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">{t('myListings')}</h1>
|
||||
<p className="text-gray-600">Manage your business listings</p>
|
||||
</div>
|
||||
<Link to="/dashboard/add-listing">
|
||||
<Button className="bg-primary hover:bg-primary-dark">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t('addListing')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Eye className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-600">Total Views</p>
|
||||
<p className="text-2xl font-bold">2,431</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 bg-green-100 rounded-lg">
|
||||
<Star className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-600">Avg. Rating</p>
|
||||
<p className="text-2xl font-bold">4.8</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<MapPin className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-600">Active Listings</p>
|
||||
<p className="text-2xl font-bold">{listings.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<Plus className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-600">This Month</p>
|
||||
<p className="text-2xl font-bold">+12</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Listings Grid */}
|
||||
{listings.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Plus className="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">No listings yet</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Get started by creating your first listing to showcase your business.
|
||||
</p>
|
||||
<Link to="/dashboard/add-listing">
|
||||
<Button className="bg-primary hover:bg-primary-dark">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Your First Listing
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{listings.map((listing) => (
|
||||
<Card key={listing.id} className="overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<div className="relative h-48">
|
||||
<img
|
||||
src={listing.images[0] || 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400'}
|
||||
alt={listing.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute top-3 right-3">
|
||||
<Badge variant="secondary" className="bg-white/90">
|
||||
{listing.rating} ⭐
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="absolute top-3 left-3">
|
||||
<Badge className="bg-primary text-white capitalize">
|
||||
{listing.category}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1">
|
||||
{listing.title}
|
||||
</h3>
|
||||
<span className="text-lg font-bold text-primary">
|
||||
${listing.price}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-gray-600 mb-3">
|
||||
<MapPin className="w-4 h-4 mr-1" />
|
||||
<span className="text-sm line-clamp-1">{listing.location.address}</span>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
|
||||
{listing.description}
|
||||
</p>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
View
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Edit className="w-4 h-4 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDeleteListing(listing.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyListings;
|
||||
318
src/pages/dashboard/Profile.tsx
Normal file
318
src/pages/dashboard/Profile.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
User,
|
||||
Mail,
|
||||
Phone,
|
||||
MapPin,
|
||||
Calendar,
|
||||
Settings,
|
||||
Shield,
|
||||
CreditCard,
|
||||
Bell,
|
||||
Lock,
|
||||
Globe
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const Profile = () => {
|
||||
const { user } = useAuth();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
phone: (user as any)?.profile?.phone || '',
|
||||
address: (user as any)?.profile?.address || '',
|
||||
language: user?.preferences?.language || 'es'
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
// Simulate saving to localStorage
|
||||
const updatedUser = {
|
||||
...user,
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
profile: {
|
||||
...(user as any)?.profile,
|
||||
phone: formData.phone,
|
||||
address: formData.address
|
||||
},
|
||||
preferences: {
|
||||
...user?.preferences,
|
||||
language: formData.language
|
||||
}
|
||||
};
|
||||
|
||||
localStorage.setItem('karibeo-user', JSON.stringify(updatedUser));
|
||||
toast.success('Perfil actualizado correctamente');
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const getRoleDisplay = (role: string) => {
|
||||
const roleMap = {
|
||||
'super_admin': 'Super Administrador',
|
||||
'admin': 'Administrador',
|
||||
'tourist': 'Turista',
|
||||
'guide': 'Guía Turístico',
|
||||
'hotel': 'Hotel',
|
||||
'taxi': 'Taxista',
|
||||
'restaurant': 'Restaurante'
|
||||
};
|
||||
return roleMap[role as keyof typeof roleMap] || role;
|
||||
};
|
||||
|
||||
const getRoleBadgeColor = (role: string) => {
|
||||
const colorMap = {
|
||||
'super_admin': 'bg-gradient-to-r from-purple-500 to-pink-500',
|
||||
'admin': 'bg-gradient-to-r from-blue-500 to-cyan-500',
|
||||
'tourist': 'bg-gradient-to-r from-green-500 to-emerald-500',
|
||||
'guide': 'bg-gradient-to-r from-yellow-500 to-orange-500',
|
||||
'hotel': 'bg-gradient-to-r from-indigo-500 to-purple-500',
|
||||
'taxi': 'bg-gradient-to-r from-red-500 to-pink-500',
|
||||
'restaurant': 'bg-gradient-to-r from-orange-500 to-red-500'
|
||||
};
|
||||
return colorMap[role as keyof typeof colorMap] || 'bg-gray-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mobile-safe-top p-4 lg:p-6 space-y-4 lg:space-y-6">
|
||||
{/* Mobile Header */}
|
||||
<div className="mobile-header lg:hidden">
|
||||
<h1 className="mobile-title">Profile</h1>
|
||||
<p className="text-muted-foreground text-sm">Manage your account information</p>
|
||||
</div>
|
||||
|
||||
{/* Profile Header */}
|
||||
<div className="mobile-card">
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center space-y-4 lg:space-y-0 lg:space-x-4">
|
||||
<Avatar className="w-16 h-16 lg:w-20 lg:h-20 mx-auto lg:mx-0">
|
||||
<AvatarImage src={user?.avatar} />
|
||||
<AvatarFallback className="text-base lg:text-lg">
|
||||
{user?.name?.split(' ').map(n => n[0]).join('').toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 text-center lg:text-left">
|
||||
<h2 className="text-xl lg:text-2xl font-bold text-foreground">{user?.name}</h2>
|
||||
<p className="text-muted-foreground text-sm lg:text-base">{user?.email}</p>
|
||||
<div className="mt-2 flex flex-col sm:flex-row items-center justify-center lg:justify-start space-y-2 sm:space-y-0 sm:space-x-2">
|
||||
<Badge className={`text-white text-xs lg:text-sm ${getRoleBadgeColor(user?.role || '')}`}>
|
||||
<Shield className="w-3 h-3 mr-1" />
|
||||
{getRoleDisplay(user?.role || '')}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-xs lg:text-sm">
|
||||
<Calendar className="w-3 h-3 mr-1" />
|
||||
Miembro desde {(user as any)?.profile?.joinedDate || '2023'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
variant={isEditing ? "secondary" : "default"}
|
||||
className="mobile-btn w-full lg:w-auto"
|
||||
>
|
||||
{isEditing ? 'Cancelar' : 'Editar Perfil'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="personal" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-4 h-auto">
|
||||
<TabsTrigger value="personal" className="text-xs lg:text-sm px-2 lg:px-4 py-2">
|
||||
<span className="hidden sm:inline">Información </span>Personal
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security" className="text-xs lg:text-sm px-2 lg:px-4 py-2">Seguridad</TabsTrigger>
|
||||
<TabsTrigger value="preferences" className="text-xs lg:text-sm px-2 lg:px-4 py-2">Preferencias</TabsTrigger>
|
||||
<TabsTrigger value="billing" className="text-xs lg:text-sm px-2 lg:px-4 py-2">Facturación</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="personal" className="space-y-4">
|
||||
<div className="mobile-card">
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex items-center space-x-2 mb-4 lg:mb-6">
|
||||
<User className="w-4 h-4 lg:w-5 lg:h-5" />
|
||||
<span className="text-lg lg:text-xl font-semibold">Datos Personales</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-sm font-medium">Nombre Completo</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
disabled={!isEditing}
|
||||
className="mobile-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">Correo Electrónico</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
disabled={!isEditing}
|
||||
className="mobile-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone" className="text-sm font-medium">Teléfono</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
disabled={!isEditing}
|
||||
className="mobile-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address" className="text-sm font-medium">Dirección</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={formData.address}
|
||||
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
||||
disabled={!isEditing}
|
||||
className="mobile-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isEditing && (
|
||||
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-2 pt-4">
|
||||
<Button variant="outline" onClick={() => setIsEditing(false)} className="mobile-btn">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="mobile-btn">
|
||||
Guardar Cambios
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="security" className="space-y-4">
|
||||
<div className="mobile-card">
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex items-center space-x-2 mb-4 lg:mb-6">
|
||||
<Lock className="w-4 h-4 lg:w-5 lg:h-5" />
|
||||
<span className="text-lg lg:text-xl font-semibold">Configuración de Seguridad</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Cambiar Contraseña</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Actualiza tu contraseña periódicamente</p>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Cambiar</Button>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Autenticación de Dos Factores</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Agrega una capa extra de seguridad</p>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Configurar</Button>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Sesiones Activas</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Gestiona tus sesiones en otros dispositivos</p>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Ver Sesiones</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="preferences" className="space-y-4">
|
||||
<div className="mobile-card">
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex items-center space-x-2 mb-4 lg:mb-6">
|
||||
<Settings className="w-4 h-4 lg:w-5 lg:h-5" />
|
||||
<span className="text-lg lg:text-xl font-semibold">Preferencias</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Globe className="w-4 h-4 lg:w-5 lg:h-5 text-muted-foreground flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Idioma</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Español</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Cambiar</Button>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Bell className="w-4 h-4 lg:w-5 lg:h-5 text-muted-foreground flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Notificaciones</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Email y push activadas</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Configurar</Button>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex items-center space-x-3">
|
||||
<MapPin className="w-4 h-4 lg:w-5 lg:h-5 text-muted-foreground flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Ubicación</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Compartir ubicación para mejores recomendaciones</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Gestionar</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="billing" className="space-y-4">
|
||||
<div className="mobile-card">
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="flex items-center space-x-2 mb-4 lg:mb-6">
|
||||
<CreditCard className="w-4 h-4 lg:w-5 lg:h-5" />
|
||||
<span className="text-lg lg:text-xl font-semibold">Información de Facturación</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Métodos de Pago</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Gestiona tus tarjetas y métodos de pago</p>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Gestionar</Button>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Historial de Facturas</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Ver todas tus facturas y recibos</p>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Ver Historial</Button>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between p-3 lg:p-4 border rounded-lg space-y-3 lg:space-y-0">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-sm lg:text-base">Suscripción</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">Plan actual y renovación</p>
|
||||
</div>
|
||||
<Button variant="outline" className="mobile-btn lg:w-auto">Ver Plan</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
405
src/pages/dashboard/Reviews.tsx
Normal file
405
src/pages/dashboard/Reviews.tsx
Normal file
@@ -0,0 +1,405 @@
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import {
|
||||
Star,
|
||||
ThumbsUp,
|
||||
MessageCircle,
|
||||
Share2,
|
||||
Camera,
|
||||
Send,
|
||||
Heart,
|
||||
Reply,
|
||||
Image,
|
||||
Smile
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { ReviewReplyDialog } from '@/components/dashboard/ReviewReplyDialog';
|
||||
import { ReviewPhotoUpload } from '@/components/dashboard/ReviewPhotoUpload';
|
||||
|
||||
interface ReviewReply {
|
||||
id: string;
|
||||
authorId: string;
|
||||
authorName: string;
|
||||
authorAvatar: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
interface Review {
|
||||
id: string;
|
||||
itemId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userAvatar: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
images: string[];
|
||||
createdAt: string;
|
||||
helpful: number;
|
||||
isHelpful: boolean;
|
||||
replies: ReviewReply[];
|
||||
canReply: boolean;
|
||||
}
|
||||
|
||||
const Reviews = () => {
|
||||
const { user } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [replyingToReview, setReplyingToReview] = useState<string | null>(null);
|
||||
const [replyContent, setReplyContent] = useState('');
|
||||
const [showPhotoUpload, setShowPhotoUpload] = useState(false);
|
||||
const [selectedReviewForReply, setSelectedReviewForReply] = useState<Review | null>(null);
|
||||
|
||||
// Sample reviews data with full functionality
|
||||
const [reviews, setReviews] = useState<Review[]>([
|
||||
{
|
||||
id: '1',
|
||||
itemId: 'item_001',
|
||||
userId: 'user_001',
|
||||
userName: 'Ethan Blackwood',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=center',
|
||||
rating: 3.5,
|
||||
comment: 'There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which.',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=200&h=200&fit=crop',
|
||||
'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=200&h=200&fit=crop',
|
||||
'https://images.unsplash.com/photo-1551632811-561732d1e306?w=200&h=200&fit=crop'
|
||||
],
|
||||
createdAt: '25 Oct 2023 at 12:27 pm',
|
||||
helpful: 16,
|
||||
isHelpful: false,
|
||||
canReply: true,
|
||||
replies: [
|
||||
{
|
||||
id: 'reply_1',
|
||||
authorId: 'owner_001',
|
||||
authorName: 'Hotel Owner',
|
||||
authorAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=center',
|
||||
content: 'Thank you for your feedback! We appreciate your honest review and are working to improve our services.',
|
||||
createdAt: '26 Oct 2023 at 10:15 am',
|
||||
images: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
itemId: 'item_001',
|
||||
userId: 'user_002',
|
||||
userName: 'Gabriel North',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=center',
|
||||
rating: 4.0,
|
||||
comment: 'This is some content from a media component. You can replace this with any content and adjust it as needed.',
|
||||
images: [],
|
||||
createdAt: '25 Oct 2023 at 12:27 pm',
|
||||
helpful: 16,
|
||||
isHelpful: true,
|
||||
canReply: true,
|
||||
replies: []
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
itemId: 'item_001',
|
||||
userId: 'user_003',
|
||||
userName: 'Pranoti Deshpande',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b5bc?w=100&h=100&fit=crop&crop=center',
|
||||
rating: 3.5,
|
||||
comment: 'There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don\'t look even slightly believable.',
|
||||
images: [],
|
||||
createdAt: '25 Oct 2023 at 12:27 pm',
|
||||
helpful: 8,
|
||||
isHelpful: false,
|
||||
canReply: true,
|
||||
replies: []
|
||||
}
|
||||
]);
|
||||
|
||||
// Rating statistics
|
||||
const ratingStats = {
|
||||
average: 4.3,
|
||||
totalRatings: 2525,
|
||||
totalReviews: 293,
|
||||
breakdown: {
|
||||
5: { count: 1138, percentage: 45 },
|
||||
4: { count: 883, percentage: 35 },
|
||||
3: { count: 379, percentage: 15 },
|
||||
2: { count: 808, percentage: 32 },
|
||||
1: { count: 1742, percentage: 69 }
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number, size = 16) => {
|
||||
const stars = [];
|
||||
const fullStars = Math.floor(rating);
|
||||
const hasHalfStar = rating % 1 !== 0;
|
||||
|
||||
for (let i = 0; i < fullStars; i++) {
|
||||
stars.push(
|
||||
<Star
|
||||
key={i}
|
||||
size={size}
|
||||
className="text-yellow-400 fill-yellow-400"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasHalfStar) {
|
||||
stars.push(
|
||||
<Star
|
||||
key="half"
|
||||
size={size}
|
||||
className="text-yellow-400 fill-yellow-400 opacity-50"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const remainingStars = 5 - Math.ceil(rating);
|
||||
for (let i = 0; i < remainingStars; i++) {
|
||||
stars.push(
|
||||
<Star
|
||||
key={`empty-${i}`}
|
||||
size={size}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return stars;
|
||||
};
|
||||
|
||||
const handleHelpfulClick = (reviewId: string) => {
|
||||
setReviews(prevReviews =>
|
||||
prevReviews.map(review =>
|
||||
review.id === reviewId
|
||||
? {
|
||||
...review,
|
||||
isHelpful: !review.isHelpful,
|
||||
helpful: review.isHelpful ? review.helpful - 1 : review.helpful + 1
|
||||
}
|
||||
: review
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleReplySubmit = (reviewId: string, content: string, images: string[] = []) => {
|
||||
const newReply: ReviewReply = {
|
||||
id: `reply_${Date.now()}`,
|
||||
authorId: user?.id || 'current_user',
|
||||
authorName: user?.name || 'Business Owner',
|
||||
authorAvatar: user?.avatar || 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=center',
|
||||
content,
|
||||
createdAt: new Date().toLocaleString(),
|
||||
images
|
||||
};
|
||||
|
||||
setReviews(prevReviews =>
|
||||
prevReviews.map(review =>
|
||||
review.id === reviewId
|
||||
? { ...review, replies: [...review.replies, newReply] }
|
||||
: review
|
||||
)
|
||||
);
|
||||
|
||||
setReplyingToReview(null);
|
||||
setReplyContent('');
|
||||
};
|
||||
|
||||
const openReplyDialog = (review: Review) => {
|
||||
setSelectedReviewForReply(review);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mobile-safe-top p-4 lg:p-6 space-y-4 lg:space-y-6">
|
||||
{/* Mobile Header */}
|
||||
<div className="mobile-header">
|
||||
<h1 className="mobile-title">Visitor Reviews</h1>
|
||||
<p className="text-muted-foreground text-sm">Manage and respond to customer reviews</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6">
|
||||
{/* Reviews Summary */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="mobile-card">
|
||||
<div className="p-4 lg:p-6 space-y-4 lg:space-y-6">
|
||||
{/* Average Rating */}
|
||||
<div className="text-center">
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-2">Average user rating</h3>
|
||||
<div className="relative inline-block">
|
||||
<div className="w-16 h-16 lg:w-20 lg:h-20 border-4 border-primary rounded-full flex items-center justify-center mb-3">
|
||||
<span className="text-xl lg:text-2xl font-bold text-primary">{ratingStats.average}</span>
|
||||
</div>
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<Star className="w-5 h-5 lg:w-6 lg:h-6 text-primary fill-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">
|
||||
{ratingStats.totalRatings.toLocaleString()} Ratings & {ratingStats.totalReviews} Reviews
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Rating Breakdown */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-4">Rating breakdown</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(ratingStats.breakdown)
|
||||
.reverse()
|
||||
.map(([rating, data]) => (
|
||||
<div key={rating} className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium w-2">{rating}</span>
|
||||
<div className="flex-1">
|
||||
<Progress
|
||||
value={data.percentage}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="secondary" className="text-xs px-2 py-0.5">
|
||||
{data.count}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reviews List */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="mobile-card">
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="space-y-4 lg:space-y-6">
|
||||
{reviews.map((review) => (
|
||||
<div key={review.id} className="border-b border-border pb-4 lg:pb-6 last:border-b-0 last:pb-0">
|
||||
<div className="flex gap-3 lg:gap-4">
|
||||
<Avatar className="h-10 w-10 lg:h-12 lg:w-12 flex-shrink-0">
|
||||
<AvatarImage src={review.userAvatar} alt={review.userName} />
|
||||
<AvatarFallback>{review.userName.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex-1 space-y-2 lg:space-y-3 min-w-0">
|
||||
{/* Review Header */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between space-y-2 lg:space-y-0">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm lg:text-base">- {review.userName}</h4>
|
||||
<p className="text-xs lg:text-sm text-muted-foreground">{review.createdAt}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center">
|
||||
{renderStars(review.rating, 14)}
|
||||
</div>
|
||||
<span className="text-xs lg:text-sm font-medium">{review.rating}/5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Review Content */}
|
||||
<p className="text-muted-foreground leading-relaxed text-sm lg:text-base">{review.comment}</p>
|
||||
|
||||
{/* Review Images */}
|
||||
{review.images.length > 0 && (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{review.images.map((image, index) => (
|
||||
<div key={index} className="relative group cursor-pointer">
|
||||
<img
|
||||
src={image}
|
||||
alt={`Review image ${index + 1}`}
|
||||
className="w-16 h-16 lg:w-20 lg:h-20 rounded-lg object-cover transition-transform group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 rounded-lg transition-colors" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Review Actions */}
|
||||
<div className="flex flex-wrap items-center gap-2 pt-2">
|
||||
<Button
|
||||
variant={review.isHelpful ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => handleHelpfulClick(review.id)}
|
||||
className="h-8 text-xs lg:text-sm"
|
||||
>
|
||||
<ThumbsUp className="h-3 w-3 lg:h-4 lg:w-4 mr-1" />
|
||||
<span className="hidden sm:inline">Helpful</span> {review.helpful}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openReplyDialog(review)}
|
||||
className="h-8 text-xs lg:text-sm"
|
||||
>
|
||||
<MessageCircle className="h-3 w-3 lg:h-4 lg:w-4 mr-1" />
|
||||
Reply
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" className="h-8 text-xs lg:text-sm">
|
||||
<Share2 className="h-3 w-3 lg:h-4 lg:w-4 mr-1" />
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Replies */}
|
||||
{review.replies.length > 0 && (
|
||||
<div className="ml-2 lg:ml-4 space-y-3 lg:space-y-4 pt-3 lg:pt-4 border-l-2 border-muted pl-3 lg:pl-4">
|
||||
{review.replies.map((reply) => (
|
||||
<div key={reply.id} className="flex gap-2 lg:gap-3">
|
||||
<Avatar className="h-6 w-6 lg:h-8 lg:w-8 flex-shrink-0">
|
||||
<AvatarImage src={reply.authorAvatar} alt={reply.authorName} />
|
||||
<AvatarFallback className="text-xs">{reply.authorName.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="bg-muted rounded-lg p-2 lg:p-3">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-1 lg:gap-2 mb-1">
|
||||
<span className="text-xs lg:text-sm font-medium">{reply.authorName}</span>
|
||||
<span className="text-xs text-muted-foreground">{reply.createdAt}</span>
|
||||
</div>
|
||||
<p className="text-xs lg:text-sm">{reply.content}</p>
|
||||
</div>
|
||||
{reply.images && reply.images.length > 0 && (
|
||||
<div className="flex gap-1 mt-2">
|
||||
{reply.images.map((image, idx) => (
|
||||
<img
|
||||
key={idx}
|
||||
src={image}
|
||||
alt={`Reply image ${idx + 1}`}
|
||||
className="w-12 h-12 lg:w-16 lg:h-16 rounded object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reply Dialog */}
|
||||
{selectedReviewForReply && (
|
||||
<ReviewReplyDialog
|
||||
review={selectedReviewForReply}
|
||||
onReply={handleReplySubmit}
|
||||
onClose={() => setSelectedReviewForReply(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reviews;
|
||||
376
src/pages/dashboard/Settings.tsx
Normal file
376
src/pages/dashboard/Settings.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
User,
|
||||
Camera,
|
||||
Settings as SettingsIcon,
|
||||
Bell,
|
||||
Shield,
|
||||
CreditCard,
|
||||
Globe
|
||||
} from 'lucide-react';
|
||||
|
||||
const Settings = () => {
|
||||
const { user, login } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [profileData, setProfileData] = useState({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
phone: (user as any)?.phone || '',
|
||||
description: (user as any)?.bio || '',
|
||||
location: typeof user?.location === 'string' ? user.location : user?.location?.lat ? `${user.location.lat}, ${user.location.lng}` : '',
|
||||
facebook: (user as any)?.socialLinks?.facebook || '',
|
||||
twitter: (user as any)?.socialLinks?.twitter || '',
|
||||
instagram: (user as any)?.socialLinks?.instagram || '',
|
||||
linkedin: (user as any)?.socialLinks?.linkedin || '',
|
||||
});
|
||||
|
||||
const handleProfileUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Update user context with new data
|
||||
const updatedUser = {
|
||||
...user!,
|
||||
name: profileData.name,
|
||||
email: profileData.email,
|
||||
phone: profileData.phone,
|
||||
bio: profileData.description,
|
||||
location: profileData.location,
|
||||
socialLinks: {
|
||||
facebook: profileData.facebook,
|
||||
twitter: profileData.twitter,
|
||||
instagram: profileData.instagram,
|
||||
linkedin: profileData.linkedin,
|
||||
}
|
||||
};
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
|
||||
toast.success('Profile updated successfully!');
|
||||
} catch (error) {
|
||||
toast.error('Failed to update profile');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setProfileData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-6xl mx-auto">
|
||||
{/* Header Section */}
|
||||
<div className="relative mb-8">
|
||||
<div className="h-48 bg-gradient-to-r from-orange-400 to-orange-500 rounded-lg relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-black/20"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="absolute top-4 right-4 bg-white/10 border-white/20 text-white hover:bg-white/20"
|
||||
>
|
||||
<Camera className="w-4 h-4 mr-2" />
|
||||
Upload header
|
||||
</Button>
|
||||
|
||||
<div className="absolute bottom-6 left-6 flex items-end space-x-4">
|
||||
<div className="relative">
|
||||
<div className="w-24 h-24 rounded-full border-4 border-white overflow-hidden bg-white">
|
||||
<img
|
||||
src={user?.avatar || "/placeholder.svg"}
|
||||
alt={user?.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="absolute -bottom-2 -right-2 w-8 h-8 p-0 rounded-full"
|
||||
>
|
||||
<Camera className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-white pb-2">
|
||||
<h1 className="text-2xl font-bold flex items-center">
|
||||
{user?.name}
|
||||
<div className="w-5 h-5 bg-green-500 rounded-full ml-2 flex items-center justify-center">
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</h1>
|
||||
<p className="text-white/80 flex items-center mt-1">
|
||||
<span className="mr-2">📍</span>
|
||||
{typeof user?.location === 'string' ? user.location : 'San Francisco, US'}
|
||||
<span className="mx-2">•</span>
|
||||
<span className="text-sm">Joined Oct 2023</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Content */}
|
||||
<Tabs defaultValue="details" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="details" className="flex items-center space-x-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>Details</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security" className="flex items-center space-x-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
<span>Security</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="notifications" className="flex items-center space-x-2">
|
||||
<Bell className="w-4 h-4" />
|
||||
<span>Notifications</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="billing" className="flex items-center space-x-2">
|
||||
<CreditCard className="w-4 h-4" />
|
||||
<span>Billing</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="details">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center text-red-500">
|
||||
<div className="w-1 h-6 bg-red-500 mr-3"></div>
|
||||
Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleProfileUpdate} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="name">Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={profileData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
placeholder="Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="phone">Phone *</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={profileData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="(123) 456 - 789"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">Email Address *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={profileData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="example@email.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">Description *</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={profileData.description}
|
||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||
placeholder="Please enter up to 4000 characters."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="facebook">Facebook Page (optional)</Label>
|
||||
<Input
|
||||
id="facebook"
|
||||
value={profileData.facebook}
|
||||
onChange={(e) => handleInputChange('facebook', e.target.value)}
|
||||
placeholder="https://facebook.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="twitter">Twitter profile (optional)</Label>
|
||||
<Input
|
||||
id="twitter"
|
||||
value={profileData.twitter}
|
||||
onChange={(e) => handleInputChange('twitter', e.target.value)}
|
||||
placeholder="https://twitter.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="instagram">Instagram profile (optional)</Label>
|
||||
<Input
|
||||
id="instagram"
|
||||
value={profileData.instagram}
|
||||
onChange={(e) => handleInputChange('instagram', e.target.value)}
|
||||
placeholder="https://instagram.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="linkedin">LinkedIn page (optional)</Label>
|
||||
<Input
|
||||
id="linkedin"
|
||||
value={profileData.linkedin}
|
||||
onChange={(e) => handleInputChange('linkedin', e.target.value)}
|
||||
placeholder="https://linkedin.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
{isLoading ? 'Updating...' : 'Update Profile'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="security">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center text-red-500">
|
||||
<div className="w-1 h-6 bg-red-500 mr-3"></div>
|
||||
Security Settings
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="current-password">Current Password</Label>
|
||||
<Input
|
||||
id="current-password"
|
||||
type="password"
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="new-password">New Password</Label>
|
||||
<Input
|
||||
id="new-password"
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="confirm-password">Confirm New Password</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</div>
|
||||
<Button className="bg-red-500 hover:bg-red-600">
|
||||
Update Password
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notifications">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center text-red-500">
|
||||
<div className="w-1 h-6 bg-red-500 mr-3"></div>
|
||||
Notification Preferences
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium">Email Notifications</h4>
|
||||
<p className="text-sm text-muted-foreground">Receive notifications via email</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Enable</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium">Push Notifications</h4>
|
||||
<p className="text-sm text-muted-foreground">Receive push notifications</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Enable</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium">SMS Notifications</h4>
|
||||
<p className="text-sm text-muted-foreground">Receive notifications via SMS</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Enable</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="billing">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center text-red-500">
|
||||
<div className="w-1 h-6 bg-red-500 mr-3"></div>
|
||||
Billing Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="card-number">Card Number</Label>
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="**** **** **** 1234"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="expiry">Expiry Date</Label>
|
||||
<Input
|
||||
id="expiry"
|
||||
placeholder="MM/YY"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="cvv">CVV</Label>
|
||||
<Input
|
||||
id="cvv"
|
||||
placeholder="123"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="billing-name">Cardholder Name</Label>
|
||||
<Input
|
||||
id="billing-name"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="bg-red-500 hover:bg-red-600">
|
||||
Update Billing Info
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
264
src/pages/dashboard/Wallet.tsx
Normal file
264
src/pages/dashboard/Wallet.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import {
|
||||
Wallet as WalletIcon,
|
||||
DollarSign,
|
||||
TrendingUp,
|
||||
ShoppingCart,
|
||||
Package,
|
||||
Calendar,
|
||||
Clock
|
||||
} from 'lucide-react';
|
||||
|
||||
const Wallet = () => {
|
||||
const { user } = useAuth();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate loading
|
||||
setTimeout(() => setLoading(false), 1000);
|
||||
}, []);
|
||||
|
||||
// Sample earnings data
|
||||
const earningsData = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Green Mart Apartment',
|
||||
amount: 99.00,
|
||||
fee: 10.00,
|
||||
date: '2024-01-15',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Ocean View Villa',
|
||||
amount: 299.00,
|
||||
fee: 25.00,
|
||||
date: '2024-01-14',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Mountain Cabin Resort',
|
||||
amount: 199.00,
|
||||
fee: 15.00,
|
||||
date: '2024-01-13',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'City Center Loft',
|
||||
amount: 149.00,
|
||||
fee: 12.00,
|
||||
date: '2024-01-12',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Beach House Paradise',
|
||||
amount: 399.00,
|
||||
fee: 35.00,
|
||||
date: '2024-01-11',
|
||||
status: 'completed'
|
||||
}
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-3"></div>
|
||||
<div className="text-muted-foreground">Loading wallet...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mobile-safe-top p-4 lg:p-6 space-y-4 lg:space-y-8">
|
||||
{/* Page Header */}
|
||||
<div className="mobile-header">
|
||||
<h1 className="mobile-title">Wallet</h1>
|
||||
<p className="text-muted-foreground text-sm">Manage your earnings and transactions</p>
|
||||
</div>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="mobile-grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="mobile-card relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-12 h-12 lg:w-16 lg:h-16 opacity-10">
|
||||
<WalletIcon className="w-full h-full text-primary" />
|
||||
</div>
|
||||
<div className="text-xs lg:text-sm text-muted-foreground mb-1">Withdrawable Balance</div>
|
||||
<h3 className="text-lg lg:text-2xl font-bold">$5,899 <span className="text-xs lg:text-sm text-muted-foreground font-normal">(USD)</span></h3>
|
||||
</div>
|
||||
|
||||
<div className="mobile-card relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-12 h-12 lg:w-16 lg:h-16 opacity-10">
|
||||
<TrendingUp className="w-full h-full text-primary" />
|
||||
</div>
|
||||
<div className="text-xs lg:text-sm text-muted-foreground mb-1">Total Earning</div>
|
||||
<h3 className="text-lg lg:text-2xl font-bold">$8,899 <span className="text-xs lg:text-sm text-muted-foreground font-normal">(USD)</span></h3>
|
||||
</div>
|
||||
|
||||
<div className="mobile-card relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-12 h-12 lg:w-16 lg:h-16 opacity-10">
|
||||
<Clock className="w-full h-full text-primary" />
|
||||
</div>
|
||||
<div className="text-xs lg:text-sm text-muted-foreground mb-1">Pending Orders</div>
|
||||
<h3 className="text-lg lg:text-2xl font-bold">$1,299 <span className="text-xs lg:text-sm text-muted-foreground font-normal">(USD)</span></h3>
|
||||
</div>
|
||||
|
||||
<div className="mobile-card relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-12 h-12 lg:w-16 lg:h-16 opacity-10">
|
||||
<Package className="w-full h-full text-primary" />
|
||||
</div>
|
||||
<div className="text-xs lg:text-sm text-muted-foreground mb-1">Total Orders</div>
|
||||
<h3 className="text-lg lg:text-2xl font-bold">659</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
|
||||
{/* Earnings Table */}
|
||||
<div className="mobile-card overflow-hidden">
|
||||
<div className="p-4 border-b border-border">
|
||||
<h3 className="text-lg font-semibold">Listings Earning</h3>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
<div className="space-y-0">
|
||||
{earningsData.map((item) => (
|
||||
<div key={item.id} className="p-4 border-b border-border last:border-b-0 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 lg:w-10 lg:h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<ShoppingCart className="w-4 h-4 lg:w-5 lg:h-5 text-primary" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h5 className="font-medium text-foreground text-sm lg:text-base truncate">{item.name}</h5>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-1 lg:gap-3 text-xs lg:text-sm mt-1">
|
||||
<span className="text-green-600 font-medium">${item.amount.toFixed(2)}</span>
|
||||
<span className="text-primary">Fee: ${item.fee.toFixed(2)}</span>
|
||||
<span className="text-muted-foreground">{item.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-badge">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
item.status === 'completed'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
||||
}`}>
|
||||
{item.status === 'completed' ? 'Completed' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Withdrawal History */}
|
||||
<div className="mobile-card overflow-hidden">
|
||||
<div className="p-4 border-b border-border">
|
||||
<h3 className="text-lg font-semibold">Recent Withdrawals</h3>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
<div className="space-y-0">
|
||||
<div className="p-4 border-b border-border hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 lg:w-10 lg:h-10 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center flex-shrink-0">
|
||||
<DollarSign className="w-4 h-4 lg:w-5 lg:h-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h5 className="font-medium text-foreground text-sm lg:text-base">Bank Transfer</h5>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-1 lg:gap-3 text-xs lg:text-sm mt-1">
|
||||
<span className="text-green-600 font-medium">$500.00</span>
|
||||
<span className="text-muted-foreground">2024-01-10</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-badge">
|
||||
<span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Completed
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-b border-border hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 lg:w-10 lg:h-10 rounded-full bg-yellow-100 dark:bg-yellow-900 flex items-center justify-center flex-shrink-0">
|
||||
<DollarSign className="w-4 h-4 lg:w-5 lg:h-5 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h5 className="font-medium text-foreground text-sm lg:text-base">PayPal Transfer</h5>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-1 lg:gap-3 text-xs lg:text-sm mt-1">
|
||||
<span className="text-yellow-600 font-medium">$300.00</span>
|
||||
<span className="text-muted-foreground">2024-01-08</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-badge">
|
||||
<span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
|
||||
Processing
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 lg:w-10 lg:h-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center flex-shrink-0">
|
||||
<DollarSign className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h5 className="font-medium text-foreground text-sm lg:text-base">Card Transfer</h5>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-1 lg:gap-3 text-xs lg:text-sm mt-1">
|
||||
<span className="text-blue-600 font-medium">$750.00</span>
|
||||
<span className="text-muted-foreground">2024-01-05</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-badge">
|
||||
<span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Completed
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="mobile-card">
|
||||
<h3 className="text-lg font-semibold mb-4 text-center">Wallet Actions</h3>
|
||||
<div className="mobile-grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<button className="mobile-btn bg-primary text-primary-foreground hover:bg-primary/90">
|
||||
<DollarSign className="w-4 h-4 lg:w-5 lg:h-5" />
|
||||
<span className="text-sm lg:text-base">Add Funds</span>
|
||||
</button>
|
||||
<button className="mobile-btn bg-green-600 text-white hover:bg-green-700">
|
||||
<TrendingUp className="w-4 h-4 lg:w-5 lg:h-5" />
|
||||
<span className="text-sm lg:text-base">Withdraw</span>
|
||||
</button>
|
||||
<button className="mobile-btn bg-blue-600 text-white hover:bg-blue-700">
|
||||
<Clock className="w-4 h-4 lg:w-5 lg:h-5" />
|
||||
<span className="text-sm lg:text-base">History</span>
|
||||
</button>
|
||||
<button className="mobile-btn border border-border text-foreground hover:bg-muted">
|
||||
<WalletIcon className="w-4 h-4 lg:w-5 lg:h-5" />
|
||||
<span className="text-sm lg:text-base">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wallet;
|
||||
Reference in New Issue
Block a user