Remove unnecessary blank lines in ShopCard and Explore components
This commit is contained in:
@@ -57,3 +57,4 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,21 +9,27 @@ export type EventRow = {
|
||||
user_id: string
|
||||
}
|
||||
|
||||
// LISTAR eventos (RLS deve garantir que só vês os teus)
|
||||
/**
|
||||
* LISTAR eventos do utilizador autenticado
|
||||
* (RLS garante isolamento por user_id)
|
||||
*/
|
||||
export async function listEvents() {
|
||||
const user = await getUser()
|
||||
if (!user) throw new Error('Utilizador não autenticado')
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('events')
|
||||
.select('id,title,description,date,user_id')
|
||||
.select('id, title, description, date, user_id')
|
||||
.order('date', { ascending: true })
|
||||
|
||||
if (error) throw error
|
||||
return (data ?? []) as EventRow[]
|
||||
}
|
||||
|
||||
// CRIAR evento
|
||||
/**
|
||||
* CRIAR evento
|
||||
* user_id vem automaticamente de auth.uid()
|
||||
*/
|
||||
export async function createEvent(input: {
|
||||
title: string
|
||||
description?: string
|
||||
@@ -38,11 +44,9 @@ export async function createEvent(input: {
|
||||
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
|
||||
// ❌ NÃO enviar user_id se tiver DEFAULT auth.uid()
|
||||
})
|
||||
.select('id,title,description,date,user_id')
|
||||
.select('id, title, description, date, user_id')
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormEvent, useState } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Input } from '../components/ui/input'
|
||||
import { Button } from '../components/ui/button'
|
||||
import { Card } from '../components/ui/card'
|
||||
@@ -17,61 +17,66 @@ export default function AuthRegister() {
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const onSubmit = async (e: FormEvent) => {
|
||||
// 🔐 Se já estiver logado, não pode ver o registo
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
;(async () => {
|
||||
const { data } = await supabase.auth.getSession()
|
||||
if (!mounted) return
|
||||
if (data.session) {
|
||||
navigate('/explorar', { replace: true })
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [navigate])
|
||||
|
||||
async function onSubmit(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (!name.trim()) return setError('Preencha o nome completo')
|
||||
if (!email.trim()) return setError('Preencha o email')
|
||||
if (!password.trim()) return setError('Preencha a senha')
|
||||
if (role === 'barbearia' && !shopName.trim()) return setError('Informe o nome da barbearia')
|
||||
|
||||
setLoading(true)
|
||||
if (role === 'barbearia' && !shopName.trim()) {
|
||||
return setError('Informe o nome da barbearia')
|
||||
}
|
||||
|
||||
try {
|
||||
// 1) Criar conta no Supabase Auth
|
||||
const { data, error: signUpError } = await supabase.auth.signUp({
|
||||
setLoading(true)
|
||||
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
name,
|
||||
name: name.trim(),
|
||||
role,
|
||||
shop_name: role === 'barbearia' ? shopName : null,
|
||||
shopName: role === 'barbearia' ? shopName.trim() : null,
|
||||
},
|
||||
},
|
||||
})
|
||||
if (signUpError) throw signUpError
|
||||
|
||||
const userId = data.user?.id
|
||||
if (!userId) {
|
||||
// Em alguns casos o user pode exigir confirmação por email
|
||||
// Mesmo assim, mostramos uma mensagem útil.
|
||||
throw new Error('Conta criada, mas sessão não iniciada. Verifica o email para confirmar a conta.')
|
||||
if (error) throw error
|
||||
|
||||
// 🔔 Se confirmação de email estiver ON
|
||||
if (!data.session) {
|
||||
navigate('/login', {
|
||||
replace: true,
|
||||
state: {
|
||||
msg: 'Conta criada! Confirma o email antes de fazer login.',
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2) Criar perfil na tabela public.profiles
|
||||
const { error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.upsert(
|
||||
{
|
||||
id: userId,
|
||||
name,
|
||||
role,
|
||||
shop_name: role === 'barbearia' ? shopName : null,
|
||||
},
|
||||
{ onConflict: 'id' }
|
||||
)
|
||||
if (profileError) throw profileError
|
||||
|
||||
// 3) Redirecionar
|
||||
navigate('/explorar', { replace: true })
|
||||
} catch (e: any) {
|
||||
// Mensagens comuns
|
||||
const msg =
|
||||
e?.message ||
|
||||
(typeof e === 'string' ? e : 'Erro ao criar conta')
|
||||
setError(msg)
|
||||
// ✅ Login automático
|
||||
navigate(role === 'barbearia' ? '/painel' : '/explorar', {
|
||||
replace: true,
|
||||
})
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Erro ao criar conta')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -84,8 +89,12 @@ export default function AuthRegister() {
|
||||
<div className="inline-flex p-3 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg mb-2">
|
||||
<UserPlus size={24} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Criar conta</h1>
|
||||
<p className="text-sm text-slate-600">Escolha o tipo de acesso</p>
|
||||
<h1 className="text-2xl font-bold text-slate-900">
|
||||
Criar conta
|
||||
</h1>
|
||||
<p className="text-sm text-slate-600">
|
||||
Escolha o tipo de acesso
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -95,9 +104,11 @@ export default function AuthRegister() {
|
||||
)}
|
||||
|
||||
<form className="space-y-5" onSubmit={onSubmit}>
|
||||
{/* Role Selection */}
|
||||
{/* Tipo de conta */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-700">Tipo de conta</label>
|
||||
<label className="text-sm font-medium text-slate-700">
|
||||
Tipo de conta
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{(['cliente', 'barbearia'] as const).map((r) => (
|
||||
<button
|
||||
@@ -109,8 +120,8 @@ export default function AuthRegister() {
|
||||
}}
|
||||
className={`p-4 rounded-xl border-2 transition-all ${
|
||||
role === r
|
||||
? 'border-amber-500 bg-gradient-to-br from-amber-50 to-amber-100/50 shadow-md'
|
||||
: 'border-slate-200 hover:border-amber-300 hover:bg-amber-50/50'
|
||||
? 'border-amber-500 bg-amber-50 shadow-md'
|
||||
: 'border-slate-200 hover:border-amber-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
@@ -119,7 +130,7 @@ export default function AuthRegister() {
|
||||
) : (
|
||||
<Scissors size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
|
||||
)}
|
||||
<span className={`text-sm font-semibold ${role === r ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||
<span className="text-sm font-semibold">
|
||||
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -131,56 +142,51 @@ export default function AuthRegister() {
|
||||
<Input
|
||||
label="Nome completo"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="João Silva"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="seu@email.com"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Senha"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
|
||||
{role === 'barbearia' && (
|
||||
<Input
|
||||
label="Nome da barbearia"
|
||||
value={shopName}
|
||||
onChange={(e) => {
|
||||
setShopName(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
onChange={(e) => setShopName(e.target.value)}
|
||||
placeholder="Barbearia XPTO"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading}>
|
||||
{loading ? 'A criar...' : 'Criar conta'}
|
||||
{loading ? 'A criar conta…' : 'Criar conta'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center pt-4 border-t border-slate-200">
|
||||
<p className="text-sm text-slate-600">
|
||||
Já tem conta?{' '}
|
||||
<Link to="/login" className="text-amber-700 font-semibold hover:text-amber-800 transition-colors">
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-amber-700 font-semibold hover:text-amber-800"
|
||||
>
|
||||
Entrar
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -1,40 +1,67 @@
|
||||
// src/pages/EventsCreate.tsx
|
||||
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'
|
||||
import { CalendarPlus } from 'lucide-react'
|
||||
|
||||
export default function EventsCreate() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [title, setTitle] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [date, setDate] = useState('')
|
||||
const [date, setDate] = useState('') // datetime-local
|
||||
const [saving, setSaving] = useState(false)
|
||||
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')
|
||||
if (!title.trim()) {
|
||||
setError('Preenche o título')
|
||||
return
|
||||
}
|
||||
if (!date) {
|
||||
setError('Escolhe a data e hora')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await createEvent({ title, description, date })
|
||||
setSaving(true)
|
||||
|
||||
// datetime-local -> ISO string (mantendo o valor como está)
|
||||
// Ex: "2026-01-28T10:30"
|
||||
const iso = new Date(date).toISOString()
|
||||
|
||||
await createEvent({
|
||||
title: title.trim(),
|
||||
description: description.trim() ? description.trim() : undefined,
|
||||
date: iso,
|
||||
})
|
||||
|
||||
navigate('/perfil', { replace: true })
|
||||
} catch (e: any) {
|
||||
setError(e?.message ?? 'Erro ao guardar evento')
|
||||
setError(e?.message || 'Erro ao criar evento')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setSaving(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>
|
||||
<div className="max-w-xl mx-auto py-8">
|
||||
<Card className="p-8 space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg">
|
||||
<CalendarPlus size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Criar evento</h1>
|
||||
<p className="text-sm text-slate-600">Este evento vai aparecer no teu perfil</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||||
@@ -46,27 +73,44 @@ export default function EventsCreate() {
|
||||
<Input
|
||||
label="Título"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Ex: Corte às 15h"
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
placeholder="Ex: Corte às 18h"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Descrição"
|
||||
label="Descrição (opcional)"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Opcional"
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
placeholder="Notas do evento..."
|
||||
/>
|
||||
|
||||
{/* Se o teu componente Input não suportar type datetime-local, troca por <input> normal */}
|
||||
<Input
|
||||
label="Data"
|
||||
label="Data e hora"
|
||||
type="datetime-local"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setDate(e.target.value)
|
||||
setError('')
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Button className="w-full" disabled={loading}>
|
||||
{loading ? 'A guardar...' : 'Guardar'}
|
||||
</Button>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button type="button" variant="outline" className="w-full" onClick={() => navigate('/perfil')}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" className="w-full" disabled={saving}>
|
||||
{saving ? 'A guardar…' : 'Criar evento'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -115,4 +115,3 @@ export default function Explore() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/pages/Profile.tsx
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Card } from '../components/ui/card'
|
||||
import { Badge } from '../components/ui/badge'
|
||||
import { currency } from '../lib/format'
|
||||
@@ -25,6 +26,7 @@ const statusLabel: Record<string, string> = {
|
||||
export default function Profile() {
|
||||
const { appointments, orders, shops } = useApp()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// ✅ Utilizador Supabase
|
||||
const [authEmail, setAuthEmail] = useState<string>('')
|
||||
@@ -50,6 +52,7 @@ export default function Profile() {
|
||||
setAuthEmail(data.user.email ?? '')
|
||||
setAuthId(data.user.id)
|
||||
}
|
||||
|
||||
setLoadingAuth(false)
|
||||
})()
|
||||
|
||||
@@ -77,7 +80,7 @@ export default function Profile() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// ✅ Recarrega eventos sempre que muda a rota (ex.: voltas de /eventos/novo para /perfil)
|
||||
// ✅ Recarrega eventos sempre que voltares para /perfil
|
||||
useEffect(() => {
|
||||
if (location.pathname === '/perfil') {
|
||||
loadEvents()
|
||||
@@ -107,6 +110,13 @@ export default function Profile() {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-slate-600">Sessão não encontrada. Faz login novamente.</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/login', { replace: true })}
|
||||
className="mt-4 text-sm font-semibold text-amber-700 hover:text-amber-800"
|
||||
>
|
||||
Ir para login
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// src/routes.tsx
|
||||
import { createBrowserRouter } from 'react-router-dom'
|
||||
import { Shell } from './components/layout/Shell'
|
||||
import { RequireAuth } from './components/auth/RequireAuth'
|
||||
@@ -23,7 +24,7 @@ export const router = createBrowserRouter([
|
||||
{ path: '/explorar', element: <Explore /> },
|
||||
{ path: '/barbearia/:id', element: <ShopDetails /> },
|
||||
|
||||
// ✅ PROTEGIDAS (precisam de login)
|
||||
// ✅ PROTEGIDAS
|
||||
{
|
||||
path: '/agendar/:id',
|
||||
element: (
|
||||
|
||||
Reference in New Issue
Block a user