rating
This commit is contained in:
112
web/src/components/ReviewModal.tsx
Normal file
112
web/src/components/ReviewModal.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @file ReviewModal.tsx
|
||||
* @description Modal de avaliação pós-atendimento com estrelas interativas.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { Star, X, Send } from 'lucide-react'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
interface ReviewModalProps {
|
||||
shopName: string
|
||||
appointmentId: string
|
||||
onSubmit: (rating: number, comment: string) => Promise<void>
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function ReviewModal({ shopName, appointmentId, onSubmit, onClose }: ReviewModalProps) {
|
||||
const [rating, setRating] = useState(0)
|
||||
const [hovered, setHovered] = useState(0)
|
||||
const [comment, setComment] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (rating === 0) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await onSubmit(rating, comment)
|
||||
setSubmitted(true)
|
||||
setTimeout(onClose, 1800)
|
||||
} catch {
|
||||
alert('Erro ao enviar avaliação. Tente novamente.')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const labels = ['', 'Muito mau', 'Mau', 'Razoável', 'Bom', 'Excelente!']
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm p-6 relative animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||
{/* Close */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 p-1 rounded-full text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
|
||||
{submitted ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="text-5xl mb-3">🎉</div>
|
||||
<h3 className="text-xl font-bold text-slate-900">Obrigado pela avaliação!</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">O seu feedback ajuda outros clientes.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-14 h-14 bg-gradient-to-br from-amber-400 to-amber-600 rounded-2xl flex items-center justify-center mx-auto mb-3 shadow-md">
|
||||
<Star size={28} className="text-white fill-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-900">Avaliar atendimento</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">{shopName}</p>
|
||||
</div>
|
||||
|
||||
{/* Stars */}
|
||||
<div className="flex justify-center gap-2 mb-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
onClick={() => setRating(star)}
|
||||
onMouseEnter={() => setHovered(star)}
|
||||
onMouseLeave={() => setHovered(0)}
|
||||
className="transition-transform hover:scale-125 active:scale-110"
|
||||
>
|
||||
<Star
|
||||
size={36}
|
||||
className={`transition-colors ${star <= (hovered || rating)
|
||||
? 'text-amber-400 fill-amber-400'
|
||||
: 'text-slate-300'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center text-sm font-medium text-amber-600 mb-5 h-5">
|
||||
{labels[hovered || rating]}
|
||||
</p>
|
||||
|
||||
{/* Comment */}
|
||||
<textarea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder="Quer deixar um comentário? (opcional)"
|
||||
rows={3}
|
||||
className="w-full border border-slate-200 rounded-xl p-3 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent mb-4 placeholder:text-slate-400"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={rating === 0 || submitting}
|
||||
className="w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<Send size={16} />
|
||||
{submitting ? 'A enviar...' : 'Enviar Avaliação'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -43,6 +43,7 @@ type AppContextValue = State & {
|
||||
updateBarber: (shopId: string, barber: Barber) => Promise<void>;
|
||||
deleteBarber: (shopId: string, barberId: string) => Promise<void>;
|
||||
updateShopDetails: (shopId: string, payload: Partial<BarberShop>) => Promise<void>;
|
||||
submitReview: (shopId: string, appointmentId: string, rating: number, comment: string) => Promise<void>;
|
||||
refreshShops: () => Promise<void>;
|
||||
shopsReady: boolean;
|
||||
};
|
||||
@@ -597,6 +598,26 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const submitReview = async (shopId: string, appointmentId: string, rating: number, comment: string) => {
|
||||
const { data: authData } = await supabase.auth.getUser();
|
||||
const customerId = authData?.user?.id;
|
||||
if (!customerId) { alert('Sess\u00e3o expirada. Faz login novamente.'); return; }
|
||||
|
||||
const { error } = await supabase.from('reviews').insert([{
|
||||
shop_id: shopId,
|
||||
customer_id: customerId,
|
||||
appointment_id: appointmentId,
|
||||
rating,
|
||||
comment: comment.trim() || null,
|
||||
}]);
|
||||
|
||||
if (error) {
|
||||
console.error('Erro submitReview:', error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const value: AppContextValue = {
|
||||
...state,
|
||||
login,
|
||||
@@ -621,6 +642,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
updateBarber,
|
||||
deleteBarber,
|
||||
updateShopDetails,
|
||||
submitReview,
|
||||
refreshShops,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
/**
|
||||
* @file Profile.tsx
|
||||
* @description Página do Perfil do utilizador web. Carrega a sessão atual a partir do Supabase,
|
||||
* interceta eventos customizados, agendamentos associados ao `customerId` e pedidos de produtos (Orders).
|
||||
* @description Página do Perfil do utilizador web.
|
||||
* Mostra agendamentos, pedidos, barbearias favoritas e permite avaliar atendimentos concluídos.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Card } from '../components/ui/card'
|
||||
import { Badge } from '../components/ui/badge'
|
||||
import { Button } from '../components/ui/button'
|
||||
import { currency } from '../lib/format'
|
||||
import { useApp } from '../context/AppContext'
|
||||
import { Calendar, ShoppingBag, User, Clock } from 'lucide-react'
|
||||
import { Calendar, ShoppingBag, User, Clock, Heart, Star, MapPin } from 'lucide-react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { ReviewModal } from '../components/ReviewModal'
|
||||
|
||||
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
|
||||
pendente: 'amber',
|
||||
@@ -27,15 +29,17 @@ const statusLabel: Record<string, string> = {
|
||||
}
|
||||
|
||||
export default function Profile() {
|
||||
const { appointments, orders, shops } = useApp()
|
||||
const location = useLocation()
|
||||
const { appointments, orders, shops, favorites, submitReview } = useApp()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// ✅ Utilizador Supabase
|
||||
const [authEmail, setAuthEmail] = useState<string>('')
|
||||
const [authId, setAuthId] = useState<string>('')
|
||||
const [loadingAuth, setLoadingAuth] = useState(true)
|
||||
|
||||
// Avaliações já enviadas (para esconder o botão se já foi avaliado)
|
||||
const [reviewedAppointments, setReviewedAppointments] = useState<Set<string>>(new Set())
|
||||
const [reviewTarget, setReviewTarget] = useState<{ appointmentId: string; shopId: string; shopName: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
; (async () => {
|
||||
@@ -47,6 +51,14 @@ export default function Profile() {
|
||||
} else {
|
||||
setAuthEmail(data.user.email ?? '')
|
||||
setAuthId(data.user.id)
|
||||
// Carregar avaliações já feitas por este utilizador
|
||||
const { data: reviews } = await supabase
|
||||
.from('reviews')
|
||||
.select('appointment_id')
|
||||
.eq('customer_id', data.user.id)
|
||||
if (reviews) {
|
||||
setReviewedAppointments(new Set(reviews.map((r: any) => r.appointment_id).filter(Boolean)))
|
||||
}
|
||||
}
|
||||
setLoadingAuth(false)
|
||||
})()
|
||||
@@ -63,22 +75,40 @@ export default function Profile() {
|
||||
return orders.filter((o) => o.customerId === authId)
|
||||
}, [orders, authId])
|
||||
|
||||
const favoriteShops = useMemo(() => {
|
||||
return shops.filter((s) => favorites.includes(s.id))
|
||||
}, [shops, favorites])
|
||||
|
||||
const handleReviewSubmit = async (rating: number, comment: string) => {
|
||||
if (!reviewTarget) return
|
||||
await submitReview(reviewTarget.shopId, reviewTarget.appointmentId, rating, comment)
|
||||
setReviewedAppointments((prev) => new Set([...prev, reviewTarget.appointmentId]))
|
||||
setReviewTarget(null)
|
||||
}
|
||||
|
||||
if (loadingAuth) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-slate-600">A carregar perfil...</p>
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 border-4 border-slate-200 border-t-amber-500 rounded-full animate-spin mx-auto mb-3" />
|
||||
<p className="text-slate-500 text-sm">A carregar perfil...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!authId) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-slate-600">Sessão não encontrada. Faz login novamente.</p>
|
||||
<div className="text-center py-16">
|
||||
<div className="w-16 h-16 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<User size={28} className="text-amber-600" />
|
||||
</div>
|
||||
<p className="text-slate-700 font-semibold mb-1">Sessão não encontrada</p>
|
||||
<p className="text-slate-500 text-sm mb-4">Faz login para ver o teu perfil.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/login', { replace: true })}
|
||||
className="mt-4 text-sm font-semibold text-amber-700 hover:text-amber-800"
|
||||
className="px-5 py-2 bg-amber-500 text-white text-sm font-semibold rounded-xl hover:bg-amber-600 transition-colors"
|
||||
>
|
||||
Ir para login
|
||||
</button>
|
||||
@@ -89,126 +119,191 @@ export default function Profile() {
|
||||
const displayName = authEmail ? authEmail.split('@')[0] : 'Utilizador'
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Profile Header */}
|
||||
<Card className="p-6 bg-gradient-to-br from-amber-50 to-white">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-4 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg">
|
||||
<User size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Olá, {displayName}</h1>
|
||||
<p className="text-sm text-slate-600">{authEmail}</p>
|
||||
<Badge color="amber" variant="soft" className="mt-2">
|
||||
Cliente Barber
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<>
|
||||
{/* Modal de avaliação */}
|
||||
{reviewTarget && (
|
||||
<ReviewModal
|
||||
shopName={reviewTarget.shopName}
|
||||
appointmentId={reviewTarget.appointmentId}
|
||||
onSubmit={handleReviewSubmit}
|
||||
onClose={() => setReviewTarget(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Appointments Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={20} className="text-amber-600" />
|
||||
<h2 className="text-xl font-bold text-slate-900">Agendamentos</h2>
|
||||
<Badge color="slate" variant="soft">
|
||||
{myAppointments.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{/* Profile Header */}
|
||||
<Card className="p-6 bg-gradient-to-br from-amber-50 via-white to-orange-50 border-amber-100">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-amber-400 to-amber-600 rounded-2xl flex items-center justify-center text-white shadow-lg flex-shrink-0">
|
||||
<User size={28} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-2xl font-bold text-slate-900 truncate">Olá, {displayName}!</h1>
|
||||
<p className="text-sm text-slate-500 truncate">{authEmail}</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge color="amber" variant="soft">Cliente</Badge>
|
||||
{favoriteShops.length > 0 && (
|
||||
<span className="flex items-center gap-1 text-xs text-rose-500 font-medium">
|
||||
<Heart size={12} className="fill-rose-500" /> {favoriteShops.length} favorita{favoriteShops.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{!myAppointments.length ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium">Nenhum agendamento ainda</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Explore barbearias e agende seu primeiro serviço!</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{myAppointments.map((a) => {
|
||||
const shop = shops.find((s) => s.id === a.shopId)
|
||||
const service = shop?.services.find((s) => s.id === a.serviceId)
|
||||
return (
|
||||
<Card key={a.id} hover className="p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-bold text-slate-900">{shop?.name}</h3>
|
||||
<Badge color={statusColor[a.status]} variant="soft">
|
||||
{statusLabel[a.status]}
|
||||
</Badge>
|
||||
{/* ❤️ Barbearias Favoritas */}
|
||||
{favoriteShops.length > 0 && (
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Heart size={20} className="text-rose-500 fill-rose-500" />
|
||||
<h2 className="text-xl font-bold text-slate-900">Barbearias Favoritas</h2>
|
||||
<Badge color="red" variant="soft">{favoriteShops.length}</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{favoriteShops.map((shop) => (
|
||||
<Link key={shop.id} to={`/barbearia/${shop.id}`}>
|
||||
<Card hover className="p-4 flex items-center gap-3 group">
|
||||
{shop.imageUrl ? (
|
||||
<img src={shop.imageUrl} alt={shop.name} className="w-14 h-14 rounded-xl object-cover flex-shrink-0" />
|
||||
) : (
|
||||
<div className="w-14 h-14 bg-gradient-to-br from-slate-100 to-slate-200 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<User size={20} className="text-slate-400" />
|
||||
</div>
|
||||
{service && (
|
||||
<p className="text-sm text-slate-600 flex items-center gap-1">
|
||||
<Clock size={14} />
|
||||
{service.name} · {service.duration} min
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-bold text-slate-900 truncate group-hover:text-amber-700 transition-colors">{shop.name}</p>
|
||||
{shop.address && (
|
||||
<p className="text-xs text-slate-500 flex items-center gap-1 mt-0.5 truncate">
|
||||
<MapPin size={10} /> {shop.address}
|
||||
</p>
|
||||
)}
|
||||
{shop.rating > 0 && (
|
||||
<p className="text-xs text-amber-600 flex items-center gap-1 mt-1">
|
||||
<Star size={10} className="fill-amber-400 text-amber-400" />
|
||||
{shop.rating.toFixed(1)}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-slate-500">{a.date}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold text-amber-600">{currency(a.total)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Orders Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingBag size={20} className="text-amber-600" />
|
||||
<h2 className="text-xl font-bold text-slate-900">Pedidos</h2>
|
||||
<Badge color="slate" variant="soft">
|
||||
{myOrders.length}
|
||||
</Badge>
|
||||
</div>
|
||||
{/* Agendamentos */}
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={20} className="text-amber-600" />
|
||||
<h2 className="text-xl font-bold text-slate-900">Agendamentos</h2>
|
||||
<Badge color="slate" variant="soft">{myAppointments.length}</Badge>
|
||||
</div>
|
||||
|
||||
{!myOrders.length ? (
|
||||
<Card className="p-8 text-center">
|
||||
<ShoppingBag size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium">Nenhum pedido ainda</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Adicione produtos ao carrinho e finalize seu primeiro pedido!</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{myOrders.map((o) => {
|
||||
const shop = shops.find((s) => s.id === o.shopId)
|
||||
return (
|
||||
<Card key={o.id} hover className="p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-bold text-slate-900">{shop?.name}</h3>
|
||||
<Badge color={statusColor[o.status]} variant="soft">
|
||||
{statusLabel[o.status]}
|
||||
</Badge>
|
||||
{!myAppointments.length ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium">Nenhum agendamento ainda</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Explore barbearias e agende o seu primeiro serviço!</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{myAppointments.map((a) => {
|
||||
const shop = shops.find((s) => s.id === a.shopId)
|
||||
const service = shop?.services.find((s) => s.id === a.serviceId)
|
||||
const canReview = a.status === 'concluido' && !reviewedAppointments.has(a.id)
|
||||
return (
|
||||
<Card key={a.id} hover className="p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-bold text-slate-900">{shop?.name}</h3>
|
||||
<Badge color={statusColor[a.status]} variant="soft">
|
||||
{statusLabel[a.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
{service && (
|
||||
<p className="text-sm text-slate-600 flex items-center gap-1">
|
||||
<Clock size={13} />
|
||||
{service.name} · {service.duration} min
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-slate-500">{a.date}</p>
|
||||
{/* Botão de avaliar */}
|
||||
{canReview && (
|
||||
<button
|
||||
onClick={() => setReviewTarget({ appointmentId: a.id, shopId: a.shopId, shopName: shop?.name ?? 'Barbearia' })}
|
||||
className="flex items-center gap-1.5 mt-2 text-xs font-semibold text-amber-600 hover:text-amber-700 bg-amber-50 hover:bg-amber-100 px-3 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
<Star size={12} className="fill-amber-400 text-amber-400" />
|
||||
Avaliar atendimento
|
||||
</button>
|
||||
)}
|
||||
{a.status === 'concluido' && reviewedAppointments.has(a.id) && (
|
||||
<p className="text-xs text-green-600 flex items-center gap-1 mt-1">
|
||||
✓ Avaliado
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="text-lg font-bold text-amber-600">{currency(a.total)}</p>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
{new Date(o.createdAt).toLocaleDateString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
{o.items.length} {o.items.length === 1 ? 'item' : 'itens'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold text-amber-600">{currency(o.total)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Pedidos */}
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingBag size={20} className="text-amber-600" />
|
||||
<h2 className="text-xl font-bold text-slate-900">Pedidos</h2>
|
||||
<Badge color="slate" variant="soft">{myOrders.length}</Badge>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{!myOrders.length ? (
|
||||
<Card className="p-8 text-center">
|
||||
<ShoppingBag size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium">Nenhum pedido ainda</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Adicione produtos ao carrinho e finalize o primeiro pedido!</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{myOrders.map((o) => {
|
||||
const shop = shops.find((s) => s.id === o.shopId)
|
||||
return (
|
||||
<Card key={o.id} hover className="p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-bold text-slate-900">{shop?.name}</h3>
|
||||
<Badge color={statusColor[o.status]} variant="soft">
|
||||
{statusLabel[o.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
{new Date(o.createdAt).toLocaleDateString('pt-PT', {
|
||||
day: '2-digit', month: 'long', year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
{o.items.length} {o.items.length === 1 ? 'item' : 'itens'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="text-lg font-bold text-amber-600">{currency(o.total)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user