eventscreate

This commit is contained in:
2026-01-28 10:34:30 +00:00
parent 51e5705104
commit e0acd9879c
6 changed files with 316 additions and 71 deletions

View File

@@ -6,7 +6,7 @@ import { supabase } from '../../lib/supabase'
import { signOut } from '../../lib/auth'
export const Header = () => {
const { cart } = useApp()
const { cart, user } = useApp()
const navigate = useNavigate()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
@@ -51,28 +51,41 @@ export const Header = () => {
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-4">
<Link
to="/explorar"
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
>
<MapPin size={16} />
<span>Barbearias</span>
</Link>
{user?.role !== 'barbearia' && (
<>
<Link
to="/explorar"
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
>
<MapPin size={16} />
<span>Barbearias</span>
</Link>
<Link
to="/carrinho"
className="relative text-slate-700 hover:text-indigo-600 transition-colors p-2 rounded-lg hover:bg-indigo-50"
>
<ShoppingCart size={18} />
{cart.length > 0 && (
<span className="absolute -right-1 -top-1 rounded-full bg-gradient-to-r from-indigo-500 to-blue-600 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-sm min-w-[18px] text-center">
{cart.length}
</span>
)}
</Link>
<Link
to="/carrinho"
className="relative text-slate-700 hover:text-indigo-600 transition-colors p-2 rounded-lg hover:bg-indigo-50"
>
<ShoppingCart size={18} />
{cart.length > 0 && (
<span className="absolute -right-1 -top-1 rounded-full bg-gradient-to-r from-indigo-500 to-blue-600 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-sm min-w-[18px] text-center">
{cart.length}
</span>
)}
</Link>
</>
)}
{isAuthed ? (
<div className="flex items-center gap-2">
{user?.role === 'barbearia' && (
<button
onClick={() => navigate('/painel')}
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
type="button"
>
<span>Painel</span>
</button>
)}
<button
onClick={() => navigate('/perfil')}
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
@@ -115,31 +128,47 @@ export const Header = () => {
{mobileMenuOpen && (
<div className="md:hidden border-t border-slate-200/60 bg-white/95 backdrop-blur-md animate-in slide-in-from-top">
<nav className="px-4 py-3 space-y-2">
<Link
to="/explorar"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
>
<MapPin size={16} />
Barbearias
</Link>
{user?.role !== 'barbearia' && (
<>
<Link
to="/explorar"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
>
<MapPin size={16} />
Barbearias
</Link>
<Link
to="/carrinho"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
>
<ShoppingCart size={16} />
Carrinho
{cart.length > 0 && (
<span className="ml-auto rounded-full bg-amber-500 px-2 py-0.5 text-[10px] font-bold text-white">
{cart.length}
</span>
)}
</Link>
<Link
to="/carrinho"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
>
<ShoppingCart size={16} />
Carrinho
{cart.length > 0 && (
<span className="ml-auto rounded-full bg-amber-500 px-2 py-0.5 text-[10px] font-bold text-white">
{cart.length}
</span>
)}
</Link>
</>
)}
{isAuthed ? (
<>
{user?.role === 'barbearia' && (
<button
onClick={() => {
navigate('/painel')
setMobileMenuOpen(false)
}}
className="w-full flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50 text-left"
type="button"
>
Painel
</button>
)}
<button
onClick={() => {
navigate('/perfil')

View File

@@ -3,6 +3,7 @@ import { nanoid } from 'nanoid';
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
import { mockShops, mockUsers } from '../data/mock';
import { storage } from '../lib/storage';
import { supabase } from '../lib/supabase';
type State = {
user?: User;
@@ -64,6 +65,98 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
storage.set('smart-agenda', state);
}, [state]);
useEffect(() => {
let mounted = true;
const clearUser = () => {
setState((s) => ({ ...s, user: undefined }));
};
const applyProfile = async (userId: string, email?: string | null) => {
const { data, error } = await supabase
.from('profiles')
.select('id,name,role,shop_name')
.eq('id', userId)
.single();
if (!mounted) return;
if (error || !data) {
clearUser();
return;
}
const role = data.role === 'barbearia' ? 'barbearia' : 'cliente';
const displayName = data.name?.trim() || (email ? email.split('@')[0] : 'Utilizador');
const shopId = role === 'barbearia' ? userId : undefined;
setState((s) => {
const nextUser: User = {
id: userId,
name: displayName,
email: email ?? '',
password: '',
role,
shopId,
};
const users = s.users.some((u) => u.id === userId)
? s.users.map((u) => (u.id === userId ? { ...u, ...nextUser } : u))
: [...s.users, nextUser];
let shops = s.shops;
if (role === 'barbearia' && shopId) {
const exists = shops.some((shop) => shop.id === shopId);
if (!exists) {
const shopName = data.shop_name?.trim() || `Barbearia ${displayName}`;
const shop: BarberShop = {
id: shopId,
name: shopName,
address: 'Endereço a definir',
rating: 0,
barbers: [],
services: [],
products: [],
};
shops = [...shops, shop];
}
}
return {
...s,
user: nextUser,
users,
shops,
};
});
};
const boot = async () => {
const { data } = await supabase.auth.getSession();
if (!mounted) return;
const session = data.session;
if (session?.user) {
await applyProfile(session.user.id, session.user.email);
} else {
clearUser();
}
};
void boot();
const { data: sub } = supabase.auth.onAuthStateChange((_event, session) => {
if (session?.user) {
void applyProfile(session.user.id, session.user.email);
} else {
clearUser();
}
});
return () => {
mounted = false;
sub.subscription.unsubscribe();
};
}, []);
const login = (email: string, password: string) => {
const found = state.users.find((u) => u.email === email && u.password === password);
if (found) {

View File

@@ -9,18 +9,42 @@ export type EventRow = {
user_id: string
}
// LISTAR eventos do utilizador logado
// LISTAR eventos (RLS deve garantir que só vês os teus)
export async function listEvents() {
const user = await getUser()
if (!user) {
throw new Error('Utilizador não autenticado')
}
if (!user) throw new Error('Utilizador não autenticado')
const { data, error } = await supabase
.from('events')
.select('*')
.select('id,title,description,date,user_id')
.order('date', { ascending: true })
if (error) throw error
return (data ?? []) as EventRow[]
}
// CRIAR evento
export async function createEvent(input: {
title: string
description?: string
date: string
}) {
const user = await getUser()
if (!user) throw new Error('Utilizador não autenticado')
const { data, error } = await supabase
.from('events')
.insert({
title: input.title,
description: input.description ?? null,
date: input.date,
// user_id:
// - Se a tua coluna user_id tem DEFAULT auth.uid(), NÃO metas aqui.
// - Se não tem default, então mete: user_id: user.id
})
.select('id,title,description,date,user_id')
.single()
if (error) throw error
return data as EventRow
}

View File

@@ -0,0 +1,74 @@
import { FormEvent, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card } from '../components/ui/card'
import { Input } from '../components/ui/input'
import { Button } from '../components/ui/button'
import { createEvent } from '../lib/events'
export default function EventsCreate() {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [date, setDate] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
async function onSubmit(e: FormEvent) {
e.preventDefault()
setError('')
if (!title.trim()) return setError('Título obrigatório')
if (!date) return setError('Data obrigatória')
setLoading(true)
try {
await createEvent({ title, description, date })
navigate('/perfil', { replace: true })
} catch (e: any) {
setError(e?.message ?? 'Erro ao guardar evento')
} finally {
setLoading(false)
}
}
return (
<div className="max-w-md mx-auto py-8">
<Card className="p-6 space-y-4">
<h1 className="text-xl font-bold text-slate-900">Criar evento</h1>
{error && (
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
{error}
</div>
)}
<form className="space-y-4" onSubmit={onSubmit}>
<Input
label="Título"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Ex: Corte às 15h"
/>
<Input
label="Descrição"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Opcional"
/>
<Input
label="Data"
type="datetime-local"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
<Button className="w-full" disabled={loading}>
{loading ? 'A guardar...' : 'Guardar'}
</Button>
</form>
</Card>
</div>
)
}

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Card } from '../components/ui/card'
import { Badge } from '../components/ui/badge'
import { currency } from '../lib/format'
@@ -23,6 +24,7 @@ const statusLabel: Record<string, string> = {
export default function Profile() {
const { appointments, orders, shops } = useApp()
const location = useLocation()
// ✅ Utilizador Supabase
const [authEmail, setAuthEmail] = useState<string>('')
@@ -56,22 +58,33 @@ export default function Profile() {
}
}, [])
async function loadEvents() {
try {
setLoadingEvents(true)
setEventsError('')
const data = await listEvents()
setEvents(data)
} catch (e: any) {
setEventsError(e?.message || 'Erro ao carregar eventos')
} finally {
setLoadingEvents(false)
}
}
// Carrega eventos ao entrar no perfil
useEffect(() => {
;(async () => {
try {
setEventsError('')
const data = await listEvents()
setEvents(data)
} catch (e: any) {
setEventsError(e?.message || 'Erro ao carregar eventos')
} finally {
setLoadingEvents(false)
}
})()
loadEvents()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Se quiseres filtrar por utilizador Supabase, fica assim.
// (Só vai funcionar quando os teus appointments/orders tiverem customerId = authId)
// ✅ Recarrega eventos sempre que muda a rota (ex.: voltas de /eventos/novo para /perfil)
useEffect(() => {
if (location.pathname === '/perfil') {
loadEvents()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname])
const myAppointments = useMemo(() => {
if (!authId) return []
return appointments.filter((a) => a.customerId === authId)
@@ -82,10 +95,6 @@ export default function Profile() {
return orders.filter((o) => o.customerId === authId)
}, [orders, authId])
// Para já, se ainda não tens customerId igual ao authId, podes mostrar tudo:
// const myAppointments = appointments
// const myOrders = orders
if (loadingAuth) {
return (
<div className="text-center py-12">
@@ -94,7 +103,6 @@ export default function Profile() {
)
}
// Se não houver user Supabase aqui, em teoria o RequireAuth já devia redirecionar.
if (!authId) {
return (
<div className="text-center py-12">
@@ -128,9 +136,17 @@ export default function Profile() {
<div className="flex items-center gap-2">
<Calendar size={20} className="text-amber-600" />
<h2 className="text-xl font-bold text-slate-900">Eventos (Supabase)</h2>
<Badge color="slate" variant="soft">
{loadingEvents ? '…' : events.length}
</Badge>
<Link
to="/eventos/novo"
className="ml-auto text-sm font-semibold text-amber-700 hover:text-amber-800 transition-colors"
>
+ Criar evento
</Link>
</div>
{loadingEvents ? (
@@ -154,12 +170,8 @@ export default function Profile() {
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<h3 className="font-bold text-slate-900">{ev.title}</h3>
{ev.description && (
<p className="text-sm text-slate-600">{ev.description}</p>
)}
<p className="text-xs text-slate-500">
{new Date(ev.date).toLocaleString()}
</p>
{ev.description && <p className="text-sm text-slate-600">{ev.description}</p>}
<p className="text-xs text-slate-500">{new Date(ev.date).toLocaleString()}</p>
</div>
</div>
</Card>
@@ -173,7 +185,9 @@ export default function Profile() {
<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>
<Badge color="slate" variant="soft">
{myAppointments.length}
</Badge>
</div>
{!myAppointments.length ? (
@@ -221,7 +235,9 @@ export default function Profile() {
<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>
<Badge color="slate" variant="soft">
{myOrders.length}
</Badge>
</div>
{!myOrders.length ? (

View File

@@ -11,6 +11,7 @@ import Booking from './pages/Booking'
import Cart from './pages/Cart'
import Profile from './pages/Profile'
import Dashboard from './pages/Dashboard'
import EventsCreate from './pages/EventsCreate'
export const router = createBrowserRouter([
{
@@ -55,6 +56,14 @@ export const router = createBrowserRouter([
</RequireAuth>
),
},
{
path: '/eventos/novo',
element: (
<RequireAuth>
<EventsCreate />
</RequireAuth>
),
},
],
},
])