feat: Implement initial application structure, core pages, UI components, and Supabase backend integration.

This commit is contained in:
Rodrigo Lopes dos Santos
2026-03-16 01:30:28 +00:00
parent 8ece90a37e
commit 0270a6cbdf
49 changed files with 2122 additions and 797 deletions

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["denoland.vscode-deno"]
}

24
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
"deno.enablePaths": [
"supabase/functions"
],
"deno.lint": true,
"deno.unstable": [
"bare-node-builtins",
"byonm",
"sloppy-imports",
"unsafe-proto",
"webgpu",
"broadcast-channel",
"worker-options",
"cron",
"kv",
"ffi",
"fs",
"http",
"net"
],
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}

View File

@@ -6,7 +6,6 @@
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
@@ -32,8 +31,9 @@
},
"extra": {
"eas": {
"projectId": "your-project-id-here"
"projectId": "b018a5db-c940-4364-81ee-596ced75cae3"
}
}
},
"owner": "rodrigo_santos08"
}
}

6
babel.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

1602
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,14 @@
"web": "expo start --web"
},
"dependencies": {
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"expo": "~54.0.27",
"@supabase/supabase-js": "^2.99.1",
"expo": "~54.0.33",
"expo-device": "~8.0.10",
"expo-notifications": "~0.32.16",
"expo-status-bar": "~3.0.9",
"nanoid": "^5.0.7",
"react": "19.1.0",
@@ -26,6 +29,7 @@
"devDependencies": {
"@types/react": "~19.1.0",
"@types/react-native": "^0.73.0",
"babel-preset-expo": "~54.0.10",
"typescript": "~5.9.2"
},
"private": true

View File

@@ -36,7 +36,7 @@ export const Button = ({ children, onPress, variant = 'solid', size = 'md', disa
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator color={variant === 'solid' ? '#fff' : '#6366f1'} />
<ActivityIndicator size={20} color={variant === 'solid' ? '#fff' : '#6366f1'} />
) : (
<Text style={textStyles}>{children}</Text>
)}

View File

@@ -8,6 +8,9 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from '
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
import { supabase } from '../lib/supabase';
import { nanoid } from 'nanoid';
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
type State = {
user?: User;
@@ -18,10 +21,12 @@ type State = {
};
type AppContextValue = State & {
login: (email: string, password: string) => boolean;
login: (email: string, password: string) => User | undefined;
logout: () => void;
register: (payload: any) => boolean;
register: (payload: any) => User | undefined;
addToCart: (item: CartItem) => void;
removeFromCart: (refId: string) => void;
placeOrder: (customerId: string, shopId: string) => Order | null;
clearCart: () => void;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise<void>;
@@ -54,7 +59,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
if (data.user) {
const { data: prof } = await supabase
.from('profiles')
.select('shop_id, role, name')
.select('shop_id, role, name, fcm_token')
.eq('id', data.user.id)
.single();
@@ -64,13 +69,22 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
email: data.user.email || '',
role: (prof?.role as any) || 'cliente',
shopId: prof?.shop_id || undefined,
fcmToken: prof?.fcm_token || undefined,
} as User);
// Regista token FCM se ainda não existir ou tiver mudado
registerForPushNotificationsAsync().then(token => {
if (token && token !== prof?.fcm_token) {
supabase.from('profiles').update({ fcm_token: token }).eq('id', data.user.id).then();
}
});
}
};
loadUser();
}, []);
const refreshShops = async () => {
console.log('AppContext: refreshShops iniciado');
try {
const { data: shopsData } = await supabase.from('shops').select('*');
const { data: servicesData } = await supabase.from('services').select('*');
@@ -134,15 +148,33 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
};
useEffect(() => {
console.log('AppContext: Iniciando carregamento...');
const init = async () => {
await refreshShops();
setLoading(false);
try {
await refreshShops();
console.log('AppContext: Lojas carregadas com sucesso.');
} catch (e) {
console.error('AppContext: Erro no init:', e);
} finally {
setLoading(false);
console.log('AppContext: setLoading(false)');
}
};
init();
}, []);
const login = (email: string, password: string) => {
return true;
// Provisório para demo
const u: User = {
id: email === 'barber@demo.com' ? 'demo-barber' : 'demo-cliente',
name: email === 'barber@demo.com' ? 'Barbeiro Chefe' : 'Utilizador Demo',
email,
password, // Adicionado para satisfazer o tipo
role: email === 'barber@demo.com' ? 'barbearia' : 'cliente',
shopId: email === 'barber@demo.com' ? shops[0]?.id : undefined
};
setUser(u);
return u;
};
const logout = async () => {
@@ -154,7 +186,39 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const id = nanoid();
const newUser: User = { ...payload, id };
setUser(newUser);
return true;
return newUser;
};
const removeFromCart = (refId: string) => {
setCart((prev) => prev.filter((i) => i.refId !== refId));
};
const placeOrder = (customerId: string, shopId: string) => {
const shopItems = cart.filter((i) => i.shopId === shopId);
if (!shopItems.length) return null;
const shop = shops.find((s) => s.id === shopId);
const total = shopItems.reduce((sum, i) => {
const price =
i.type === 'service'
? shop?.services.find((s) => s.id === i.refId)?.price ?? 0
: shop?.products.find((p) => p.id === i.refId)?.price ?? 0;
return sum + price * i.qty;
}, 0);
const newOrder: Order = {
id: nanoid(),
shopId,
customerId,
items: shopItems,
total,
status: 'pendente',
createdAt: new Date().toISOString(),
};
setOrders((prev) => [...prev, newOrder]);
setCart((prev) => prev.filter((i) => i.shopId !== shopId));
return newOrder;
};
const addToCart = (item: CartItem) => {
@@ -221,7 +285,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
customer_id: input.customerId,
date: input.date,
status: 'pendente',
total
total,
reminder_minutes: input.reminderMinutes
}]).select().single();
await refreshShops();
return data as any as Appointment;
@@ -248,6 +313,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
logout,
register,
addToCart,
removeFromCart,
placeOrder,
clearCart,
createAppointment,
updateAppointmentStatus,
@@ -275,4 +342,36 @@ export const useApp = () => {
const ctx = useContext(AppContext);
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
return ctx;
};
};
async function registerForPushNotificationsAsync() {
let token;
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('Falha ao obter token push!');
return;
}
token = (await Notifications.getExpoPushTokenAsync({
projectId: 'b018a5db-c940-4364-81ee-596ced75cae3',
})).data;
} else {
console.log('Necessário dispositivo físico para notificações push');
}
return token;
}

View File

@@ -43,8 +43,8 @@ export default function AuthLogin() {
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* O componente Card encapsula de forma visual os inputs de login */}
<Card style={styles.card}>
<Text style={styles.title}>Bem-vindo de volta</Text>
<Text style={styles.subtitle}>Entre na sua conta para continuar</Text>
<Text style={styles.title}>Bem-vindo</Text>
<Text style={styles.subtitle}>Aceda à sua conta</Text>
{/* Bloco temporário para dados demo */}
<View style={styles.demoBox}>
@@ -91,7 +91,7 @@ export default function AuthLogin() {
style={styles.footerLink}
onPress={() => navigation.navigate('Register' as never)}
>
Criar conta
Criar Conta
</Text>
</View>
</Card>
@@ -126,9 +126,9 @@ const styles = StyleSheet.create({
textAlign: 'center',
},
demoBox: {
backgroundColor: '#fef3c7',
backgroundColor: '#e0e7ff',
borderWidth: 1,
borderColor: '#fbbf24',
borderColor: '#6366f1',
borderRadius: 8,
padding: 12,
marginBottom: 20,
@@ -136,12 +136,12 @@ const styles = StyleSheet.create({
demoTitle: {
fontSize: 12,
fontWeight: '600',
color: '#92400e',
color: '#4338ca',
marginBottom: 4,
},
demoText: {
fontSize: 11,
color: '#92400e',
color: '#4338ca',
},
submitButton: {
width: '100%',

View File

@@ -31,6 +31,14 @@ export default function Booking() {
const [barberId, setBarber] = useState('');
const [date, setDate] = useState('');
const [slot, setSlot] = useState('');
const [reminderMinutes, setReminderMinutes] = useState(1440); // 24h por padrão
const reminderOptions = [
{ label: '10 min', value: 10 },
{ label: '30 min', value: 30 },
{ label: '1 hora', value: 60 },
{ label: '24 horas', value: 1440 },
];
// Fallback visual caso ocorra um erro a obter o ID requisitado
if (!shop) {
@@ -99,7 +107,7 @@ export default function Booking() {
* Desencadeia o pedido assíncrono à API para materializar o DTO na tabela 'appointments'.
* Verifica se o token de Auth está válido (`!user`).
*/
const submit = () => {
const submit = async () => {
if (!user) {
// Impede requisições anónimas, delegando a sessão para o sistema de autenticação
Alert.alert('Login necessário', 'Faça login para agendar');
@@ -109,12 +117,13 @@ export default function Booking() {
if (!canSubmit) return;
// Cria o agendamento fornecendo as 'Foreign Keys' vitais (shopId, serviceId, etc...)
const appt = createAppointment({
const appt = await createAppointment({
shopId: shop.id,
serviceId,
barberId,
customerId: user.id, // Auth User UID
date: `${date} ${slot}`
date: `${date} ${slot}`,
reminderMinutes
});
if (appt) {
@@ -131,7 +140,7 @@ export default function Booking() {
<Text style={styles.title}>Agendar em {shop.name}</Text>
<Card style={styles.card}>
<Text style={styles.sectionTitle}>1. Escolha o serviço</Text>
<Text style={styles.sectionTitle}>1. Seleção de Serviço</Text>
{/* Renderiza um botão (bloco flexível) por cada serviço (ex: Corte, Barba) vindos do mapeamento DB */}
<View style={styles.grid}>
{shop.services.map((s) => (
@@ -146,7 +155,7 @@ export default function Booking() {
))}
</View>
<Text style={styles.sectionTitle}>2. Escolha o barbeiro</Text>
<Text style={styles.sectionTitle}>2. Barbeiro</Text>
{/* Renderiza os profissionais, normalmente provindo dum JOIN na base de dados (tabela barbeiros + barbearia) */}
<View style={styles.barberContainer}>
{shop.barbers.map((b) => (
@@ -162,7 +171,7 @@ export default function Booking() {
))}
</View>
<Text style={styles.sectionTitle}>3. Escolha a data</Text>
<Text style={styles.sectionTitle}>3. Data de Preferência</Text>
{/* Componente simples de input que deverá mapear para a inserção final do timestamp Postgres */}
<Input
value={date}
@@ -170,8 +179,7 @@ export default function Booking() {
placeholder="YYYY-MM-DD"
/>
<Text style={styles.sectionTitle}>4. Escolha o horário</Text>
{/* Lista mapeada e computada: Apenas slots `available` (que passaram pela query preventiva de duplicação) */}
<Text style={styles.sectionTitle}>4. Horário</Text>
<View style={styles.slotsContainer}>
{availableSlots.length > 0 ? (
availableSlots.map((h) => (
@@ -184,14 +192,29 @@ export default function Booking() {
</TouchableOpacity>
))
) : (
<Text style={styles.noSlots}>Escolha primeiro o barbeiro e a data</Text>
<Text style={styles.noSlots}>Selecione primeiro o mestre e a data</Text>
)}
</View>
<Text style={styles.sectionTitle}>5. Receber Lembrete</Text>
<View style={styles.reminderContainer}>
{reminderOptions.map((opt) => (
<TouchableOpacity
key={opt.value}
style={[styles.reminderButton, reminderMinutes === opt.value && styles.reminderButtonActive]}
onPress={() => setReminderMinutes(opt.value)}
>
<Text style={[styles.reminderText, reminderMinutes === opt.value && styles.reminderTextActive]}>
{opt.label} antes
</Text>
</TouchableOpacity>
))}
</View>
{/* Quadro resumo: Apenas mostrado se o estado interno conter todas as variáveis relacionais */}
{canSubmit && selectedService && (
<View style={styles.summary}>
<Text style={styles.summaryTitle}>Resumo</Text>
<Text style={styles.summaryTitle}>Resumo do Agendamento</Text>
<Text style={styles.summaryText}>Serviço: {selectedService.name}</Text>
<Text style={styles.summaryText}>Barbeiro: {selectedBarber?.name}</Text>
<Text style={styles.summaryText}>Data: {date} às {slot}</Text>
@@ -201,7 +224,7 @@ export default function Booking() {
{/* Botão para concretizar o INSERT na base de dados com as validações pré-acionadas */}
<Button onPress={submit} disabled={!canSubmit} style={styles.submitButton} size="lg">
{user ? 'Confirmar agendamento' : 'Entrar para agendar'}
{user ? 'Confirmar Reserva' : 'Entrar para Reservar'}
</Button>
</Card>
</ScrollView>
@@ -319,6 +342,31 @@ const styles = StyleSheet.create({
color: '#94a3b8',
fontStyle: 'italic',
},
reminderContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 16,
},
reminderButton: {
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
borderWidth: 1,
borderColor: '#e2e8f0',
},
reminderButtonActive: {
borderColor: '#6366f1',
backgroundColor: '#e0e7ff',
},
reminderText: {
fontSize: 12,
color: '#64748b',
},
reminderTextActive: {
color: '#6366f1',
fontWeight: 'bold',
},
summary: {
backgroundColor: '#f1f5f9',
padding: 16,

View File

@@ -22,7 +22,7 @@ export default function Cart() {
return (
<View style={styles.container}>
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Carrinho vazio</Text>
<Text style={styles.emptyText}>Sua Seleção está Deserta</Text>
</Card>
</View>
);
@@ -49,7 +49,7 @@ export default function Cart() {
const handleCheckout = (shopId: string) => {
// Verificamos de forma segura pelo objeto user se o authState (sessão Supabase) existe
if (!user) {
Alert.alert('Login necessário', 'Faça login para finalizar o pedido');
Alert.alert('Sessão Necessária', 'Inicie sessão para confirmar o seu pedido');
navigation.navigate('Login' as never);
return;
}
@@ -63,7 +63,7 @@ export default function Cart() {
return (
// A página permite visibilidade escalonada num conteúdo flexível (ScrollView)
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Carrinho</Text>
<Text style={styles.title}>Minha Seleção</Text>
{/* Renderiza dinamicamente 1 Card de Checkout por Loja agrupada no objeto `grouped` */}
{Object.entries(grouped).map(([shopId, items]) => {
@@ -122,14 +122,14 @@ export default function Cart() {
{/* Renderização condicional no React para encaminhar fluxo para login se anónimo */}
{user ? (
<Button onPress={() => handleCheckout(shopId)} style={styles.checkoutButton}>
Finalizar pedido
Finalizar Aquisição
</Button>
) : (
<Button
onPress={() => navigation.navigate('Login' as never)}
style={styles.checkoutButton}
>
Entrar para finalizar
Entrar para Adquirir
</Button>
)}
</Card>

View File

@@ -136,12 +136,12 @@ export default function Dashboard() {
};
const tabs = [
{ id: 'overview', label: 'Visão Geral' },
{ id: 'appointments', label: 'Agendamentos' },
{ id: 'orders', label: 'Pedidos' },
{ id: 'overview', label: 'Estatísticas' },
{ id: 'appointments', label: 'Reservas' },
{ id: 'orders', label: 'Pedidos Boutique' },
{ id: 'services', label: 'Serviços' },
{ id: 'products', label: 'Produtos' },
{ id: 'barbers', label: 'Barbeiros' },
{ id: 'barbers', label: 'Equipa' },
];
return (
@@ -172,7 +172,7 @@ export default function Dashboard() {
<View>
<View style={styles.statsGrid}>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Faturamento</Text>
<Text style={styles.statLabel}>Receita Total</Text>
<Text style={styles.statValue}>{currency(totalRevenue)}</Text>
</Card>
<Card style={styles.statCard}>

View File

@@ -23,7 +23,7 @@ export default function Explore() {
return (
// Componente raiz do ecã de exploração
<View style={styles.container}>
<Text style={styles.title}>Explorar barbearias</Text>
<Text style={styles.title}>Barbearias</Text>
{/* FlatList é o componente nativo otimizado para renderizar grandes arrays de dados provenientes da BD */}
<FlatList
@@ -54,7 +54,7 @@ export default function Explore() {
variant="outline"
style={styles.button}
>
Ver detalhes
Ver Barbearia
</Button>
{/* Redirecionamento direto com foreign key injetada para a view de Agendamentos */}
@@ -62,7 +62,7 @@ export default function Explore() {
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
style={styles.button}
>
Agendar
Reservar
</Button>
</View>
</Card>

View File

@@ -19,51 +19,48 @@ export default function Landing() {
<View style={styles.hero}>
<Text style={styles.heroTitle}>Smart Agenda</Text>
<Text style={styles.heroSubtitle}>
Agendamentos, produtos e gestão em um único lugar.
Agendamento e Gestão de Barbearias.
</Text>
<Text style={styles.heroDesc}>
Experiência mobile-first para clientes e painel completo para barbearias.
A sua solução completa para o dia-a-dia da barbearia.
</Text>
<View style={styles.buttons}>
{/* Este fluxo permite utilizadores visitarem dados públicos da plataforma via Explore */}
<Button
onPress={() => navigation.navigate('Explore' as never)}
style={styles.button}
size="lg"
>
Explorar barbearias
Ver Barbearias
</Button>
{/* Botão focado no registo de novos utilizadores */}
<Button
onPress={() => navigation.navigate('Register' as never)}
variant="outline"
style={styles.button}
size="lg"
>
Criar conta
Criar Conta
</Button>
</View>
</View>
<View style={styles.features}>
{/* Componentes estáticos descritivos sobre as features que mapeiam para funcionalidades da BD */}
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Agendamentos</Text>
<Text style={styles.featureTitle}>Reservas Rápidas</Text>
<Text style={styles.featureDesc}>
Escolha serviço, barbeiro, data e horário com validação de slots.
Selecione o seu barbeiro e o horário ideal em poucos segundos.
</Text>
</Card>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Carrinho</Text>
<Text style={styles.featureTitle}>Produtos</Text>
<Text style={styles.featureDesc}>
Produtos e serviços agrupados por barbearia, pagamento rápido.
Produtos de cuidado masculino selecionados para si.
</Text>
</Card>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Painel</Text>
<Text style={styles.featureTitle}>Gestão de Barbearia</Text>
<Text style={styles.featureDesc}>
Faturamento, agendamentos, pedidos, barbearia no controle.
Controlo total sobre o faturamento e operação da sua barbearia.
</Text>
</Card>
</View>

View File

@@ -13,8 +13,8 @@ import { Button } from '../components/ui/Button';
import { currency } from '../lib/format';
// Mapeamento visual estático das strings de estado do Postgres/State para cores da UI
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
pendente: 'amber',
const statusColor: Record<string, 'indigo' | 'green' | 'slate' | 'red'> = {
pendente: 'indigo',
confirmado: 'green',
concluido: 'green',
cancelado: 'red',
@@ -47,7 +47,7 @@ export default function Profile() {
<Text style={styles.profileEmail}>{user.email}</Text>
{/* Distanciamento visual e lógica dos tipos de perfil 'role' presentes na BD */}
<Badge color="amber" style={styles.roleBadge}>
<Badge color="indigo" style={styles.roleBadge}>
{user.role === 'cliente' ? 'Cliente' : 'Barbearia'}
</Badge>
@@ -57,7 +57,7 @@ export default function Profile() {
</Button>
</Card>
<Text style={styles.sectionTitle}>Agendamentos</Text>
<Text style={styles.sectionTitle}>As Minhas Reservas</Text>
{/* Renderiza a lista se existirem marcações no percurso deste utilizador */}
{myAppointments.length > 0 ? (
myAppointments.map((a) => {
@@ -83,7 +83,7 @@ export default function Profile() {
</Card>
)}
<Text style={styles.sectionTitle}>Pedidos</Text>
<Text style={styles.sectionTitle}>As Minhas Compras</Text>
{/* Renderiza o histórico de compras de retalho/produtos usando idêntica lógica */}
{myOrders.length > 0 ? (
myOrders.map((o) => {

View File

@@ -51,7 +51,7 @@ export default function ShopDetails() {
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
style={styles.bookButton}
>
Agendar
Reservar Experiência
</Button>
</View>
@@ -61,13 +61,13 @@ export default function ShopDetails() {
style={[styles.tab, tab === 'servicos' && styles.tabActive]}
onPress={() => setTab('servicos')}
>
<Text style={[styles.tabText, tab === 'servicos' && styles.tabTextActive]}>Serviços</Text>
<Text style={[styles.tabText, tab === 'servicos' && styles.tabTextActive]}>Menu de Serviços</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, tab === 'produtos' && styles.tabActive]}
onPress={() => setTab('produtos')}
>
<Text style={[styles.tabText, tab === 'produtos' && styles.tabTextActive]}>Produtos</Text>
<Text style={[styles.tabText, tab === 'produtos' && styles.tabTextActive]}>Boutique</Text>
</TouchableOpacity>
</View>
@@ -89,7 +89,7 @@ export default function ShopDetails() {
size="sm"
style={styles.addButton}
>
Adicionar ao carrinho
Adicionar à Seleção
</Button>
</Card>
))}
@@ -106,7 +106,7 @@ export default function ShopDetails() {
<Text style={styles.itemDesc}>Stock: {product.stock} unidades</Text>
{/* Alerta de urgência de reposição assente numa regra simples de negócios matemática */}
{product.stock <= 3 && <Badge color="amber" style={styles.stockBadge}>Stock baixo</Badge>}
{product.stock <= 3 && <Badge color="indigo" style={styles.stockBadge}>Últimas unidades</Badge>}
{/* Botão em React é afetado logicamente face à impossibilidade material de encomenda */}
<Button
@@ -115,7 +115,7 @@ export default function ShopDetails() {
style={styles.addButton}
disabled={product.stock <= 0}
>
{product.stock > 0 ? 'Adicionar ao carrinho' : 'Sem stock'}
{product.stock > 0 ? 'Adicionar à Seleção' : 'Indisponível'}
</Button>
</Card>
))}
@@ -165,7 +165,7 @@ const styles = StyleSheet.create({
},
tabActive: {
borderColor: '#6366f1',
backgroundColor: '#fef3c7',
backgroundColor: '#e0e7ff',
},
tabText: {
fontSize: 14,

View File

@@ -4,9 +4,9 @@ export type Product = { id: string; name: string; price: number; stock: number }
export type BarberShop = { id: string; name: string; address: string; rating: number; services: Service[]; products: Product[]; barbers: Barber[] };
export type AppointmentStatus = 'pendente' | 'confirmado' | 'concluido' | 'cancelado';
export type OrderStatus = 'pendente' | 'confirmado' | 'concluido' | 'cancelado';
export type Appointment = { id: string; shopId: string; serviceId: string; barberId: string; customerId: string; date: string; status: AppointmentStatus; total: number };
export type Appointment = { id: string; shopId: string; serviceId: string; barberId: string; customerId: string; date: string; status: AppointmentStatus; total: number; reminderMinutes?: number };
export type CartItem = { shopId: string; type: 'service' | 'product'; refId: string; qty: number };
export type Order = { id: string; shopId: string; customerId: string; items: CartItem[]; total: number; status: OrderStatus; createdAt: string };
export type User = { id: string; name: string; email: string; password: string; role: 'cliente' | 'barbearia'; shopId?: string };
export type User = { id: string; name: string; email: string; password: string; role: 'cliente' | 'barbearia'; shopId?: string; fcmToken?: string };

8
supabase/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# Supabase
.branches
.temp
# dotenvx
.env.keys
.env.local
.env.*.local

399
supabase/config.toml Normal file
View File

@@ -0,0 +1,399 @@
# For detailed configuration reference documentation, visit:
# https://supabase.com/docs/guides/local-development/cli/config
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "smartagenda_pap"
[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000
[api.tls]
# Enable HTTPS endpoints locally using a self-signed certificate.
enabled = false
# Paths to self-signed certificate pair.
# cert_path = "../certs/my-cert.pem"
# key_path = "../certs/my-key.pem"
[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# Maximum amount of time to wait for health check when starting the local database.
health_timeout = "2m"
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 17
[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100
# [db.vault]
# secret_key = "env(SECRET_VALUE)"
[db.migrations]
# If disabled, migrations will be skipped during a db push or reset.
enabled = true
# Specifies an ordered list of schema files that describe your database.
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
schema_paths = []
[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]
[db.network_restrictions]
# Enable management of network restrictions.
enabled = false
# List of IPv4 CIDR blocks allowed to connect to the database.
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
allowed_cidrs = ["0.0.0.0/0"]
# List of IPv6 CIDR blocks allowed to connect to the database.
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
allowed_cidrs_v6 = ["::/0"]
# Uncomment to reject non-secure connections to the database.
# [db.ssl_enforcement]
# enabled = true
[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096
[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326
# admin_email = "admin@email.com"
# sender_name = "Admin"
[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"
# Uncomment to configure local storage buckets
# [storage.buckets.images]
# public = false
# file_size_limit = "50MiB"
# allowed_mime_types = ["image/png", "image/jpeg"]
# objects_path = "./images"
# Allow connections via S3 compatible clients
[storage.s3_protocol]
enabled = true
# Image transformation API is available to Supabase Pro plan.
# [storage.image_transformation]
# enabled = true
# Store analytical data in S3 for running ETL jobs over Iceberg Catalog
# This feature is only available on the hosted platform.
[storage.analytics]
enabled = false
max_namespaces = 5
max_tables = 10
max_catalogs = 2
# Analytics Buckets is available to Supabase Pro plan.
# [storage.analytics.buckets.my-warehouse]
# Store vector embeddings in S3 for large and durable datasets
# This feature is only available on the hosted platform.
[storage.vector]
enabled = false
max_buckets = 10
max_indexes = 5
# Vector Buckets is available to Supabase Pro plan.
# [storage.vector.buckets.documents-openai]
[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:<port>/auth/v1).
# jwt_issuer = ""
# Path to JWT signing key. DO NOT commit your signing keys file to git.
# signing_keys_path = "./signing_keys.json"
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
minimum_password_length = 6
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
password_requirements = ""
[auth.rate_limit]
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
email_sent = 2
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
sms_sent = 30
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
anonymous_users = 30
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
token_refresh = 150
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
sign_in_sign_ups = 30
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
token_verifications = 30
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
web3 = 30
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
# [auth.captcha]
# enabled = true
# provider = "hcaptcha"
# secret = ""
[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Number of characters used in the email OTP.
otp_length = 6
# Number of seconds before the email OTP expires (defaults to 1 hour).
otp_expiry = 3600
# Use a production-ready SMTP server
# [auth.email.smtp]
# enabled = true
# host = "smtp.sendgrid.net"
# port = 587
# user = "apikey"
# pass = "env(SENDGRID_API_KEY)"
# admin_email = "admin@email.com"
# sender_name = "Admin"
# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"
# Uncomment to customize notification email template
# [auth.email.notification.password_changed]
# enabled = true
# subject = "Your password has been changed"
# content_path = "./templates/password_changed_notification.html"
[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = false
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }}"
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"
# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
# 4152127777 = "123456"
# Configure logged in session timeouts.
# [auth.sessions]
# Force log out after the specified duration.
# timebox = "24h"
# Force log out if the user has been inactive longer than the specified duration.
# inactivity_timeout = "8h"
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
# [auth.hook.before_user_created]
# enabled = true
# uri = "pg-functions://postgres/auth/before-user-created-hook"
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
# Multi-factor-authentication is available to Supabase Pro plan.
[auth.mfa]
# Control how many MFA factors can be enrolled at once per user.
max_enrolled_factors = 10
# Control MFA via App Authenticator (TOTP)
[auth.mfa.totp]
enroll_enabled = false
verify_enabled = false
# Configure MFA via Phone Messaging
[auth.mfa.phone]
enroll_enabled = false
verify_enabled = false
otp_length = 6
template = "Your code is {{ .Code }}"
max_frequency = "5s"
# Configure MFA via WebAuthn
# [auth.mfa.web_authn]
# enroll_enabled = true
# verify_enabled = true
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address.
email_optional = false
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
[auth.web3.solana]
enabled = false
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false
# project_id = "my-firebase-project"
# Use Auth0 as a third-party provider alongside Supabase Auth.
[auth.third_party.auth0]
enabled = false
# tenant = "my-auth0-tenant"
# tenant_region = "us"
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
[auth.third_party.aws_cognito]
enabled = false
# user_pool_id = "my-user-pool-id"
# user_pool_region = "us-east-1"
# Use Clerk as a third-party provider alongside Supabase Auth.
[auth.third_party.clerk]
enabled = false
# Obtain from https://clerk.com/setup/supabase
# domain = "example.clerk.accounts.dev"
# OAuth server configuration
[auth.oauth_server]
# Enable OAuth server functionality
enabled = false
# Path for OAuth consent flow UI
authorization_url_path = "/oauth/consent"
# Allow dynamic client registration
allow_dynamic_registration = false
[edge_runtime]
enabled = true
# Supported request policies: `oneshot`, `per_worker`.
# `per_worker` (default) — enables hot reload during local development.
# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks).
policy = "per_worker"
# Port to attach the Chrome inspector for debugging edge functions.
inspector_port = 8083
# The Deno major version to use.
deno_version = 2
# [edge_runtime.secrets]
# secret_key = "env(SECRET_VALUE)"
[analytics]
enabled = true
port = 54327
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"
# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"
[functions.send-reminder]
enabled = true
verify_jwt = true
import_map = "./functions/send-reminder/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
entrypoint = "./functions/send-reminder/index.ts"
# Specifies static files to be bundled with the function. Supports glob patterns.
# For example, if you want to serve static HTML pages in your function:
# static_files = [ "./functions/send-reminder/*.html" ]

View File

@@ -0,0 +1,3 @@
# Configuration for private npm package dependencies
# For more information on using private registries with Edge Functions, see:
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries

View File

@@ -0,0 +1,5 @@
{
"imports": {
"@supabase/functions-js": "jsr:@supabase/functions-js@^2"
}
}

View File

@@ -0,0 +1,71 @@
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
// URL da API de Push do Expo (não precisa de chaves de servidor para o básico)
const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send';
Deno.serve(async (req) => {
// Criar o cliente Supabase com as variáveis de ambiente do sistema
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
try {
// 1. Procurar agendamentos pendentes com tokens válidos
// Nota: O campo 'fcm_token' na BD irá guardar o 'ExponentPushToken'
const { data: appts, error } = await supabase
.from('appointments')
.select('*, profiles(fcm_token)')
.eq('status', 'pendente')
.not('profiles.fcm_token', 'is', null);
if (error) throw error;
const notifications = [];
for (const appt of appts) {
const expoToken = appt.profiles?.fcm_token;
// Verifica se é um token válido do Expo
if (expoToken && expoToken.startsWith('ExponentPushToken')) {
notifications.push({
to: expoToken,
sound: 'default',
title: 'Lembrete Smart Agenda',
body: `Olá! Não se esqueça do seu agendamento em breve.`,
data: { appointmentId: appt.id },
});
}
}
console.log(`Enviando ${notifications.length} notificações via Expo...`);
if (notifications.length > 0) {
// 2. Enviar notificações em lote (Batch) via Expo
const response = await fetch(EXPO_PUSH_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Encoding': 'gzip, deflate',
},
body: JSON.stringify(notifications),
});
const result = await response.json();
console.log('Resultado do Expo:', result);
}
return new Response(JSON.stringify({ success: true, sent: notifications.length }), {
headers: { "Content-Type": "application/json" },
status: 200,
});
} catch (err) {
console.error('Erro na função send-reminder:', err);
return new Response(JSON.stringify({ error: err.message }), {
headers: { "Content-Type": "application/json" },
status: 500,
});
}
})

View File

@@ -1,6 +1,10 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
"strict": true,
"jsx": "react-native",
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node"
}
}

9
web/package-lock.json generated
View File

@@ -75,7 +75,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1362,7 +1361,6 @@
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -1530,7 +1528,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2129,7 +2126,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -2397,7 +2393,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -2603,7 +2598,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -2616,7 +2610,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -3030,7 +3023,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3159,7 +3151,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View File

@@ -7,20 +7,20 @@ export const BarberList = ({ barbers }: { barbers: Barber[] }) => (
{barbers.map((b) => (
<Card key={b.id} hover className="p-5 flex flex-col items-center text-center gap-4 group">
<div className="relative">
<div className="w-full aspect-square rounded-2xl overflow-hidden border-4 border-slate-50 bg-slate-100 flex items-center justify-center shadow-sm group-hover:border-amber-100 transition-colors">
<div className="w-full aspect-square rounded-2xl overflow-hidden border-4 border-slate-50 bg-slate-100 flex items-center justify-center shadow-sm group-hover:border-indigo-100 transition-colors">
{b.imageUrl ? (
<img src={b.imageUrl} alt={b.name} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
) : (
<User size={40} className="text-slate-300" />
)}
</div>
<div className="absolute -bottom-1 -right-1 w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center text-white border-2 border-white shadow-sm">
<div className="absolute -bottom-1 -right-1 w-8 h-8 bg-indigo-600 rounded-full flex items-center justify-center text-white border-2 border-white shadow-sm">
<Scissors size={14} />
</div>
</div>
<div>
<h3 className="font-bold text-slate-900 text-lg group-hover:text-amber-700 transition-colors">{b.name}</h3>
<h3 className="font-bold text-slate-900 text-lg group-hover:text-indigo-700 transition-colors">{b.name}</h3>
<div className="mt-2 flex flex-wrap justify-center gap-1">
{b.specialties.map((spec, i) => (
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-bold uppercase tracking-wider">

View File

@@ -219,7 +219,7 @@ export const CalendarWeekView = ({
const service = shop.services.find((s) => s.id === apt.serviceId);
const barber = shop.barbers.find((b) => b.id === apt.barberId);
const statusColors = {
pendente: 'bg-amber-100 border-amber-300 text-amber-800',
pendente: 'bg-indigo-100 border-indigo-300 text-indigo-800',
confirmado: 'bg-green-100 border-green-300 text-green-800',
concluido: 'bg-blue-100 border-blue-300 text-blue-800',
cancelado: 'bg-red-100 border-red-300 text-red-800',
@@ -252,7 +252,7 @@ export const CalendarWeekView = ({
{/* Legenda */}
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded border-l-2 bg-amber-100 border-amber-300"></div>
<div className="w-4 h-4 rounded border-l-2 bg-indigo-100 border-indigo-300"></div>
<span>Pendente</span>
</div>
<div className="flex items-center gap-2">

View File

@@ -16,10 +16,10 @@ export const CartPanel = () => {
</div>
<div className="space-y-2">
<h3 className="text-2xl font-black text-slate-900 uppercase italic tracking-tighter">O seu carrinho está vazio</h3>
<p className="text-slate-500 font-medium italic">Explore os melhores produtos e serviços de luxo.</p>
<p className="text-slate-500 font-medium italic">Explore os melhores produtos e serviços disponíveis.</p>
</div>
<Button asChild className="h-14 px-8 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-2xl uppercase tracking-widest text-xs italic">
<Link to="/explorar">Começar a Explorar</Link>
<Button asChild className="h-14 px-8 bg-slate-900 hover:bg-slate-800 text-indigo-400 font-black rounded-2xl uppercase tracking-widest text-xs italic">
<Link to="/explorar">Ver Barbearias</Link>
</Button>
</div>
);
@@ -60,7 +60,7 @@ export const CartPanel = () => {
return (
<div key={shopId} className="space-y-4 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center gap-2 px-2">
<ShoppingBag size={14} className="text-amber-600" />
<ShoppingBag size={14} className="text-indigo-600" />
<h3 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-400">Origem: <span className="text-slate-900">{shop?.name ?? 'Barbearia'}</span></h3>
</div>
@@ -105,7 +105,7 @@ export const CartPanel = () => {
{user ? (
<Button
onClick={() => handleCheckout(shopId)}
className="h-14 px-8 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-2xl uppercase tracking-widest text-xs italic shadow-lg active:scale-95 transition-all"
className="h-14 px-8 bg-slate-900 hover:bg-slate-800 text-indigo-400 font-black rounded-2xl uppercase tracking-widest text-xs italic shadow-lg active:scale-95 transition-all"
>
Finalizar Compra
<ArrowRight size={14} className="ml-2" />

View File

@@ -41,7 +41,7 @@ export const DashboardCards = ({ periodFilter }: Props) => {
<div className="grid md:grid-cols-3 gap-3">
<Card className="p-4">
<div className="text-sm text-slate-600">Faturamento</div>
<div className="text-xl font-semibold text-amber-700">{currency(total)}</div>
<div className="text-xl font-semibold text-indigo-700">{currency(total)}</div>
</Card>
<Card className="p-4">
<div className="text-sm text-slate-600">Agendamentos pendentes</div>

View File

@@ -16,13 +16,13 @@ export const ProductList = ({
{products.map((p) => {
const lowStock = p.stock <= 3;
return (
<Card key={p.id} className="relative overflow-hidden border-none glass-card rounded-[2rem] premium-shadow group hover:shadow-2xl transition-all duration-300 flex flex-col">
<div className="aspect-square bg-slate-50 flex items-center justify-center p-8 group-hover:bg-amber-50 transition-colors">
<Package size={48} className="text-slate-200 group-hover:text-amber-200 transition-all group-hover:scale-110 duration-500" />
<Card key={p.id} className="relative overflow-hidden border-none glass-card rounded-[2rem] group hover:shadow-2xl transition-all duration-300 flex flex-col">
<div className="aspect-square bg-slate-50 flex items-center justify-center p-8 group-hover:bg-indigo-50 transition-colors">
<Package size={48} className="text-slate-200 group-hover:text-indigo-200 transition-all group-hover:scale-110 duration-500" />
{lowStock && (
<div className="absolute top-4 left-4">
<Badge color="amber" variant="solid" className="text-[9px] px-2 py-0.5 font-black uppercase tracking-widest shadow-lg animate-pulse">
<Badge color="indigo" variant="solid" className="text-[9px] px-2 py-0.5 font-black uppercase tracking-widest shadow-lg animate-pulse">
Últimas Unidades
</Badge>
</div>
@@ -31,7 +31,7 @@ export const ProductList = ({
<div className="p-5 flex-1 flex flex-col gap-4">
<div className="space-y-1">
<h3 className="font-black text-slate-900 text-base tracking-tight leading-tight group-hover:text-amber-600 transition-colors uppercase italic truncate">{p.name}</h3>
<h3 className="font-black text-slate-900 text-base tracking-tight leading-tight group-hover:text-indigo-600 transition-colors uppercase italic truncate">{p.name}</h3>
<div className="text-xs font-bold text-slate-400 uppercase tracking-widest">
{p.stock} em stock
</div>
@@ -46,7 +46,7 @@ export const ProductList = ({
<Button
onClick={() => onAdd(p.id)}
disabled={p.stock <= 0}
className="w-full h-10 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-95 uppercase tracking-widest text-[10px]"
className="w-full h-10 bg-slate-900 hover:bg-slate-800 text-indigo-400 font-black rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-95 uppercase tracking-widest text-[10px]"
>
{p.stock > 0 ? 'Adicionar' : 'Esgotado'}
</Button>

View File

@@ -56,7 +56,7 @@ export function ReviewModal({ shopName, appointmentId, onSubmit, onClose }: Revi
) : (
<>
<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">
<div className="w-14 h-14 bg-gradient-to-br from-indigo-400 to-indigo-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>
@@ -76,14 +76,14 @@ export function ReviewModal({ shopName, appointmentId, onSubmit, onClose }: Revi
<Star
size={36}
className={`transition-colors ${star <= (hovered || rating)
? 'text-amber-400 fill-amber-400'
? 'text-indigo-400 fill-indigo-400'
: 'text-slate-300'
}`}
/>
</button>
))}
</div>
<p className="text-center text-sm font-medium text-amber-600 mb-5 h-5">
<p className="text-center text-sm font-medium text-indigo-600 mb-5 h-5">
{labels[hovered || rating]}
</p>
@@ -93,7 +93,7 @@ export function ReviewModal({ shopName, appointmentId, onSubmit, onClose }: Revi
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"
className="w-full border border-slate-200 rounded-xl p-3 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-transparent mb-4 placeholder:text-slate-400"
/>
<Button

View File

@@ -13,10 +13,10 @@ export const ServiceList = ({
}) => (
<div className="grid md:grid-cols-2 gap-6">
{services.map((s) => (
<Card key={s.id} className="p-6 border-none glass-card rounded-[2rem] premium-shadow group hover:shadow-2xl transition-all duration-300">
<Card key={s.id} className="p-6 border-none glass-card rounded-[2rem] group hover:shadow-2xl transition-all duration-300">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<h3 className="font-black text-slate-900 text-xl tracking-tight leading-tight group-hover:text-amber-600 transition-colors uppercase italic">{s.name}</h3>
<h3 className="font-black text-slate-900 text-xl tracking-tight leading-tight group-hover:text-indigo-600 transition-colors uppercase italic">{s.name}</h3>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5 px-2 py-1 bg-slate-100 rounded-lg text-xs font-bold text-slate-500 uppercase">
<Clock size={12} />
@@ -26,7 +26,7 @@ export const ServiceList = ({
<span className="text-xs font-black text-slate-400 uppercase tracking-widest italic">Corte Profissional</span>
</div>
</div>
<div className="text-2xl font-black text-slate-900 tracking-tighter bg-amber-50 px-3 py-1 rounded-xl border border-amber-100">
<div className="text-2xl font-black text-slate-900 tracking-tighter bg-indigo-50 px-3 py-1 rounded-xl border border-indigo-100">
{currency(s.price)}
</div>
</div>
@@ -36,7 +36,7 @@ export const ServiceList = ({
<p className="text-xs font-medium text-slate-400 max-w-[150px]">Lugar disponível hoje</p>
<Button
onClick={() => onSelect(s.id)}
className="flex-1 h-11 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-95 uppercase tracking-widest text-xs"
className="flex-1 h-11 bg-slate-900 hover:bg-slate-800 text-indigo-400 font-black rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-95 uppercase tracking-widest text-xs"
>
Reservar Agora
</Button>

View File

@@ -6,7 +6,7 @@ import { Button } from './ui/button';
export const ShopCard = ({ shop }: { shop: BarberShop }) => {
return (
<Card className="overflow-hidden border-none glass-card rounded-[2rem] premium-shadow group hover:shadow-2xl hover:shadow-slate-200/60 transition-all duration-500">
<Card className="overflow-hidden border-none glass-card rounded-[2.5rem] group hover:shadow-2xl hover:shadow-slate-200/60 transition-all duration-500">
<div className="relative h-44 overflow-hidden">
{shop.imageUrl ? (
<img
@@ -15,7 +15,7 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => {
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
) : (
<div className="w-full h-full bg-slate-900 flex items-center justify-center text-amber-500">
<div className="w-full h-full bg-slate-900 flex items-center justify-center text-indigo-400">
<Scissors size={40} />
</div>
)}
@@ -23,7 +23,7 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => {
{/* Rating Badge */}
<div className="absolute top-4 right-4 bg-slate-900/90 backdrop-blur-md border border-white/10 px-3 py-1 rounded-full flex items-center gap-1.5 shadow-xl">
<Star size={14} className="fill-amber-500 text-amber-500" />
<Star size={14} className="fill-indigo-500 text-indigo-500" />
<span className="text-white text-xs font-black tracking-wider">
{shop.rating ? shop.rating.toFixed(1) : '0.0'}
</span>
@@ -32,11 +32,11 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => {
<div className="p-6 space-y-4">
<div>
<h2 className="text-slate-900 text-xl font-black tracking-tight group-hover:text-amber-600 transition-colors truncate">
<h2 className="text-slate-900 text-xl font-black tracking-tight group-hover:text-indigo-600 transition-colors truncate">
{shop.name}
</h2>
<div className="flex items-center gap-1.5 text-slate-500 mt-1">
<MapPin size={14} className="text-amber-600" />
<MapPin size={14} className="text-indigo-600" />
<p className="text-sm font-medium line-clamp-1">
{shop.address || 'Endereço Indisponível'}
</p>
@@ -57,7 +57,7 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => {
</span>
</div>
<Button asChild className="rounded-xl bg-slate-900 hover:bg-slate-800 text-amber-500 font-bold px-5 h-10 shadow-lg shadow-slate-200 transition-all active:scale-95">
<Button asChild className="rounded-xl bg-slate-900 hover:bg-slate-800 text-indigo-400 font-bold px-5 h-10 shadow-lg shadow-slate-200 transition-all active:scale-95">
<Link to={`/barbearia/${shop.id}`}>Reservar</Link>
</Button>
</div>

View File

@@ -22,10 +22,10 @@ export const Header = () => {
className="text-2xl font-black tracking-tighter text-slate-900 group flex items-center gap-2"
onClick={() => setMobileMenuOpen(false)}
>
<div className="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center text-amber-500 shadow-lg shadow-slate-200">
<div className="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center text-indigo-400 shadow-lg shadow-slate-200">
<User size={18} fill="currentColor" />
</div>
<span className="group-hover:text-amber-600 transition-colors">Smart Agenda</span>
<span className="group-hover:text-indigo-600 transition-colors uppercase italic font-black">Smart Agenda</span>
</Link>
{/* Desktop Navigation */}
@@ -36,8 +36,8 @@ export const Header = () => {
to="/explorar"
className="text-sm font-bold text-slate-600 hover:text-slate-900 transition-all flex items-center gap-2"
>
<MapPin size={16} className="text-amber-600" />
<span>Explorar</span>
<MapPin size={16} className="text-indigo-600" />
<span>Descobrir Barbearias</span>
</Link>
<Link
@@ -46,7 +46,7 @@ export const Header = () => {
>
<ShoppingCart size={20} />
{cart.length > 0 && (
<span className="absolute -right-1 -top-1 rounded-full bg-slate-900 px-1.5 py-0.5 text-[10px] font-black text-amber-500 shadow-md min-w-[18px] text-center">
<span className="absolute -right-1 -top-1 rounded-full bg-slate-900 px-1.5 py-0.5 text-[10px] font-black text-indigo-400 shadow-md min-w-[18px] text-center">
{cart.length}
</span>
)}
@@ -63,7 +63,7 @@ export const Header = () => {
className="flex items-center gap-3 bg-slate-50 hover:bg-slate-100 border border-slate-200/60 pl-3 pr-4 py-1.5 rounded-full transition-all group"
type="button"
>
<div className="w-7 h-7 rounded-full bg-slate-900 flex items-center justify-center text-amber-500 shadow-sm">
<div className="w-7 h-7 rounded-full bg-slate-900 flex items-center justify-center text-indigo-400 shadow-sm">
<User size={14} fill="currentColor" />
</div>
<span className="text-sm font-bold text-slate-700 group-hover:text-slate-900 max-w-[120px] truncate">{user.name}</span>
@@ -87,8 +87,8 @@ export const Header = () => {
Login
</Link>
<Link
to="/registro"
className="inline-flex items-center justify-center rounded-xl bg-slate-900 px-5 py-2 text-sm font-bold text-amber-500 shadow-lg shadow-slate-200 hover:bg-slate-800 transition-all"
to="/registo"
className="inline-flex items-center justify-center rounded-xl bg-slate-900 px-5 py-2 text-sm font-bold text-indigo-400 shadow-lg shadow-slate-200 hover:bg-slate-800 transition-all font-black uppercase italic tracking-widest"
>
Criar Conta
</Link>
@@ -115,21 +115,21 @@ export const Header = () => {
<Link
to="/explorar"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-3 text-base font-bold text-slate-700 hover:text-amber-600 p-3 rounded-2xl bg-slate-50 transition-all"
className="flex items-center gap-3 text-base font-black text-slate-700 hover:text-indigo-600 p-3 rounded-2xl bg-slate-50 transition-all uppercase italic"
>
<MapPin size={18} className="text-amber-600" />
Barbearias
<MapPin size={18} className="text-indigo-600" />
Descobrir Barbearias
</Link>
<Link
to="/carrinho"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-3 text-base font-bold text-slate-700 hover:text-amber-600 p-3 rounded-2xl bg-slate-50 transition-all"
className="flex items-center gap-3 text-base font-black text-slate-700 hover:text-indigo-600 p-3 rounded-2xl bg-slate-50 transition-all uppercase italic"
>
<ShoppingCart size={18} className="text-amber-600" />
<ShoppingCart size={18} className="text-indigo-600" />
Meu Carrinho
{cart.length > 0 && (
<span className="ml-auto rounded-full bg-slate-900 px-2 py-0.5 text-[10px] font-black text-amber-500">
<span className="ml-auto rounded-full bg-slate-900 px-2 py-0.5 text-[10px] font-black text-indigo-400">
{cart.length}
</span>
)}
@@ -172,9 +172,9 @@ export const Header = () => {
Entrar
</Link>
<Link
to="/registro"
to="/registo"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center justify-center rounded-2xl bg-slate-900 py-3 text-sm font-bold text-amber-500 shadow-lg shadow-slate-200"
className="flex items-center justify-center rounded-2xl bg-slate-900 py-3 text-sm font-bold text-indigo-400 shadow-lg shadow-slate-200 font-black uppercase italic tracking-widest"
>
Criar Conta
</Link>

View File

@@ -1,25 +1,25 @@
import { cn } from '../../lib/cn';
type Props = { children: React.ReactNode; color?: 'amber' | 'slate' | 'green' | 'red' | 'blue'; className?: string; variant?: 'solid' | 'soft' };
type Props = { children: React.ReactNode; color?: 'slate' | 'green' | 'red' | 'blue' | 'indigo'; className?: string; variant?: 'solid' | 'soft' };
const colorMap = {
solid: {
amber: 'bg-amber-500 text-white',
slate: 'bg-slate-600 text-white',
green: 'bg-emerald-500 text-white',
red: 'bg-rose-500 text-white',
slate: 'bg-slate-500 text-white',
green: 'bg-green-500 text-white',
red: 'bg-red-500 text-white',
blue: 'bg-blue-500 text-white',
indigo: 'bg-indigo-500 text-white',
},
soft: {
amber: 'bg-amber-50 text-amber-700 border border-amber-200/60',
slate: 'bg-slate-50 text-slate-700 border border-slate-200/60',
green: 'bg-emerald-50 text-emerald-700 border border-emerald-200/60',
red: 'bg-rose-50 text-rose-700 border border-rose-200/60',
green: 'bg-green-50 text-green-700 border border-green-200/60',
red: 'bg-red-50 text-red-700 border border-red-200/60',
blue: 'bg-blue-50 text-blue-700 border border-blue-200/60',
indigo: 'bg-indigo-50 text-indigo-700 border border-indigo-200/60',
},
};
export const Badge = ({ children, color = 'amber', variant = 'soft', className }: Props) => (
export const Badge = ({ children, color = 'indigo', variant = 'soft', className }: Props) => (
<span className={cn('inline-flex items-center px-2.5 py-1 text-xs rounded-full font-semibold transition-colors', colorMap[variant][color], className)}>
{children}
</span>

View File

@@ -5,7 +5,7 @@ export const Chip = ({ children, active, onClick, className }: { children: React
onClick={onClick}
className={cn(
'px-3 py-1.5 rounded-full border text-sm transition font-medium',
active ? 'border-amber-500 bg-amber-50 text-amber-700' : 'border-slate-200 text-slate-700 hover:bg-slate-100',
active ? 'border-indigo-500 bg-indigo-50 text-indigo-700' : 'border-slate-200 text-slate-700 hover:bg-slate-100',
className
)}
>

View File

@@ -14,7 +14,7 @@ export const Input = ({ className, label, error, ...props }: Props) => {
'placeholder:text-slate-400',
error
? 'border-rose-300 focus:border-rose-500 focus:ring-2 focus:ring-rose-500/30'
: 'border-slate-300 focus:border-amber-500 focus:outline-none focus:ring-2 focus:ring-amber-500/30',
: 'border-slate-300 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/30',
className
)}
{...props}

View File

@@ -11,13 +11,13 @@ export const Tabs = ({ tabs, active, onChange, className }: { tabs: Tab[]; activ
className={cn(
"px-6 py-2.5 text-sm font-black uppercase tracking-widest transition-all rounded-xl whitespace-nowrap",
active === t.id
? "bg-slate-900 text-amber-500 shadow-xl"
? "bg-slate-900 text-indigo-400 shadow-xl"
: "text-slate-500 hover:text-slate-900 hover:bg-white/50"
)}
>
{t.label}
{t.badge && (
<span className="ml-2 inline-flex items-center justify-center w-5 h-5 text-[10px] font-black text-white bg-slate-900 rounded-full border border-amber-500/50">
<span className="ml-2 inline-flex items-center justify-center w-5 h-5 text-[10px] font-black text-white bg-slate-900 rounded-full border border-indigo-500/50">
{t.badge}
</span>
)}

View File

@@ -16,63 +16,30 @@ export const mockShops: BarberShop[] = [
imageUrl:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='480'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0%25' stop-color='%23111827'/%3E%3Cstop offset='100%25' stop-color='%232563eb'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='800' height='480' fill='url(%23g)'/%3E%3Ccircle cx='640' cy='120' r='120' fill='%23ffffff22'/%3E%3Crect x='80' y='320' width='300' height='12' fill='%23ffffff66'/%3E%3Crect x='80' y='350' width='220' height='10' fill='%23ffffff55'/%3E%3C/text%3E%3C/svg%3E",
barbers: [
{ id: 'b1', name: 'João', specialties: ['Fade', 'Navalha'], schedule: [{ day: '2025-01-01', slots: ['09:00', '10:00', '11:00'] }] },
{ id: 'b2', name: 'Carlos', specialties: ['Barba', 'Clássico'], schedule: [{ day: '2025-01-01', slots: ['14:00', '15:00'] }] },
{ id: 'b1', name: 'João', specialties: ['Fade', 'Navalha'], schedule: [{ day: '2026-03-15', slots: ['09:00', '10:00', '11:00'] }] },
{ id: 'b2', name: 'Carlos', specialties: ['Barba', 'Clássico'], schedule: [{ day: '2026-03-15', slots: ['14:00', '15:00'] }] },
],
services: [
{ id: 'sv1', name: 'Corte Fade', price: 60, duration: 45, barberIds: ['b1'] },
{ id: 'sv2', name: 'Barba Completa', price: 40, duration: 30, barberIds: ['b2'] },
{ id: 'sv1', name: 'Corte', price: 15, duration: 45, barberIds: ['b1'] },
{ id: 'sv2', name: 'Barba', price: 10, duration: 30, barberIds: ['b2'] },
],
products: [
{ id: 'p1', name: 'Pomada Matte', price: 35, stock: 8 },
{ id: 'p2', name: 'Óleo para Barba', price: 45, stock: 5 },
{ id: 'p1', name: 'Pomada Matte', price: 12, stock: 8 },
{ id: 'p2', name: 'Óleo para Barba', price: 15, stock: 5 },
],
},
{
id: 's2',
name: 'Barbearia Bairro',
address: 'Av. Verde, 45',
name: 'Barbearia do Bairro',
address: 'Av. Principal, 45',
rating: 4.5,
imageUrl:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='480'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0%25' stop-color='%230f172a'/%3E%3Cstop offset='100%25' stop-color='%230ea5e9'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='800' height='480' fill='url(%23g)'/%3E%3Crect x='60' y='90' width='260' height='180' rx='24' fill='%23ffffff1f'/%3E%3Crect x='380' y='260' width='300' height='12' fill='%23ffffff66'/%3E%3Crect x='380' y='290' width='180' height='10' fill='%23ffffff55'/%3E%3C/svg%3E",
barbers: [
{ id: 'b3', name: 'Miguel', specialties: ['Clássico', 'Fade'], schedule: [{ day: '2025-01-01', slots: ['09:30', '10:30'] }] },
{ id: 'b3', name: 'Miguel', specialties: ['Clássico', 'Fade'], schedule: [{ day: '2026-03-15', slots: ['09:30', '10:30'] }] },
],
services: [{ id: 'sv3', name: 'Corte Clássico', price: 50, duration: 40, barberIds: ['b3'] }],
products: [{ id: 'p3', name: 'Shampoo Masculino', price: 30, stock: 10 }],
},
{
id: 's3',
name: 'Barbearia Central',
address: 'Rua Principal, 123',
rating: 4.7,
imageUrl:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='480'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0%25' stop-color='%23111827'/%3E%3Cstop offset='100%25' stop-color='%232563eb'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='800' height='480' fill='url(%23g)'/%3E%3Ccircle cx='640' cy='120' r='120' fill='%23ffffff22'/%3E%3Crect x='80' y='320' width='300' height='12' fill='%23ffffff66'/%3E%3Crect x='80' y='350' width='220' height='10' fill='%23ffffff55'/%3E%3C/text%3E%3C/svg%3E",
barbers: [
{ id: 'b1', name: 'João', specialties: ['Fade', 'Navalha'], schedule: [{ day: '2025-01-01', slots: ['09:00', '10:00', '11:00'] }] },
{ id: 'b2', name: 'Carlos', specialties: ['Barba', 'Clássico'], schedule: [{ day: '2025-01-01', slots: ['14:00', '15:00'] }] },
],
services: [
{ id: 'sv1', name: 'Corte Fade', price: 60, duration: 45, barberIds: ['b1'] },
{ id: 'sv2', name: 'Barba Completa', price: 40, duration: 30, barberIds: ['b2'] },
],
products: [
{ id: 'p1', name: 'Pomada Matte', price: 35, stock: 8 },
{ id: 'p2', name: 'Óleo para Barba', price: 45, stock: 5 },
],
},
{
id: 's4',
name: 'Barbearia Bairro',
address: 'Av. Verde, 45',
rating: 4.5,
imageUrl:
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='480'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0%25' stop-color='%230f172a'/%3E%3Cstop offset='100%25' stop-color='%230ea5e9'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='800' height='480' fill='url(%23g)'/%3E%3Crect x='60' y='90' width='260' height='180' rx='24' fill='%23ffffff1f'/%3E%3Crect x='380' y='260' width='300' height='12' fill='%23ffffff66'/%3E%3Crect x='380' y='290' width='180' height='10' fill='%23ffffff55'/%3E%3C/svg%3E",
barbers: [
{ id: 'b3', name: 'Miguel', specialties: ['Clássico', 'Fade'], schedule: [{ day: '2025-01-01', slots: ['09:30', '10:30'] }] },
],
services: [{ id: 'sv3', name: 'Corte Clássico', price: 50, duration: 40, barberIds: ['b3'] }],
products: [{ id: 'p3', name: 'Shampoo Masculino', price: 30, stock: 10 }],
services: [{ id: 'sv3', name: 'Corte Clássico', price: 12, duration: 40, barberIds: ['b3'] }],
products: [{ id: 'p3', name: 'Shampoo', price: 8, stock: 10 }],
},
];

View File

@@ -4,8 +4,9 @@
@layer base {
:root {
--brand-gold: #d97706;
--brand-gold-light: #fbbf24;
--brand-primary: #4f46e5;
--brand-primary-light: #818cf8;
--brand-primary-dark: #3730a3;
--obsidian: #0f172a;
--obsidian-light: #1e293b;
--slate-950: #020617;
@@ -19,7 +20,7 @@
body {
@apply bg-[#f8fafc] text-slate-900 font-sans antialiased;
background-image:
radial-gradient(at 0% 0%, rgba(217, 119, 6, 0.03) 0px, transparent 50%),
radial-gradient(at 0% 0%, rgba(79, 70, 229, 0.03) 0px, transparent 50%),
radial-gradient(at 100% 0%, rgba(15, 23, 42, 0.03) 0px, transparent 50%);
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
min-height: 100vh;
@@ -39,7 +40,7 @@
}
::-webkit-scrollbar-thumb {
@apply bg-slate-300 rounded-full hover:bg-slate-400;
@apply bg-indigo-200 rounded-full hover:bg-indigo-300;
}
}
@@ -52,12 +53,9 @@
@apply bg-white/80 backdrop-blur-md border border-white/20 shadow-xl shadow-slate-200/50;
}
.premium-shadow {
box-shadow: 0 10px 40px -10px rgba(15, 23, 42, 0.1);
}
.gold-gradient {
@apply bg-gradient-to-br from-amber-500 via-amber-600 to-amber-700;
.indigo-gradient {
@apply bg-gradient-to-br from-indigo-500 via-indigo-600 to-indigo-700;
}
.obsidian-gradient {

View File

@@ -1,6 +1,6 @@
export const brand = {
primary: 'amber-500',
primaryDark: 'amber-600',
primary: 'indigo-600',
primaryDark: 'indigo-700',
bg: 'slate-50',
card: 'white',
text: 'slate-900',

View File

@@ -67,9 +67,9 @@ export default function AuthLogin() {
return (
<div className="min-h-[80vh] flex items-center justify-center px-6 py-12">
<Card className="w-full max-w-[440px] p-10 space-y-8 glass-card border-none rounded-[2rem] premium-shadow animate-in fade-in zoom-in duration-500">
<Card className="w-full max-w-[440px] p-10 space-y-8 glass-card border-none rounded-[2rem] animate-in fade-in zoom-in duration-500">
<div className="text-center space-y-4">
<Link to="/" className="inline-flex p-4 bg-slate-900 rounded-2xl text-amber-500 shadow-xl mb-2 hover:scale-105 transition-transform">
<Link to="/" className="inline-flex p-4 bg-slate-900 rounded-2xl text-indigo-400 shadow-xl mb-2 hover:scale-105 transition-transform">
<LogIn size={32} />
</Link>
<div className="space-y-1">
@@ -93,7 +93,7 @@ export default function AuthLogin() {
onChange={(e) => setEmail(e.target.value)}
placeholder="exemplo@email.com"
required
className="rounded-xl border-slate-200/60 focus:ring-amber-500/20"
className="rounded-xl border-slate-200/60 focus:ring-indigo-500/20"
/>
<Input
@@ -103,14 +103,14 @@ export default function AuthLogin() {
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="rounded-xl border-slate-200/60 focus:ring-amber-500/20"
className="rounded-xl border-slate-200/60 focus:ring-indigo-500/20"
/>
</div>
<Button type="submit" className="w-full h-12 bg-slate-900 hover:bg-slate-800 text-amber-500 font-bold rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-[0.98]" disabled={loading}>
<Button type="submit" className="w-full h-12 bg-slate-900 hover:bg-slate-800 text-indigo-400 font-bold rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-[0.98]" disabled={loading}>
{loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-amber-500 border-t-transparent rounded-full animate-spin" />
<div className="w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
<span>A entrar...</span>
</div>
) : 'Entrar na Conta'}
@@ -120,7 +120,7 @@ export default function AuthLogin() {
<div className="text-center pt-6 border-t border-slate-100">
<p className="text-sm text-slate-500 font-medium">
Ainda não tem conta?{' '}
<Link to="/registo" className="text-amber-600 font-bold hover:text-amber-700 underline-offset-4 hover:underline transition-all">
<Link to="/registo" className="text-indigo-600 font-bold hover:text-indigo-700 underline-offset-4 hover:underline transition-all">
Criar conta grátis
</Link>
</p>

View File

@@ -109,9 +109,9 @@ export default function AuthRegister() {
return (
<div className="min-h-[80vh] flex items-center justify-center px-6 py-12">
<Card className="w-full max-w-[500px] p-10 space-y-8 glass-card border-none rounded-[2rem] premium-shadow animate-in fade-in zoom-in duration-500">
<Card className="w-full max-w-[500px] p-10 space-y-8 glass-card border-none rounded-[2rem] animate-in fade-in zoom-in duration-500">
<div className="text-center space-y-4">
<div className="inline-flex p-4 bg-slate-900 rounded-2xl text-amber-500 shadow-xl mb-2">
<div className="inline-flex p-4 bg-slate-900 rounded-2xl text-indigo-400 shadow-xl mb-2">
<UserPlus size={32} />
</div>
<div className="space-y-1">
@@ -142,15 +142,15 @@ export default function AuthRegister() {
setError('')
}}
className={`p-4 rounded-2xl border-2 transition-all group ${role === r
? 'border-slate-900 bg-slate-900 text-amber-500 shadow-lg'
? 'border-slate-900 bg-slate-900 text-indigo-400 shadow-lg'
: 'border-slate-100 bg-slate-50/50 hover:border-slate-200 text-slate-500'
}`}
>
<div className="flex flex-col items-center gap-2">
{r === 'cliente' ? (
<User size={20} className={role === r ? 'text-amber-500' : 'text-slate-400 group-hover:text-slate-600'} />
<User size={20} className={role === r ? 'text-indigo-400' : 'text-slate-400 group-hover:text-slate-600'} />
) : (
<Scissors size={20} className={role === r ? 'text-amber-500' : 'text-slate-400 group-hover:text-slate-600'} />
<Scissors size={20} className={role === r ? 'text-indigo-400' : 'text-slate-400 group-hover:text-slate-600'} />
)}
<span className="text-sm font-bold uppercase tracking-tight">
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
@@ -168,7 +168,7 @@ export default function AuthRegister() {
onChange={(e) => setName(e.target.value)}
placeholder="Ex: João Silva"
required
className="rounded-xl border-slate-200 focus:ring-amber-500/20"
className="rounded-xl border-slate-200 focus:ring-indigo-500/20"
/>
<Input
@@ -178,7 +178,7 @@ export default function AuthRegister() {
onChange={(e) => setEmail(e.target.value)}
placeholder="exemplo@email.com"
required
className="rounded-xl border-slate-200 focus:ring-amber-500/20"
className="rounded-xl border-slate-200 focus:ring-indigo-500/20"
/>
<Input
@@ -188,7 +188,7 @@ export default function AuthRegister() {
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="rounded-xl border-slate-200 focus:ring-amber-500/20"
className="rounded-xl border-slate-200 focus:ring-indigo-500/20"
/>
{role === 'barbearia' && (
@@ -203,10 +203,10 @@ export default function AuthRegister() {
)}
</div>
<Button type="submit" className="w-full h-12 bg-slate-900 hover:bg-slate-800 text-amber-500 font-bold rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-[0.98]" disabled={loading}>
<Button type="submit" className="w-full h-12 bg-slate-900 hover:bg-slate-800 text-indigo-400 font-bold rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-[0.98]" disabled={loading}>
{loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-amber-500 border-t-transparent rounded-full animate-spin" />
<div className="w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
<span>A processar...</span>
</div>
) : 'Criar minha conta'}
@@ -216,7 +216,7 @@ export default function AuthRegister() {
<div className="text-center pt-6 border-t border-slate-100">
<p className="text-sm text-slate-500 font-medium">
tem uma conta?{' '}
<Link to="/login" className="text-amber-600 font-bold hover:text-amber-700 underline-offset-4 hover:underline transition-all">
<Link to="/login" className="text-indigo-600 font-bold hover:text-indigo-700 underline-offset-4 hover:underline transition-all">
Fazer Login
</Link>
</p>

View File

@@ -99,16 +99,16 @@ export default function Booking() {
return (
<div className="max-w-4xl mx-auto space-y-10 py-4 pb-20">
<header className="space-y-4 text-center">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-amber-100 text-amber-700 text-[10px] font-black uppercase tracking-widest">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-indigo-100 text-indigo-700 text-[10px] font-black uppercase tracking-widest">
<Calendar size={10} fill="currentColor" />
<span>Reserva Exclusiva</span>
</div>
<div className="space-y-1">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 tracking-tighter uppercase italic">
Agendar em <span className="text-amber-600 block md:inline">{shop.name}</span>
Reservar em <span className="text-indigo-600 block md:inline">{shop.name}</span>
</h1>
<div className="flex items-center justify-center gap-2 text-slate-500 font-medium">
<MapPin size={14} className="text-amber-600" />
<MapPin size={14} className="text-indigo-600" />
<p className="text-sm">{shop.address}</p>
</div>
</div>
@@ -127,9 +127,9 @@ export default function Booking() {
disabled={!s.completed && s.id > step}
className={`w-12 h-12 rounded-2xl flex items-center justify-center border-2 transition-all duration-500 scale-100 active:scale-90 ${
s.active
? 'bg-slate-900 border-slate-900 text-amber-500 shadow-2xl shadow-slate-300 -translate-y-2'
? 'bg-slate-900 border-slate-900 text-indigo-400 shadow-2xl shadow-slate-300 -translate-y-2'
: s.completed
? 'bg-amber-500 border-amber-500 text-white'
? 'bg-indigo-600 border-indigo-600 text-white'
: 'bg-white border-slate-200 text-slate-400'
}`}
>
@@ -162,8 +162,8 @@ export default function Booking() {
{step === 1 && (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="space-y-2 text-center md:text-left">
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">1. Selecione o <span className="text-amber-600">Serviço</span></h3>
<p className="text-slate-500 font-medium italic">O primeiro passo para a sua transformação de elite.</p>
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">1. Selecione o <span className="text-indigo-600">Serviço</span></h3>
<p className="text-slate-500 font-medium italic">O primeiro passo para o seu agendamento.</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
{shop.services.map((s) => (
@@ -176,12 +176,12 @@ export default function Booking() {
className={`group p-6 rounded-[2rem] border-2 text-left transition-all duration-300 flex flex-col gap-4 ${
serviceId === s.id
? 'border-slate-900 bg-slate-900 text-white shadow-2xl translate-y-[-4px]'
: 'border-slate-50 bg-slate-50 hover:border-amber-200 hover:bg-amber-50/50'
: 'border-slate-50 bg-slate-50 hover:border-indigo-200 hover:bg-indigo-50/50'
}`}
>
<div className="flex items-start justify-between">
<div className="space-y-1">
<div className={`font-black text-xl tracking-tight uppercase italic ${serviceId === s.id ? 'text-amber-500' : 'text-slate-900 group-hover:text-amber-600'}`}>
<div className={`font-black text-xl tracking-tight uppercase italic ${serviceId === s.id ? 'text-indigo-500' : 'text-slate-900 group-hover:text-indigo-600'}`}>
{s.name}
</div>
<div className={`flex items-center gap-2 text-xs font-bold uppercase tracking-widest ${serviceId === s.id ? 'text-slate-400' : 'text-slate-500'}`}>
@@ -202,7 +202,7 @@ export default function Booking() {
{step === 2 && (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="space-y-2 text-center md:text-left">
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">2. Escolha o <span className="text-amber-600">Mestre</span></h3>
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">2. Escolha o <span className="text-indigo-600">Mestre</span></h3>
<p className="text-slate-500 font-medium italic">Selecione o artista que cuidará do seu visual.</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
@@ -216,10 +216,10 @@ export default function Booking() {
className={`group p-6 rounded-[2.5rem] border-2 text-center transition-all duration-300 flex flex-col items-center gap-5 ${
barberId === b.id
? 'border-slate-900 bg-slate-900 text-white shadow-2xl translate-y-[-4px]'
: 'border-slate-50 bg-slate-50 hover:border-amber-200 hover:bg-amber-50/50'
: 'border-slate-50 bg-slate-50 hover:border-indigo-200 hover:bg-indigo-50/50'
}`}
>
<div className={`w-32 h-32 rounded-[2rem] overflow-hidden border-4 transition-all duration-500 ${barberId === b.id ? 'border-amber-500 rotate-3' : 'border-white group-hover:border-amber-100'}`}>
<div className={`w-32 h-32 rounded-[2rem] overflow-hidden border-4 transition-all duration-500 ${barberId === b.id ? 'border-indigo-500 rotate-3' : 'border-white group-hover:border-indigo-100'}`}>
{b.imageUrl ? (
<img src={b.imageUrl} alt={b.name} className="w-full h-full object-cover" />
) : (
@@ -229,9 +229,9 @@ export default function Booking() {
)}
</div>
<div className="space-y-1">
<p className={`font-black text-lg uppercase italic tracking-tight ${barberId === b.id ? 'text-amber-500' : 'text-slate-900 group-hover:text-amber-600'}`}>{b.name}</p>
<p className={`font-black text-lg uppercase italic tracking-tight ${barberId === b.id ? 'text-indigo-500' : 'text-slate-900 group-hover:text-indigo-600'}`}>{b.name}</p>
<p className={`text-[10px] font-black uppercase tracking-[0.2em] ${barberId === b.id ? 'text-slate-400' : 'text-slate-400'}`}>
{b.specialties[0] || 'Elite Barber'}
{b.specialties[0] || 'Barbeiro'}
</p>
</div>
</button>
@@ -243,11 +243,11 @@ export default function Booking() {
{step === 3 && (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="space-y-2 text-center md:text-left">
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">3. Defina o <span className="text-amber-600">Momento</span></h3>
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">3. Escolha o <span className="text-indigo-600">Horário</span></h3>
<p className="text-slate-500 font-medium italic">Seu tempo é valioso. Escolha a data perfeita.</p>
</div>
<div className="max-w-md mx-auto relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-amber-600 pointer-events-none z-10">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-indigo-600 pointer-events-none z-10">
<Calendar size={20} />
</div>
<Input
@@ -258,7 +258,7 @@ export default function Booking() {
if (e.target.value) setStep(4);
}}
min={new Date().toISOString().split('T')[0]}
className="h-16 pl-14 pr-6 bg-slate-50 border-none rounded-2xl text-lg font-black uppercase tracking-widest text-slate-900 focus:ring-2 focus:ring-amber-500/20 transition-all shadow-inner"
className="h-16 pl-14 pr-6 bg-slate-50 border-none rounded-2xl text-lg font-black uppercase tracking-widest text-slate-900 focus:ring-2 focus:ring-indigo-500/20 transition-all shadow-inner"
/>
</div>
</div>
@@ -267,7 +267,7 @@ export default function Booking() {
{step === 4 && (
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="space-y-2 text-center md:text-left">
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">4. Escolha o <span className="text-amber-600">Horário</span></h3>
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">4. Escolha o <span className="text-indigo-600">Horário Privilegiado</span></h3>
<p className="text-slate-500 font-medium italic">A pontualidade é a cortesia dos reis.</p>
</div>
@@ -275,12 +275,12 @@ export default function Booking() {
{/* Left Side: Summary Sidebar */}
<div className="w-full md:w-80 space-y-4">
<div className="p-6 bg-slate-900 text-white rounded-[2rem] space-y-6 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
<div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
<div className="space-y-4 relative z-10">
<div className="space-y-1 border-b border-white/10 pb-4">
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Serviço</p>
<p className="text-lg font-black uppercase italic tracking-tight">{selectedService?.name}</p>
<p className="text-xl font-black text-amber-500 tracking-tighter">{currency(selectedService?.price || 0)}</p>
<p className="text-xl font-black text-indigo-500 tracking-tighter">{currency(selectedService?.price || 0)}</p>
</div>
<div className="space-y-1 border-b border-white/10 pb-4">
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Mestre</p>
@@ -306,8 +306,8 @@ export default function Booking() {
onClick={() => setSlot(h)}
className={`h-14 rounded-2xl border-2 text-sm font-black tracking-widest transition-all duration-300 ${
slot === h
? 'border-slate-900 bg-slate-900 text-amber-500 shadow-xl scale-105 z-10'
: 'border-slate-50 bg-slate-50 text-slate-600 hover:border-amber-200 hover:bg-amber-50'
? 'border-slate-900 bg-slate-900 text-indigo-400 shadow-xl scale-105 z-10'
: 'border-slate-50 bg-slate-50 text-slate-600 hover:border-indigo-200 hover:bg-indigo-50'
}`}
>
{h}
@@ -325,9 +325,9 @@ export default function Booking() {
<Button
onClick={submit}
size="lg"
className="w-full h-16 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-2xl shadow-2xl transition-all active:scale-95 uppercase tracking-[0.2em] text-sm italic"
className="w-full h-16 bg-slate-900 hover:bg-slate-800 text-indigo-400 font-black rounded-2xl shadow-2xl transition-all active:scale-95 uppercase tracking-[0.2em] text-sm italic"
>
Confirmar Experiência de Elite
Confirmar Agendamento
</Button>
<p className="text-center mt-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
Pagamento realizado após o serviço

View File

@@ -367,8 +367,8 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
key={p}
onClick={() => setPeriod(p)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${period === p
? 'border-amber-500 bg-amber-50 text-amber-700 shadow-sm'
: 'border-slate-200 text-slate-700 hover:border-amber-300 hover:bg-amber-50/50'
? 'border-indigo-500 bg-indigo-50 text-indigo-700 shadow-sm'
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50/50'
}`}
>
{p === 'mes' ? 'Mês' : p === 'hoje' ? 'Hoje' : p === 'semana' ? 'Semana' : 'Total'}
@@ -550,13 +550,13 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
<h2 className="text-xl font-bold text-slate-900">Caixa de Pedidos</h2>
<p className="text-sm text-slate-600">Aprove ou recuse os pedidos pendentes e conclua os serviços de hoje.</p>
</div>
<Badge color="amber" variant="soft">{pendingAppts} Novos Pedidos</Badge>
<Badge color="indigo" variant="soft">{pendingAppts} Novos Pedidos</Badge>
</div>
{/* Secção de Pedidos Pendentes */}
<Card className="p-6 border-amber-200">
<Card className="p-6 border-indigo-200">
<div className="flex items-center gap-2 mb-4 pb-4 border-b border-slate-100">
<Clock className="text-amber-500" size={20} />
<Clock className="text-indigo-500" size={20} />
<h3 className="font-bold text-slate-900">Aguardam Aprovação</h3>
<Badge color="slate" variant="soft" className="ml-auto">{pendingAppts}</Badge>
</div>
@@ -569,7 +569,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
const aptDate = new Date(a.date.replace(' ', 'T'));
return (
<div key={a.id} className="flex flex-col sm:flex-row sm:items-center justify-between p-4 bg-amber-50/50 border border-amber-100 rounded-lg gap-4">
<div key={a.id} className="flex flex-col sm:flex-row sm:items-center justify-between p-4 bg-indigo-50/50 border border-indigo-100 rounded-lg gap-4">
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<p className="text-xs text-slate-500 mb-1">Cliente</p>
@@ -582,7 +582,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
</div>
<div>
<p className="text-xs text-slate-500 mb-1">Data / Hora</p>
<p className="font-semibold text-amber-700">
<p className="font-semibold text-indigo-700">
{aptDate.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' })} às {aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' })}
</p>
</div>
@@ -596,7 +596,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
Recusar
</Button>
<Button
className="bg-amber-500 hover:bg-amber-600 text-white"
className="bg-indigo-600 hover:bg-indigo-700 text-white"
onClick={() => updateAppointmentStatus(a.id, 'confirmado')}
>
Aceitar
@@ -744,16 +744,16 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
return sum + (prod?.price ?? 0) * item.qty;
}, 0);
return (
<div key={o.id} className="p-4 border border-slate-200 rounded-lg hover:border-amber-300 transition-colors">
<div key={o.id} className="p-4 border border-slate-200 rounded-lg hover:border-indigo-300 transition-colors">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<p className="font-bold text-amber-700">{currency(productTotal)}</p>
<Badge color={o.status === 'pendente' ? 'amber' : o.status === 'confirmado' ? 'green' : o.status === 'concluido' ? 'green' : 'red'}>
<p className="font-bold text-indigo-700">{currency(productTotal)}</p>
<Badge color={o.status === 'pendente' ? 'indigo' : o.status === 'confirmado' ? 'green' : o.status === 'concluido' ? 'green' : 'red'}>
{o.status === 'pendente' ? 'Pendente' : o.status === 'confirmado' ? 'Confirmado' : o.status === 'concluido' ? 'Concluído' : 'Cancelado'}
</Badge>
</div>
<select
className="text-sm border border-slate-300 rounded-lg px-3 py-2 focus:border-amber-500 focus:ring-2 focus:ring-amber-500/30"
className="text-sm border border-slate-300 rounded-lg px-3 py-2 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30"
value={o.status}
onChange={(e) => updateOrderStatus(o.id, e.target.value as any)}
>
@@ -771,7 +771,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
return (
<div key={item.refId} className="flex items-center justify-between text-sm bg-slate-50 rounded px-2 py-1">
<span className="text-slate-700">{prod?.name ?? 'Produto'} x{item.qty}</span>
<span className="text-amber-600 font-semibold">{currency((prod?.price ?? 0) * item.qty)}</span>
<span className="text-indigo-600 font-semibold">{currency((prod?.price ?? 0) * item.qty)}</span>
</div>
);
})}
@@ -805,7 +805,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
<p className="text-sm text-slate-600">Duração: {s.duration} min</p>
</div>
<div className="flex items-center gap-4">
<span className="text-lg font-bold text-amber-600">{currency(s.price)}</span>
<span className="text-lg font-bold text-indigo-600">{currency(s.price)}</span>
<Button variant="danger" size="sm" onClick={() => deleteService(shop.id, s.id)}>
<Trash2 size={16} />
</Button>
@@ -839,8 +839,8 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
<Badge color="slate" variant="soft">{shop.products.length} produtos</Badge>
</div>
{lowStock.length > 0 && (
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p className="text-sm font-semibold text-amber-800 flex items-center gap-2">
<div className="mb-4 p-3 bg-indigo-50 border border-indigo-200 rounded-lg">
<p className="text-sm font-semibold text-indigo-800 flex items-center gap-2">
<AlertTriangle size={16} />
Atenção: {lowStock.length} {lowStock.length === 1 ? 'produto com stock baixo' : 'produtos com stock baixo'}
</p>
@@ -850,18 +850,18 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
{shop.products.map((p) => (
<div
key={p.id}
className={`flex items-center justify-between p-4 border rounded-lg ${p.stock <= 3 ? 'border-amber-300 bg-amber-50' : 'border-slate-200'
className={`flex items-center justify-between p-4 border rounded-lg ${p.stock <= 3 ? 'border-indigo-300 bg-indigo-50' : 'border-slate-200'
}`}
>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<p className="font-bold text-slate-900">{p.name}</p>
{p.stock <= 3 && <Badge color="amber" variant="solid">Stock baixo</Badge>}
{p.stock <= 3 && <Badge color="indigo" variant="solid">Stock baixo</Badge>}
</div>
<p className="text-sm text-slate-600">Stock: {p.stock} unidades</p>
</div>
<div className="flex items-center gap-4">
<span className="text-lg font-bold text-amber-600">{currency(p.price)}</span>
<span className="text-lg font-bold text-indigo-600">{currency(p.price)}</span>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => updateProductStock(p, -1)}>
<Minus size={14} />

View File

@@ -63,7 +63,7 @@ export default function EventsCreate() {
<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">
<div className="p-3 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-xl text-white shadow-lg">
<CalendarPlus size={22} />
</div>
<div>

View File

@@ -57,12 +57,12 @@ export default function Explore() {
<section className="space-y-4 text-center md:text-left">
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div className="space-y-1">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-amber-100 text-amber-700 text-[10px] font-black uppercase tracking-widest mb-2">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-indigo-100 text-indigo-700 text-[10px] font-black uppercase tracking-widest mb-2">
<Star size={10} fill="currentColor" />
<span>As melhores Barbearias</span>
<span>As Nossas Barbearias</span>
</div>
<h1 className="text-4xl md:text-5xl font-black text-slate-900 tracking-tighter">
Explorar <span className="text-amber-600">Espaços</span>
<h1 className="text-4xl md:text-5xl font-black text-slate-900 tracking-tighter uppercase italic">
Ver <span className="text-indigo-600">Barbearias</span>
</h1>
<p className="text-slate-500 font-medium max-w-md">Descubra barbearias exclusivas e reserve o seu próximo corte em segundos.</p>
</div>
@@ -91,14 +91,14 @@ export default function Explore() {
<Chip
active={filter === 'todas'}
onClick={() => setFilter('todas')}
className={`h-11 px-6 rounded-2xl font-bold uppercase tracking-tight transition-all ${filter === 'todas' ? '!bg-slate-900 !text-amber-500 border-none shadow-lg' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
className={`h-11 px-6 rounded-2xl font-bold uppercase tracking-tight transition-all ${filter === 'todas' ? '!bg-slate-900 !text-indigo-400 border-none shadow-lg' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
>
Todas
</Chip>
<Chip
active={filter === 'top'}
onClick={() => setFilter('top')}
className={`h-11 px-6 rounded-2xl font-bold uppercase tracking-tight transition-all ${filter === 'top' ? '!bg-slate-900 !text-amber-500 border-none shadow-lg' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
className={`h-11 px-6 rounded-2xl font-bold uppercase tracking-tight transition-all ${filter === 'top' ? '!bg-slate-900 !text-indigo-400 border-none shadow-lg' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
>
Top Avaliadas
</Chip>
@@ -106,7 +106,7 @@ export default function Explore() {
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
className="h-11 rounded-2xl border-none bg-slate-100 px-4 text-sm font-bold text-slate-700 focus:ring-2 focus:ring-amber-500/20"
className="h-11 rounded-2xl border-none bg-slate-100 px-4 text-sm font-bold text-slate-700 focus:ring-2 focus:ring-indigo-500/20"
>
<option value="avaliacao">Melhor avaliação</option>
<option value="servicos">Mais serviços</option>
@@ -117,7 +117,7 @@ export default function Explore() {
{!useApp().shopsReady ? (
<div className="py-24 text-center">
<div className="inline-block w-12 h-12 border-4 border-slate-200 border-t-amber-600 rounded-full animate-spin mb-4" />
<div className="inline-block w-12 h-12 border-4 border-slate-200 border-t-indigo-600 rounded-full animate-spin mb-4" />
<p className="text-slate-500 font-bold uppercase tracking-widest text-xs">A carregar espaços...</p>
</div>
) : filtered.length === 0 ? (
@@ -129,7 +129,7 @@ export default function Explore() {
<p className="text-2xl font-black text-slate-900 tracking-tight">Nenhuma barbearia encontrada</p>
<p className="text-slate-500 font-medium">Tente ajustar o termo de pesquisa ou os filtros ativos.</p>
</div>
<Button variant="ghost" onClick={() => {setQuery(''); setFilter('todas');}} className="font-bold text-amber-600 hover:text-amber-700">
<Button variant="ghost" onClick={() => {setQuery(''); setFilter('todas');}} className="font-bold text-indigo-600 hover:text-indigo-700">
Limpar Tudo
</Button>
</Card>

View File

@@ -19,7 +19,7 @@ import { useApp } from '../context/AppContext';
import { mockShops } from '../data/mock';
export default function Landing() {
const { user } = useApp();
const { user, shops } = useApp();
const navigate = useNavigate();
useEffect(() => {
@@ -28,36 +28,36 @@ export default function Landing() {
navigate(target, { replace: true });
}, [user, navigate]);
const featuredShops = mockShops.slice(0, 3);
const featuredShops = shops.slice(0, 3);
return (
<div className="space-y-24 md:space-y-32 pb-24">
{/* Hero Section - Midnight Luxury Style */}
<section className="relative overflow-hidden rounded-[3rem] obsidian-gradient text-white px-8 py-20 md:px-16 md:py-32 shadow-[0_20px_50px_rgba(0,0,0,0.3)] border border-white/5">
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] opacity-20 pointer-events-none"></div>
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-amber-500/10 rounded-full blur-[120px] -translate-y-1/2 translate-x-1/2"></div>
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-indigo-500/10 rounded-full blur-[120px] -translate-y-1/2 translate-x-1/2"></div>
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-slate-500/10 rounded-full blur-[120px] translate-y-1/2 -translate-x-1/2"></div>
<div className="relative z-10 space-y-10 max-w-5xl">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-white/5 backdrop-blur-md rounded-full text-[10px] font-black uppercase tracking-[0.3em] w-fit border border-white/10 animate-fade-in text-amber-500">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-white/5 backdrop-blur-md rounded-full text-[10px] font-black uppercase tracking-[0.3em] w-fit border border-white/10 animate-fade-in text-indigo-400">
<Sparkles size={14} className="animate-pulse" />
<span>O Novo Standard do Cuidado Masculino</span>
<span>A Solução Completa para a Sua Barbearia</span>
</div>
<h1 className="text-6xl md:text-8xl font-black leading-[0.9] tracking-tighter text-balance uppercase italic">
Elegância em cada <br />
<span className="gold-gradient bg-clip-text text-transparent italic">Agendamento</span>
Gestão Simplificada da sua <br />
<span className="indigo-gradient bg-clip-text text-transparent italic">Barbearia</span>
</h1>
<p className="text-xl md:text-2xl text-slate-300 max-w-2xl leading-relaxed font-medium">
Transforme a rotina da sua barbearia com uma experiência digital digna de um cavalheiro.
Mobile-first, premium e inteligente.
Organize a sua barbearia com facilidade.
Simples, rápido e eficiente.
</p>
<div className="flex flex-wrap gap-6 pt-6">
<Button asChild size="lg" className="h-16 px-10 bg-white text-slate-950 hover:bg-amber-500 hover:text-white font-black uppercase tracking-widest text-xs transition-all duration-300 rounded-2xl shadow-2xl">
<Button asChild size="lg" className="h-16 px-10 bg-white text-slate-950 hover:bg-indigo-600 hover:text-white font-black uppercase tracking-widest text-xs transition-all duration-300 rounded-2xl shadow-2xl">
<Link to="/explorar" className="flex items-center gap-3">
Explorar Espaços
Ver Barbearias
<ArrowRight size={18} />
</Link>
</Button>
@@ -70,47 +70,30 @@ export default function Landing() {
<div className="grid grid-cols-3 gap-10 pt-12 border-t border-white/10 max-w-2xl">
<div className="space-y-1">
<div className="text-4xl md:text-5xl font-black tracking-tighter italic">500+</div>
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Espaços de Luxo</div>
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Lojas Registadas</div>
</div>
<div className="space-y-1">
<div className="text-4xl md:text-5xl font-black tracking-tighter italic">10K+</div>
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Cortes Marcados</div>
</div>
<div className="space-y-1">
<div className="text-4xl md:text-5xl font-black tracking-tighter gold-gradient bg-clip-text text-transparent italic">4.9</div>
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Rating de Elite</div>
<div className="text-4xl md:text-5xl font-black tracking-tighter indigo-gradient bg-clip-text text-transparent italic">4.9</div>
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Média de Avaliação</div>
</div>
</div>
</div>
</section>
{/* Hero Image Mockup (Place with a generated-like look) */}
<section className="relative -mt-32 px-6">
<div className="max-w-6xl mx-auto rounded-[3rem] overflow-hidden shadow-[0_50px_100px_rgba(0,0,0,0.5)] border-4 border-white/10 glass-card">
<div className="aspect-video bg-slate-900 flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-950 flex items-center justify-center opacity-80" />
<div className="relative z-10 text-center space-y-6">
<Scissors size={80} className="text-amber-500 mx-auto mb-4" />
<h3 className="text-2xl font-black text-white uppercase italic tracking-tighter">Smart Agenda Elite Edition</h3>
<div className="flex gap-4 justify-center">
<div className="w-12 h-1 bg-amber-500 rounded-full" />
<div className="w-12 h-1 bg-white/20 rounded-full" />
<div className="w-12 h-1 bg-white/20 rounded-full" />
</div>
</div>
</div>
</div>
</section>
{/* Features - Minimalist & Bold */}
<section className="max-w-7xl mx-auto px-6">
<div className="text-center md:text-left mb-16 flex flex-col md:flex-row md:items-end justify-between gap-6">
<div className="space-y-4">
<h2 className="text-5xl md:text-6xl font-black text-slate-900 tracking-tighter uppercase italic pr-8 border-l-[12px] border-amber-500 pl-8">
Ecossistema <br /> <span className="text-amber-600">Completo</span>
<h2 className="text-5xl md:text-6xl font-black text-slate-900 tracking-tighter uppercase italic pr-8 border-l-[12px] border-indigo-600 pl-8">
Tudo para o <br /> <span className="text-indigo-700">Seu Negócio</span>
</h2>
<p className="text-xl text-slate-500 font-medium max-w-xl">
Tudo o que a sua barbearia precisa para escalar com sofisticação.
Tudo o que precisa para gerir e fazer crescer a sua barbearia.
</p>
</div>
</div>
@@ -119,30 +102,27 @@ export default function Landing() {
{[
{
icon: Clock,
title: 'Gestão Cirúrgica',
desc: 'Controle de horários com precisão absoluta. Slot management inteligente e automação de reserva.',
color: 'bg-slate-950 shadow-[0_15px_35px_rgba(0,0,0,0.15)]'
title: 'Agenda Prática',
desc: 'Gerencie os horários dos seus barbeiros e evite conflitos de agenda de forma automática.',
},
{
icon: ShoppingBag,
title: 'Curadoria de Produtos',
desc: 'Venda produtos de elite diretamente no ecossistema. Gestão de stock e carrinho omnicanal.',
color: 'bg-white border-2 border-slate-50'
title: 'Venda de Produtos',
desc: 'Venda pomadas, óleos e outros produtos diretamente aos seus clientes através da plataforma.',
},
{
icon: BarChart3,
title: 'Analytics de Luxo',
desc: 'Relatórios detalhados de faturamento, performance de barbeiros e taxas de retenção.',
color: 'bg-white border-2 border-slate-50'
title: 'Controle de Ganhos',
desc: 'Saiba exatamente quanto a barbearia está faturando e quais os serviços mais procurados.',
},
].map((feature) => (
<Card key={feature.title} className={`p-10 space-y-6 rounded-[2.5rem] transition-all duration-500 hover:-translate-y-2 group ${feature.color}`}>
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center shadow-inner ${feature.icon === Clock ? 'bg-amber-500 text-slate-900' : 'bg-slate-900 text-amber-500'}`}>
<Card key={feature.title} className="p-10 space-y-6 rounded-[2.5rem] transition-all duration-500 hover:-translate-y-2 group bg-white border-2 border-slate-50 shadow-sm hover:shadow-xl">
<div className="w-16 h-16 rounded-2xl flex items-center justify-center bg-slate-900 text-indigo-400 group-hover:bg-indigo-600 group-hover:text-white transition-colors duration-300">
<feature.icon size={32} />
</div>
<div className="space-y-3">
<h3 className={`text-2xl font-black tracking-tight uppercase italic ${feature.icon === Clock ? 'text-white' : 'text-slate-900'}`}>{feature.title}</h3>
<p className={`text-sm leading-relaxed font-medium ${feature.icon === Clock ? 'text-slate-400' : 'text-slate-500'}`}>{feature.desc}</p>
<h3 className="text-2xl font-black tracking-tight uppercase italic text-slate-900">{feature.title}</h3>
<p className="text-sm leading-relaxed font-medium text-slate-600">{feature.desc}</p>
</div>
</Card>
))}
@@ -155,23 +135,23 @@ export default function Landing() {
<div className="max-w-6xl mx-auto px-6 relative z-10">
<div className="text-center mb-20">
<h2 className="text-5xl md:text-6xl font-black text-white tracking-tighter uppercase italic mb-6">
A Jornada do <span className="gold-gradient bg-clip-text text-transparent">Cavalheiro</span>
Como <span className="indigo-gradient bg-clip-text text-transparent">Funciona</span>
</h2>
<div className="w-24 h-1 bg-amber-500 mx-auto rounded-full" />
<div className="w-24 h-1 bg-indigo-600 mx-auto rounded-full" />
</div>
<div className="grid md:grid-cols-3 gap-16 relative">
{[
{ step: '01', title: 'Descobrir', desc: 'Encontre os espaços mais exclusivos da cidade com avaliações reais.' },
{ step: '02', title: 'Personalizar', desc: 'Escolha o seu barbeiro de confiança e o seu horário preferido.' },
{ step: '03', title: 'Vivenciar', desc: 'Receba o tratamento de elite que você merece, sem esperas.' },
{ step: '01', title: 'Explorar', desc: 'Encontre as barbearias mais próximas e veja as fotos e avaliações reais.' },
{ step: '02', title: 'Agendar', desc: 'Escolha o seu barbeiro e o serviço. Marque o dia e hora que preferir.' },
{ step: '03', title: 'Cortar', desc: 'Apareça na barbearia à hora marcada e desfrute do serviço. Sem esperas.' },
].map((item, idx) => (
<div key={item.step} className="text-center space-y-8 relative group">
<div className="relative">
<div className="text-8xl font-black text-white/5 absolute -top-12 left-1/2 -translate-x-1/2 select-none group-hover:text-amber-500/10 transition-colors duration-700">
<div className="text-8xl font-black text-white/5 absolute -top-12 left-1/2 -translate-x-1/2 select-none group-hover:text-indigo-500/10 transition-colors duration-700">
{item.step}
</div>
<div className="w-20 h-20 mx-auto rounded-full obsidian-gradient border-2 border-white/10 flex items-center justify-center text-amber-500 text-2xl font-black italic shadow-[0_0_30px_rgba(245,158,11,0.2)]">
<div className="w-20 h-20 mx-auto rounded-full obsidian-gradient border-2 border-white/10 flex items-center justify-center text-indigo-400 text-2xl font-black italic shadow-[0_0_30px_rgba(79,70,229,0.2)]">
{item.step}
</div>
</div>
@@ -189,9 +169,9 @@ export default function Landing() {
<section className="max-w-7xl mx-auto px-6">
<div className="flex flex-col md:flex-row items-center justify-between mb-16 gap-6">
<div className="space-y-2 text-center md:text-left">
<h2 className="text-4xl md:text-5xl font-black text-slate-900 tracking-tighter uppercase italic">
Clubes <span className="text-amber-600">Membros</span>
</h2>
<h1 className="text-4xl md:text-5xl font-black text-slate-900 tracking-tighter uppercase italic">
Descobrir <span className="text-indigo-600">Barbearias</span>
</h1>
<p className="text-slate-500 font-bold uppercase tracking-[0.2em] text-xs">As melhores barbearias do país</p>
</div>
<Button asChild variant="ghost" className="h-14 px-8 rounded-2xl bg-slate-50 border border-slate-100 font-black text-slate-900 uppercase tracking-widest text-xs hover:bg-slate-100 transition-all">
@@ -212,21 +192,21 @@ export default function Landing() {
{/* Final CTA - Immersive Dark */}
<section className="px-6">
<div className="max-w-6xl mx-auto relative overflow-hidden rounded-[4rem] obsidian-gradient text-white px-8 py-20 md:px-20 md:py-24 shadow-2xl border border-white/5">
<div className="absolute top-0 right-0 w-[400px] h-[400px] bg-amber-500/10 rounded-full blur-[100px]" />
<div className="absolute top-0 right-0 w-[400px] h-[400px] bg-indigo-500/10 rounded-full blur-[100px]" />
<div className="absolute bottom-0 left-0 w-[400px] h-[400px] bg-slate-500/10 rounded-full blur-[100px]" />
<div className="relative text-center space-y-12 max-w-3xl mx-auto">
<h2 className="text-5xl md:text-7xl font-black tracking-tighter uppercase italic leading-[0.9]">
Faça Parte <br /> do <span className="gold-gradient bg-clip-text text-transparent">Legado</span>
Registe a <br /> sua <span className="indigo-gradient bg-clip-text text-transparent">Barbearia</span>
</h2>
<p className="text-xl text-slate-400 font-medium leading-relaxed">
Centenas de profissionais elevaram o seu negócio ao próximo nível.
A sua barbearia merece o melhor.
</p>
<div className="flex flex-wrap justify-center gap-6 pt-4">
<Button asChild size="lg" className="h-16 px-10 bg-white text-slate-950 hover:bg-amber-500 hover:text-white font-black uppercase tracking-widest text-xs rounded-2xl transition-all shadow-2xl">
<Button asChild size="lg" className="h-16 px-10 bg-white text-slate-950 hover:bg-indigo-600 hover:text-white font-black uppercase tracking-widest text-xs rounded-2xl transition-all shadow-2xl">
<Link to="/registo" className="flex items-center gap-3">
Criar Conta Grátis
Criar Conta
<ArrowRight size={18} />
</Link>
</Button>

View File

@@ -14,8 +14,8 @@ import { Calendar, ShoppingBag, User, Clock, Heart, Star, MapPin, CheckCircle2 }
import { supabase } from '../lib/supabase'
import { ReviewModal } from '../components/ReviewModal'
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
pendente: 'amber',
const statusColor: Record<string, 'indigo' | 'green' | 'slate' | 'red'> = {
pendente: 'indigo',
confirmado: 'green',
concluido: 'green',
cancelado: 'red',
@@ -90,7 +90,7 @@ export default function Profile() {
return (
<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" />
<div className="w-10 h-10 border-4 border-slate-200 border-t-indigo-500 rounded-full animate-spin mx-auto mb-3" />
<p className="text-slate-500 text-sm">A carregar perfil...</p>
</div>
</div>
@@ -100,15 +100,15 @@ export default function Profile() {
if (!authId) {
return (
<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 className="w-16 h-16 bg-indigo-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<User size={28} className="text-indigo-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="px-5 py-2 bg-amber-500 text-white text-sm font-semibold rounded-xl hover:bg-amber-600 transition-colors"
className="px-5 py-2 bg-indigo-600 text-white text-sm font-semibold rounded-xl hover:bg-slate-900 transition-colors"
>
Ir para login
</button>
@@ -131,20 +131,20 @@ export default function Profile() {
)}
<div className="max-w-4xl mx-auto space-y-12 pb-20">
{/* Profile Header - Luxury Style */}
{/* Cabeçalho do Perfil */}
<section className="relative overflow-hidden rounded-[3rem] obsidian-gradient text-white p-8 md:p-12 shadow-2xl border border-white/5">
<div className="absolute top-0 right-0 w-64 h-64 bg-amber-500/10 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2" />
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/10 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2" />
<div className="relative z-10 flex flex-col md:flex-row items-center gap-8 md:text-left text-center">
<div className="relative group">
<div className="absolute inset-0 bg-amber-500 blur-2xl opacity-20 group-hover:opacity-40 transition-opacity" />
<div className="w-24 h-24 bg-white/10 backdrop-blur-xl border-2 border-white/20 rounded-[2rem] flex items-center justify-center text-amber-500 shadow-2xl relative z-10 transition-transform duration-500 hover:rotate-6">
<div className="absolute inset-0 bg-indigo-500 blur-2xl opacity-20 group-hover:opacity-40 transition-opacity" />
<div className="w-24 h-24 bg-white/10 backdrop-blur-xl border-2 border-white/20 rounded-[2rem] flex items-center justify-center text-indigo-400 shadow-2xl relative z-10 transition-transform duration-500 hover:rotate-6">
<User size={48} />
</div>
</div>
<div className="space-y-3">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/5 border border-white/10 rounded-full text-[10px] font-black uppercase tracking-[0.2em] text-amber-500">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/5 border border-white/10 rounded-full text-[10px] font-black uppercase tracking-[0.2em] text-indigo-400">
<Star size={12} fill="currentColor" />
<span>Membro de Elite</span>
<span>Utilizador Registado</span>
</div>
<h1 className="text-4xl md:text-5xl font-black tracking-tighter uppercase italic leading-[0.9]">
{displayName}
@@ -169,7 +169,7 @@ export default function Profile() {
<Link key={shop.id} to={`/barbearia/${shop.id}`}>
<Card className="p-2 border-none glass-card rounded-[2rem] shadow-lg shadow-slate-200/50 hover:-translate-y-1 transition-all duration-300 group">
<div className="flex items-center gap-4 p-4">
<div className="w-16 h-16 rounded-2xl overflow-hidden border-2 border-slate-50 shadow-inner group-hover:border-amber-200 transition-colors">
<div className="w-16 h-16 rounded-2xl overflow-hidden border-2 border-slate-50 shadow-inner group-hover:border-indigo-200 transition-colors">
{shop.imageUrl ? (
<img src={shop.imageUrl} alt={shop.name} className="w-full h-full object-cover" />
) : (
@@ -179,10 +179,10 @@ export default function Profile() {
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-black text-slate-900 uppercase italic tracking-tight group-hover:text-amber-600 transition-colors truncate">{shop.name}</p>
<p className="font-black text-slate-900 uppercase italic tracking-tight group-hover:text-indigo-600 transition-colors truncate">{shop.name}</p>
<div className="flex items-center gap-3 mt-1">
<div className="flex items-center gap-1 text-[10px] font-black text-amber-600 uppercase tracking-widest">
<Star size={10} className="fill-amber-500" />
<div className="flex items-center gap-1 text-[10px] font-black text-indigo-600 uppercase tracking-widest">
<Star size={10} className="fill-indigo-500" />
{shop.rating.toFixed(1)}
</div>
<div className="flex items-center gap-1 text-[10px] font-black text-slate-400 uppercase tracking-widest truncate">
@@ -204,7 +204,7 @@ export default function Profile() {
<section className="lg:col-span-3 space-y-6">
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-2 text-slate-900">
<Calendar size={16} className="text-amber-600" />
<Calendar size={16} className="text-indigo-600" />
<h2 className="text-sm font-black uppercase tracking-[0.3em]">Minha Agenda</h2>
</div>
</div>
@@ -214,7 +214,7 @@ export default function Profile() {
<Calendar size={64} className="mx-auto text-slate-100 mb-6" />
<h3 className="text-xl font-black text-slate-900 uppercase italic tracking-tight">Sem Reservas</h3>
<p className="text-slate-400 font-medium italic mt-2">Sua jornada de estilo ainda não começou.</p>
<Button asChild className="mt-8 h-12 px-8 bg-slate-900 text-amber-500 font-black rounded-2xl uppercase tracking-widest text-[10px] italic">
<Button asChild className="mt-8 h-12 px-8 bg-slate-900 text-indigo-400 font-black rounded-2xl uppercase tracking-widest text-[10px] italic">
<Link to="/explorar">Agendar Agora</Link>
</Button>
</Card>
@@ -246,7 +246,7 @@ export default function Profile() {
<div className="flex items-center justify-between pt-4 border-t border-slate-50">
{service && (
<div className="flex items-center gap-2 text-[10px] font-black text-slate-500 uppercase tracking-widest">
<Clock size={12} className="text-amber-600" />
<Clock size={12} className="text-indigo-600" />
{service.name} · {service.duration} MIN
</div>
)}
@@ -254,10 +254,10 @@ export default function Profile() {
{canReview && (
<button
onClick={() => setReviewTarget({ appointmentId: a.id, shopId: a.shopId, shopName: shop?.name ?? 'Barbearia' })}
className="flex items-center gap-2 px-4 py-2 bg-amber-500 hover:bg-slate-900 text-white hover:text-amber-500 rounded-xl transition-all duration-300 transform active:scale-95 shadow-lg shadow-amber-500/20"
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-slate-900 text-white hover:text-indigo-400 rounded-xl transition-all duration-300 transform active:scale-95 shadow-lg shadow-indigo-500/20"
>
<Star size={12} className="fill-current" />
<span className="text-[10px] font-black uppercase tracking-widest">Avaliar Experiência</span>
<span className="text-[10px] font-black uppercase tracking-widest">Avaliar Atendimento</span>
</button>
)}
@@ -280,7 +280,7 @@ export default function Profile() {
<section className="lg:col-span-2 space-y-6">
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-2 text-slate-900">
<ShoppingBag size={16} className="text-amber-600" />
<ShoppingBag size={16} className="text-indigo-600" />
<h2 className="text-sm font-black uppercase tracking-[0.3em]">Pedidos</h2>
</div>
</div>
@@ -298,7 +298,7 @@ export default function Profile() {
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-black text-slate-900 uppercase italic tracking-tight truncate max-w-[120px]">{shop?.name}</h3>
<div className="text-lg font-black text-amber-600 tracking-tighter">{currency(o.total)}</div>
<div className="text-lg font-black text-indigo-600 tracking-tighter">{currency(o.total)}</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-50">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">

View File

@@ -77,12 +77,12 @@ export default function ShopDetails() {
<div className="absolute bottom-10 left-10 space-y-3">
<div className="flex items-center gap-2 bg-slate-900/40 backdrop-blur-md border border-white/20 w-fit px-3 py-1 rounded-full">
<Star size={14} className="fill-amber-500 text-amber-500" />
<Star size={14} className="fill-indigo-500 text-indigo-500" />
<span className="text-white text-xs font-black tracking-widest">{(shop.rating || 0).toFixed(1)} EXCELENTE</span>
</div>
<h1 className="text-4xl md:text-5xl font-black text-white tracking-tighter">{shop.name}</h1>
<div className="flex items-center gap-2 text-white/90">
<MapPin size={16} className="text-amber-500" />
<MapPin size={16} className="text-indigo-600" />
<p className="text-base font-medium">{shop.address}</p>
</div>
</div>