feat: implement financial recurrence, privacy settings, and premium DateTimePicker overhaul

This commit is contained in:
Erik Silva
2026-01-25 20:17:36 -03:00
parent 416bd83ea7
commit e4935941fc
43 changed files with 3238 additions and 765 deletions

View File

@@ -3,9 +3,11 @@
import { useState } from 'react'
import { createFinancialEvent } from '@/actions/finance'
import type { FinancialEventType } from '@prisma/client'
import { Loader2, Plus, Users, Calculator } from 'lucide-react'
import { Loader2, Plus, Users, Calculator, Calendar, AlertCircle } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { DateTimePicker } from '@/components/DateTimePicker'
interface CreateFinanceEventModalProps {
isOpen: boolean
onClose: () => void
@@ -20,7 +22,8 @@ export function CreateFinanceEventModal({ isOpen, onClose, players }: CreateFina
// Form State
const [type, setType] = useState<FinancialEventType>('MONTHLY_FEE')
const [title, setTitle] = useState('')
const [dueDate, setDueDate] = useState('')
const [description, setDescription] = useState('')
const [dueDate, setDueDate] = useState(() => new Date().toISOString().split('T')[0])
const [priceMode, setPriceMode] = useState<'FIXED' | 'TOTAL'>('FIXED') // Fixed per person or Total to split
const [amount, setAmount] = useState('')
const [isRecurring, setIsRecurring] = useState(false)
@@ -37,6 +40,7 @@ export function CreateFinanceEventModal({ isOpen, onClose, players }: CreateFina
const result = await createFinancialEvent({
title: title || (type === 'MONTHLY_FEE' ? 'Mensalidade' : 'Evento'),
description: description,
type,
dueDate,
selectedPlayerIds: selectedPlayers,
@@ -106,47 +110,61 @@ export function CreateFinanceEventModal({ isOpen, onClose, players }: CreateFina
</div>
<div className="ui-form-field">
<label className="text-label ml-1">Vencimento</label>
<input
type="date"
value={dueDate}
onChange={e => setDueDate(e.target.value)}
className="ui-input w-full [color-scheme:dark]"
<label className="text-label ml-1">Descrição (Opcional)</label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Ex: Pagamento da quadra do mês"
className="ui-input w-full min-h-[80px] py-3 resize-none"
/>
</div>
{type === 'MONTHLY_FEE' && (
<div className="bg-surface/50 border border-border p-4 rounded-xl space-y-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={isRecurring}
onChange={(e) => setIsRecurring(e.target.checked)}
className="w-4 h-4 rounded border-border text-primary bg-background accent-primary"
id="recurring-check"
/>
<label htmlFor="recurring-check" className="text-sm font-medium cursor-pointer select-none">
Repetir mensalmente
</label>
</div>
<DateTimePicker
label="Vencimento"
value={dueDate}
onChange={setDueDate}
mode="date"
/>
{isRecurring && (
<div className="pl-7 animate-in fade-in slide-in-from-top-2">
<label className="text-label ml-0.5 mb-1.5 block">Repetir até</label>
<input
type="date"
value={recurrenceEndDate}
onChange={(e) => setRecurrenceEndDate(e.target.value)}
className="ui-input w-full h-10 text-sm [color-scheme:dark]"
min={dueDate}
/>
<p className="text-[10px] text-muted mt-1.5">
Serão criados eventos para cada mês até a data limite.
</p>
<div className="bg-surface/50 border border-border p-4 rounded-xl space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isRecurring ? 'bg-primary/20 text-primary' : 'bg-surface-raised text-muted'}`}>
<Calendar className="w-4 h-4" />
</div>
)}
<div className="space-y-0.5">
<label htmlFor="recurring-check" className="text-sm font-bold cursor-pointer select-none">
Repetir Evento
</label>
<p className="text-[10px] text-muted">Criar cópias mensais automaticamente</p>
</div>
</div>
<button
type="button"
onClick={() => setIsRecurring(!isRecurring)}
className={`w-10 h-5 rounded-full transition-colors relative ${isRecurring ? 'bg-primary' : 'bg-zinc-700'}`}
>
<div className={`absolute top-1 w-3 h-3 bg-white rounded-full transition-all ${isRecurring ? 'right-1' : 'left-1'}`} />
</button>
</div>
)}
{isRecurring && (
<div className="pt-2 border-t border-border/50 animate-in fade-in slide-in-from-top-2">
<DateTimePicker
label="Até quando repetir?"
value={recurrenceEndDate}
onChange={setRecurrenceEndDate}
placeholder="Data final da recorrência"
mode="date"
/>
<p className="text-[10px] text-orange-400 font-medium mt-2 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
O sistema parará de criar eventos após esta data.
</p>
</div>
)}
</div>
<div className="space-y-2">
<label className="text-label ml-1">Valor</label>

View File

@@ -0,0 +1,349 @@
'use client'
import React, { useState, useMemo, useEffect, useRef } from 'react'
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, Clock, X, Check } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { clsx } from 'clsx'
interface DateTimePickerProps {
value: string // ISO string or datetime-local format
onChange: (value: string) => void
label?: string
placeholder?: string
required?: boolean
mode?: 'date' | 'datetime'
}
export function DateTimePicker({
value,
onChange,
label,
placeholder,
required,
mode = 'datetime'
}: DateTimePickerProps) {
const [isOpen, setIsOpen] = useState(false)
const [mounted, setMounted] = useState(false)
// Better initialization to avoid timezone issues with YYYY-MM-DD
const parseValue = (val: string) => {
if (!val) return new Date()
if (val.length === 10) { // YYYY-MM-DD
const [year, month, day] = val.split('-').map(Number)
return new Date(year, month - 1, day, 12, 0, 0)
}
return new Date(val)
}
const [viewDate, setViewDate] = useState(() => parseValue(value))
const containerRef = useRef<HTMLDivElement>(null)
const defaultPlaceholder = mode === 'date' ? "Selecione a data" : "Selecione data e hora"
const currentPlaceholder = placeholder || defaultPlaceholder
useEffect(() => {
setMounted(true)
}, [])
// Sync viewDate when value changes from outside
useEffect(() => {
if (value) {
setViewDate(parseValue(value))
}
}, [value])
// Close when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const selectedDate = value ? new Date(value) : null
const daysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate()
const firstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay()
const calendarDays = useMemo(() => {
const year = viewDate.getFullYear()
const month = viewDate.getMonth()
const days = []
const prevMonthDays = daysInMonth(year, month - 1)
const startDay = firstDayOfMonth(year, month)
// Previous month days
for (let i = startDay - 1; i >= 0; i--) {
days.push({
day: prevMonthDays - i,
month: month - 1,
year: month === 0 ? year - 1 : year,
currentMonth: false
})
}
// Current month days
const currentMonthDays = daysInMonth(year, month)
for (let i = 1; i <= currentMonthDays; i++) {
days.push({
day: i,
month,
year,
currentMonth: true
})
}
// Next month days
const remaining = 42 - days.length
for (let i = 1; i <= remaining; i++) {
days.push({
day: i,
month: month + 1,
year: month === 11 ? year + 1 : year,
currentMonth: false
})
}
return days
}, [viewDate])
const handleDateSelect = (day: number, month: number, year: number) => {
const newDate = parseValue(value)
newDate.setFullYear(year)
newDate.setMonth(month)
newDate.setDate(day)
if (mode === 'date') {
onChange(newDate.toISOString().split('T')[0])
setIsOpen(false)
} else {
// If it's the first time selecting in datetime mode, set a default time
if (!value) {
newDate.setHours(19, 0, 0, 0)
}
onChange(newDate.toISOString())
}
}
const handleTimeChange = (hours: number, minutes: number) => {
const newDate = parseValue(value)
newDate.setHours(hours)
newDate.setMinutes(minutes)
newDate.setSeconds(0)
newDate.setMilliseconds(0)
onChange(newDate.toISOString())
}
const formatDisplay = (val: string) => {
if (!val) return ''
const d = parseValue(val)
return d.toLocaleString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
...(mode === 'datetime' ? {
hour: '2-digit',
minute: '2-digit'
} : {})
})
}
const months = [
'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho',
'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'
]
const nextMonth = () => {
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1))
}
const prevMonth = () => {
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1))
}
const hours = Array.from({ length: 24 }, (_, i) => i)
const minutes = Array.from({ length: 12 }, (_, i) => i * 5)
return (
<div className="ui-form-field" ref={containerRef}>
{label && <label className="text-label ml-1">{label}</label>}
<div className="relative">
<div
onClick={() => setIsOpen(!isOpen)}
className={clsx(
"ui-input w-full h-12 flex items-center justify-between cursor-pointer transition-all",
isOpen ? "border-primary ring-2 ring-primary/20 shadow-lg shadow-primary/10" : "bg-surface"
)}
>
<div className="flex items-center gap-3">
<CalendarIcon className={clsx("w-4 h-4 transition-colors", isOpen ? "text-primary" : "text-muted")} />
<span className={clsx("text-sm transition-colors", !value && "text-muted")}>
{!mounted ? currentPlaceholder : (value ? formatDisplay(value) : currentPlaceholder)}
</span>
</div>
</div>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className={clsx(
"absolute top-full left-0 mt-2 z-[100] bg-surface-raised border border-border shadow-2xl rounded-2xl overflow-hidden flex flex-col sm:flex-row",
mode === 'date' ? "w-[310px]" : "w-[320px] sm:w-[500px]"
)}
>
{/* Calendar Section */}
<div className={clsx(
"p-6 flex-1 bg-surface-raised/40 backdrop-blur-xl",
mode === 'datetime' && "border-b sm:border-b-0 sm:border-r border-white/5"
)}>
<div className="flex items-center justify-between mb-6">
<div className="space-y-0.5">
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-primary/80">
Selecione a Data
</h4>
<p className="text-lg font-black uppercase italic tracking-tighter leading-none">
{months[viewDate.getMonth()]} <span className="text-muted/40">{viewDate.getFullYear()}</span>
</p>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={(e) => { e.stopPropagation(); prevMonth(); }}
className="p-2 bg-white/5 hover:bg-white/10 border border-white/5 rounded-xl transition-all text-muted hover:text-primary active:scale-90"
>
<ChevronLeft className="w-4 h-4" />
</button>
<button
type="button"
onClick={(e) => { e.stopPropagation(); nextMonth(); }}
className="p-2 bg-white/5 hover:bg-white/10 border border-white/5 rounded-xl transition-all text-muted hover:text-primary active:scale-90"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-2 mb-2">
{['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'].map((d, i) => (
<div key={i} className="text-[9px] font-black text-muted/30 text-center uppercase tracking-widest py-2">
{d}
</div>
))}
</div>
<div className="grid grid-cols-7 gap-2">
{calendarDays.map((d, i) => {
const isSelected = selectedDate &&
selectedDate.getDate() === d.day &&
selectedDate.getMonth() === d.month &&
selectedDate.getFullYear() === d.year
const isToday = new Date().getDate() === d.day &&
new Date().getMonth() === d.month &&
new Date().getFullYear() === d.year
return (
<button
key={i}
type="button"
onClick={() => handleDateSelect(d.day, d.month, d.year)}
className={clsx(
"w-full aspect-square text-xs font-black rounded-xl transition-all flex items-center justify-center relative border overflow-hidden group",
d.currentMonth ? "text-foreground" : "text-muted/10",
isSelected
? "bg-primary text-black border-primary shadow-[0_0_15px_rgba(var(--primary-rgb),0.3)] scale-105 z-10"
: "bg-white/[0.02] border-white/5 hover:border-primary/40 hover:bg-primary/5 hover:text-primary",
!isSelected && isToday && "border-primary/20 after:content-[''] after:absolute after:bottom-1.5 after:w-1 after:h-1 after:bg-primary after:rounded-full"
)}
>
<span className="relative z-10">{d.day}</span>
{isSelected && (
<motion.div
layoutId="activeDay"
className="absolute inset-0 bg-primary"
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
</button>
)
})}
</div>
</div>
{/* Time Section */}
{mode === 'datetime' && (
<div className="bg-black/40 backdrop-blur-2xl p-6 w-full sm:w-56 flex flex-col font-sans">
<div className="space-y-1 mb-6">
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-primary/80">Ajuste de</h4>
<p className="text-xl font-black uppercase italic tracking-tighter leading-none">Horário</p>
</div>
<div className="flex gap-4 flex-1">
{/* Hours */}
<div className="flex-1 flex flex-col">
<span className="text-[9px] font-black uppercase text-muted/30 mb-3 tracking-widest text-center">Hora</span>
<div className="h-[220px] overflow-y-auto w-full space-y-2 pr-2 custom-scrollbar">
{hours.map(h => (
<button
key={h}
type="button"
onClick={() => handleTimeChange(h, selectedDate?.getMinutes() || 0)}
className={clsx(
"w-full py-2.5 text-sm font-black rounded-xl transition-all border",
selectedDate?.getHours() === h
? "bg-primary/20 text-primary border-primary/40 shadow-[0_0_10px_rgba(var(--primary-rgb),0.1)]"
: "bg-white/[0.02] border-white/5 hover:bg-white/5 text-muted/60 hover:text-foreground"
)}
>
{h.toString().padStart(2, '0')}
</button>
))}
</div>
</div>
{/* Minutes */}
<div className="flex-1 flex flex-col">
<span className="text-[9px] font-black uppercase text-muted/30 mb-3 tracking-widest text-center">Min</span>
<div className="h-[220px] overflow-y-auto w-full space-y-2 pr-2 custom-scrollbar">
{minutes.map(m => (
<button
key={m}
type="button"
onClick={() => handleTimeChange(selectedDate?.getHours() || 0, m)}
className={clsx(
"w-full py-2.5 text-sm font-black rounded-xl transition-all border",
selectedDate?.getMinutes() === m
? "bg-primary/20 text-primary border-primary/40 shadow-[0_0_10px_rgba(var(--primary-rgb),0.1)]"
: "bg-white/[0.02] border-white/5 hover:bg-white/5 text-muted/60 hover:text-foreground"
)}
>
{m.toString().padStart(2, '0')}
</button>
))}
</div>
</div>
</div>
<button
type="button"
onClick={() => setIsOpen(false)}
className="mt-6 w-full h-11 bg-white text-black font-black uppercase text-[10px] tracking-widest rounded-xl hover:scale-[1.02] active:scale-[0.98] transition-all shadow-xl shadow-black/20"
>
Confirmar
</button>
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
)
}

View File

@@ -1,9 +1,10 @@
'use client'
import React, { useState, useMemo } from 'react'
import { Plus, Wallet, TrendingUp, Calendar, AlertCircle, ChevronRight, Check, X, Search, Filter, LayoutGrid, List, Trash2, MoreHorizontal, Share2, Copy } from 'lucide-react'
import { Plus, Wallet, TrendingUp, Calendar, AlertCircle, ChevronRight, Check, X, Search, Filter, LayoutGrid, List, Trash2, MoreHorizontal, Share2, Copy, Settings, Eye, EyeOff } from 'lucide-react'
import { CreateFinanceEventModal } from '@/components/CreateFinanceEventModal'
import { markPaymentAsPaid, markPaymentAsPending, deleteFinancialEvents } from '@/actions/finance'
import { FinancialSettingsModal } from '@/components/FinancialSettingsModal'
import { markPaymentAsPaid, markPaymentAsPending, deleteFinancialEvents, toggleEventPrivacy } from '@/actions/finance'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { motion, AnimatePresence } from 'framer-motion'
@@ -13,12 +14,32 @@ import { DeleteConfirmationModal } from '@/components/DeleteConfirmationModal'
interface FinancialPageProps {
events: any[]
players: any[]
group: any
}
export function FinancialDashboard({ events, players }: FinancialPageProps) {
export function FinancialDashboard({ events, players, group }: FinancialPageProps) {
const router = useRouter()
const [mounted, setMounted] = React.useState(false)
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
// Helper to format date safely without hydration mismatch
const formatDate = (dateInput: string | Date) => {
const d = new Date(dateInput)
// Since dueDate is saved at 00:00:00 UTC (from YYYY-MM-DD string),
// we use UTC methods to ensure the same date is shown everywhere.
const day = String(d.getUTCDate()).padStart(2, '0')
const month = String(d.getUTCMonth() + 1).padStart(2, '0')
const year = d.getUTCFullYear()
return `${day}/${month}/${year}`
}
const [expandedEventId, setExpandedEventId] = useState<string | null>(null)
const [privacyPendingId, setPrivacyPendingId] = useState<string | null>(null)
// Filter & View State
const [searchQuery, setSearchQuery] = useState('')
@@ -182,10 +203,20 @@ export function FinancialDashboard({ events, players }: FinancialPageProps) {
</button>
</div>
<button onClick={() => setIsCreateModalOpen(true)} className="ui-button w-full sm:w-auto shadow-lg shadow-primary/20 h-10">
<Plus className="w-4 h-4 mr-2" />
Novo Evento
</button>
<div className="flex items-center gap-2 w-full sm:w-auto">
<button
onClick={() => setIsSettingsModalOpen(true)}
className="p-2.5 bg-surface-raised border border-border rounded-xl text-muted hover:text-primary hover:border-primary/30 transition-all"
title="Configurações de Privacidade"
>
<Settings className="w-5 h-5" />
</button>
<button onClick={() => setIsCreateModalOpen(true)} className="ui-button flex-1 sm:flex-initial shadow-lg shadow-primary/20 h-10 px-6">
<Plus className="w-4 h-4 mr-2" />
Novo Evento
</button>
</div>
</>
)}
</div>
@@ -300,7 +331,7 @@ export function FinancialDashboard({ events, players }: FinancialPageProps) {
{event.type === 'MONTHLY_FEE' ? 'Mensalidade' : 'Evento'}
</span>
<span className="text-[10px] text-muted font-bold">
{new Date(event.dueDate).toLocaleDateString('pt-BR')}
{mounted ? formatDate(event.dueDate) : '--/--/----'}
</span>
</div>
<h3 className="font-bold text-base leading-tight group-hover:text-primary transition-colors">{event.title}</h3>
@@ -342,7 +373,7 @@ export function FinancialDashboard({ events, players }: FinancialPageProps) {
const paid = event.payments.filter((p: any) => p.status === 'PAID').map((p: any) => p.player.name)
const pending = event.payments.filter((p: any) => p.status !== 'PAID').map((p: any) => p.player.name)
const message = `*${event.title.toUpperCase()}*\nVencimento: ${new Date(event.dueDate).toLocaleDateString('pt-BR')}\n\n` +
const message = `*${event.title.toUpperCase()}*\nVencimento: ${formatDate(event.dueDate)}\n\n` +
`✅ *PAGOS (${paid.length})*:\n${paid.join(', ')}\n\n` +
`⏳ *PENDENTES (${pending.length})*:\n${pending.join(', ')}\n\n` +
`💰 *Total Arrecadado*: R$ ${event.stats.totalPaid.toFixed(2)}\n` +
@@ -370,44 +401,82 @@ export function FinancialDashboard({ events, players }: FinancialPageProps) {
</div>
</div>
<div>
<div className="flex items-center justify-between mb-3">
<h4 className="text-xs font-bold uppercase tracking-widest text-muted">Gerenciar Pagamentos</h4>
<Link
href={`/financial-report/${event.id}`}
className="text-[10px] font-bold text-primary hover:underline flex items-center gap-1"
target="_blank"
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-xs font-bold uppercase tracking-widest text-muted">Privacidade</h4>
</div>
<button
onClick={async (e) => {
e.stopPropagation();
setPrivacyPendingId(event.id);
await toggleEventPrivacy(event.id, !event.showTotalInPublic);
setPrivacyPendingId(null);
router.refresh();
}}
className={clsx(
"flex items-center justify-between w-full p-3 rounded-xl border transition-all",
event.showTotalInPublic
? "bg-primary/5 border-primary/20 text-primary"
: "bg-surface border-border text-muted"
)}
>
Ver Página Pública <ChevronRight className="w-3 h-3" />
</Link>
<div className="flex items-center gap-3">
{event.showTotalInPublic ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
<span className="text-xs font-bold uppercase tracking-wider">
{event.showTotalInPublic ? 'Total Visível' : 'Total Oculto'}
</span>
</div>
<div className={`w-8 h-4 rounded-full relative transition-colors ${event.showTotalInPublic ? 'bg-primary' : 'bg-zinc-700'}`}>
<div className={`absolute top-0.5 w-3 h-3 bg-white rounded-full transition-all ${event.showTotalInPublic ? 'right-0.5' : 'left-0.5'}`} />
</div>
</button>
<p className="text-[9px] text-muted italic">
{event.showTotalInPublic
? "* Atletas conseguem ver a meta e o total arrecadado no link público."
: "* Atletas verão apenas a lista de pagantes, sem valores totais."}
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
{event.payments.map((payment: any) => (
<div key={payment.id} className="flex items-center justify-between p-2 rounded-lg bg-surface border border-border/50 group/item hover:border-primary/20 transition-colors">
<div className="flex items-center gap-3">
<div className={clsx("w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold transition-colors",
payment.status === 'PAID' ? 'bg-green-500 text-black' : 'bg-surface-raised text-muted group-hover/item:bg-surface-raised/80'
)}>
{payment.status === 'PAID' ? <Check className="w-3 h-3" /> : payment.player?.name.substring(0, 1).toUpperCase()}
</div>
<span className={clsx("text-xs font-medium truncate max-w-[100px]", payment.status === 'PAID' ? 'text-foreground' : 'text-muted')}>
{payment.player?.name}
</span>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-xs font-bold uppercase tracking-widest text-muted">Gerenciar Pagamentos</h4>
<Link
href={`/financial-report/${event.id}`}
className="text-[10px] font-bold text-primary hover:underline flex items-center gap-1"
target="_blank"
>
Ver Página Pública <ChevronRight className="w-3 h-3" />
</Link>
</div>
<button
onClick={() => togglePayment(payment.id, payment.status)}
className={clsx("text-[10px] font-bold px-2 py-1 rounded transition-colors",
payment.status === 'PENDING'
? "bg-primary/10 text-primary hover:bg-primary hover:text-background"
: "text-muted hover:text-red-500 hover:bg-red-500/10"
)}
>
{payment.status === 'PENDING' ? 'RECEBER' : 'DESFAZER'}
</button>
</div>
))}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{event.payments.map((payment: any) => (
<div key={payment.id} className="flex items-center justify-between p-2 rounded-lg bg-surface border border-border/50 group/item hover:border-primary/20 transition-colors">
<div className="flex items-center gap-3">
<div className={clsx("w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold transition-colors",
payment.status === 'PAID' ? 'bg-green-500 text-black' : 'bg-surface-raised text-muted group-hover/item:bg-surface-raised/80'
)}>
{payment.status === 'PAID' ? <Check className="w-3 h-3" /> : payment.player?.name.substring(0, 1).toUpperCase()}
</div>
<span className={clsx("text-xs font-medium truncate max-w-[100px]", payment.status === 'PAID' ? 'text-foreground' : 'text-muted')}>
{payment.player?.name}
</span>
</div>
<button
onClick={() => togglePayment(payment.id, payment.status)}
className={clsx("text-[10px] font-bold px-2 py-1 rounded transition-colors",
payment.status === 'PENDING'
? "bg-primary/10 text-primary hover:bg-primary hover:text-background"
: "text-muted hover:text-red-500 hover:bg-red-500/10"
)}
>
{payment.status === 'PENDING' ? 'RECEBER' : 'DESFAZER'}
</button>
</div>
))}
</div>
</div>
</div>
</div>
@@ -439,6 +508,12 @@ export function FinancialDashboard({ events, players }: FinancialPageProps) {
players={players}
/>
<FinancialSettingsModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
group={group}
/>
<DeleteConfirmationModal
isOpen={deleteModal.isOpen}
onClose={() => setDeleteModal(prev => ({ ...prev, isOpen: false }))}

View File

@@ -0,0 +1,102 @@
'use client'
import React, { useState } from 'react'
import { X, Shield, ShieldOff, Eye, EyeOff, Loader2 } from 'lucide-react'
import { updateFinancialSettings } from '@/actions/finance'
import { useRouter } from 'next/navigation'
import { motion, AnimatePresence } from 'framer-motion'
interface FinancialSettingsModalProps {
isOpen: boolean
onClose: () => void
group: any
}
export function FinancialSettingsModal({ isOpen, onClose, group }: FinancialSettingsModalProps) {
const router = useRouter()
const [isPending, setIsPending] = useState(false)
const [showTotal, setShowTotal] = useState(group.showTotalInPublic ?? true)
if (!isOpen) return null
const handleSave = async () => {
setIsPending(true)
try {
await updateFinancialSettings({ showTotalInPublic: showTotal })
onClose()
router.refresh()
} catch (error) {
console.error(error)
} finally {
setIsPending(false)
}
}
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="bg-surface border border-border rounded-2xl w-full max-w-md shadow-2xl overflow-hidden"
>
<div className="p-6 border-b border-border flex items-center justify-between bg-surface-raised/50">
<div>
<h3 className="text-lg font-black uppercase italic tracking-tighter text-primary">Configurações Financeiras</h3>
<p className="text-[10px] text-muted font-bold uppercase tracking-widest mt-1">Privacidade e Padrões</p>
</div>
<button onClick={onClose} className="p-2 hover:bg-white/5 rounded-full transition-colors">
<X className="w-5 h-5 text-muted" />
</button>
</div>
<div className="p-6 space-y-6">
<div className="space-y-4">
<div className="flex items-start gap-4 p-4 rounded-xl border border-white/5 bg-white/[0.02]">
<div className={`p-2 rounded-lg ${showTotal ? 'bg-primary/10 text-primary' : 'bg-red-500/10 text-red-500'}`}>
{showTotal ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />}
</div>
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold">Mostrar Total Arrecadado</h4>
<button
onClick={() => setShowTotal(!showTotal)}
className={`w-10 h-5 rounded-full transition-colors relative ${showTotal ? 'bg-primary' : 'bg-zinc-700'}`}
>
<div className={`absolute top-1 w-3 h-3 bg-white rounded-full transition-all ${showTotal ? 'right-1' : 'left-1'}`} />
</button>
</div>
<p className="text-[10px] text-muted leading-relaxed">
Define se os links públicos devem mostrar o valor total arrecadado e a meta.
Quando desativado, os atletas verão apenas a lista de quem pagou.
</p>
</div>
</div>
<div className="p-4 rounded-xl bg-orange-500/5 border border-orange-500/20">
<p className="text-[10px] text-orange-500 font-bold leading-relaxed">
* Esta configuração será aplicada como padrão para todos os NOVOS eventos criados.
Eventos existentes podem ser alterados individualmente.
</p>
</div>
</div>
</div>
<div className="p-6 border-t border-border flex justify-end gap-3 bg-surface-raised/30">
<button
onClick={onClose}
className="px-4 py-2 text-xs font-bold text-muted hover:text-foreground transition-colors"
>
Cancelar
</button>
<button
onClick={handleSave}
disabled={isPending}
className="ui-button px-6 h-10 text-xs font-black uppercase tracking-widest bg-white text-black shadow-md hover:scale-[1.02] active:scale-[0.98] transition-all"
>
{isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Salvar Alterações'}
</button>
</div>
</motion.div>
</div>
)
}

View File

@@ -1,19 +1,21 @@
'use client'
import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Plus, Users, Calendar, Shuffle, Download, Trophy, Save, Check, Star, RefreshCw } from 'lucide-react'
import React, { useState, useEffect, useRef } from 'react'
import { motion } from 'framer-motion'
import { Shuffle, Download, Check, Star, RefreshCw, ChevronDown, Trophy, Zap, Shield } from 'lucide-react'
import { createMatch, updateMatchStatus } from '@/actions/match'
import { getMatchWithAttendance } from '@/actions/attendance'
import { toPng } from 'html-to-image'
import { renderMatchCard } from '@/utils/MatchCardCanvas'
import { clsx } from 'clsx'
import { useSearchParams } from 'next/navigation'
import { DateTimePicker } from '@/components/DateTimePicker'
import type { Arena } from '@prisma/client'
interface MatchFlowProps {
group: any
arenas?: Arena[]
sponsors?: any[]
}
const getInitials = (name: string) => {
@@ -26,7 +28,6 @@ const getInitials = (name: string) => {
.slice(0, 2)
}
// Simple seed-based random generator for transparency
const seededRandom = (seed: string) => {
let hash = 0
for (let i = 0; i < seed.length; i++) {
@@ -39,7 +40,79 @@ const seededRandom = (seed: string) => {
}
}
export function MatchFlow({ group, arenas = [] }: MatchFlowProps) {
// Sub-component for individual Canvas Preview
const CanvasTeamPreview = ({ team, group, date, location, drawSeed, sponsors, options, onDownload }: any) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [rendering, setRendering] = useState(true);
useEffect(() => {
const draw = async () => {
if (canvasRef.current) {
setRendering(true);
const dateObj = new Date(date);
const day = dateObj.toLocaleDateString('pt-BR', { day: '2-digit' });
const month = dateObj.toLocaleDateString('pt-BR', { month: 'short' }).replace('.', '');
const time = dateObj.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
await renderMatchCard(canvasRef.current, {
groupName: group.name,
logoUrl: group.logoUrl,
teamName: team.name,
teamColor: team.color,
day: day,
month: month,
time: time,
location: location || 'ARENA TEMFUT',
players: team.players,
sponsors: sponsors,
drawSeed: drawSeed,
options: options
});
setRendering(false);
}
};
draw();
}, [team, group, date, location, drawSeed, sponsors, options]);
return (
<div className="flex flex-col items-center w-full max-w-[460px] group">
<div className="w-full aspect-[9/16] bg-black rounded-[2.5rem] border border-white/10 shadow-2xl overflow-hidden relative">
{rendering && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/80 z-20 backdrop-blur-sm">
<RefreshCw className="w-8 h-8 text-primary animate-spin mb-4" />
<span className="text-[10px] font-black uppercase tracking-widest text-white/40">Renderizando Pixel Perfect...</span>
</div>
)}
<canvas
ref={canvasRef}
id={`canvas-${team.name.toLowerCase().replace(/\s+/g, '-')}`}
className="w-full h-full object-cover"
/>
</div>
<button
onClick={() => {
if (canvasRef.current) {
canvasRef.current.toBlob((blob) => {
if (blob) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = `temfut-capa-${team.name.toLowerCase().replace(/\s+/g, '-')}.png`;
link.href = url;
link.click();
URL.revokeObjectURL(url);
}
}, 'image/png', 1.0);
}
}}
className="mt-6 ui-button w-full h-11 text-[10px] font-black uppercase tracking-[0.3em] shadow-md bg-white border-white/10 text-black hover:scale-[1.02] transition-all group-hover:shadow-primary/20"
>
<Download className="w-4 h-4 mr-2" /> BAIXAR CAPA HD
</button>
</div>
);
};
export function MatchFlow({ group, arenas = [], sponsors = [] }: MatchFlowProps) {
const searchParams = useSearchParams()
const scheduledMatchId = searchParams.get('id')
@@ -50,17 +123,31 @@ export function MatchFlow({ group, arenas = [] }: MatchFlowProps) {
const [teamCount, setTeamCount] = useState(2)
const [selectedPlayers, setSelectedPlayers] = useState<string[]>([])
const [teams, setTeams] = useState<any[]>([])
const [matchDate, setMatchDate] = useState(new Date().toISOString().split('T')[0])
const [matchDate, setMatchDate] = useState(() => {
const now = new Date()
const offset = now.getTimezoneOffset() * 60000
return new Date(now.getTime() - offset).toISOString().split('T')[0]
})
const [isSaving, setIsSaving] = useState(false)
const [drawSeed, setDrawSeed] = useState('')
const [location, setLocation] = useState('')
const [selectedArenaId, setSelectedArenaId] = useState('')
const [mounted, setMounted] = useState(false)
const [cardOptions, setCardOptions] = useState({
showNumbers: true,
showPositions: true,
showStars: true,
showSponsors: true,
})
useEffect(() => {
setMounted(true)
}, [])
useEffect(() => {
if (scheduledMatchId) {
loadScheduledData(scheduledMatchId)
}
// Generate a random seed on mount
generateNewSeed()
}, [scheduledMatchId])
@@ -78,7 +165,7 @@ export function MatchFlow({ group, arenas = [] }: MatchFlowProps) {
setActiveMatchId(data.id)
}
} catch (error) {
console.error('Erro ao carregar dados agendados:', error)
console.error('Erro ao lidar com dados agendados:', error)
}
}
@@ -96,9 +183,7 @@ export function MatchFlow({ group, arenas = [] }: MatchFlowProps) {
const playersToDraw = group.players.filter((p: any) => selectedPlayers.includes(p.id))
if (playersToDraw.length < teamCount) return
// Use the seed for transparency
const random = seededRandom(drawSeed)
let pool = [...playersToDraw]
const newTeams: any[] = Array.from({ length: teamCount }, (_, i) => ({
name: `Time ${i + 1}`,
@@ -107,10 +192,9 @@ export function MatchFlow({ group, arenas = [] }: MatchFlowProps) {
}))
if (drawMode === 'balanced') {
// Balanced still uses levels, but we shuffle within same levels or use seed for order
pool.sort((a, b) => {
if (b.level !== a.level) return b.level - a.level
return random() - 0.5 // Use seeded random for tie-breaking
return random() - 0.5
})
pool.forEach((p) => {
@@ -122,36 +206,17 @@ export function MatchFlow({ group, arenas = [] }: MatchFlowProps) {
teamWithLowestQuality.players.push(p)
})
} else {
// Pure random draw based on seed
pool = pool.sort(() => random() - 0.5)
pool.forEach((p, i) => {
newTeams[i % teamCount].players.push(p)
})
}
setTeams(newTeams)
}
const downloadImage = async (id: string) => {
const element = document.getElementById(id)
if (!element) return
try {
const dataUrl = await toPng(element, { backgroundColor: '#000' })
const link = document.createElement('a')
link.download = `temfut-time-${id}.png`
link.href = dataUrl
link.click()
} catch (err) {
console.error(err)
}
}
const handleConfirm = async () => {
setIsSaving(true)
try {
// If it was a scheduled match, we just update it. Otherwise create new.
// If it was a scheduled match, we just update it. Otherwise create new.
// For simplicity in this demo, createMatch now handles the seed and location.
const match = await createMatch(group.id, matchDate, teams, 'IN_PROGRESS', location, selectedPlayers.length, drawSeed, selectedArenaId)
setActiveMatchId(match.id)
setStep(2)
@@ -176,331 +241,220 @@ export function MatchFlow({ group, arenas = [] }: MatchFlowProps) {
}
return (
<div className="space-y-8 max-w-5xl mx-auto pb-20">
{/* Step Indicator */}
<div className="flex items-center gap-4">
<div className={clsx("h-1 flex-1 rounded-full bg-border relative overflow-hidden")}>
<div
className={clsx("absolute inset-0 bg-primary transition-all duration-700 ease-out")}
style={{ width: step === 1 ? '50%' : '100%' }}
/>
<div className="space-y-6 max-w-6xl mx-auto pb-10 px-4">
{/* Step Header */}
<div className="flex items-center justify-between py-2 border-b border-white/5">
<div className="flex items-center gap-2">
<div className={clsx("w-2 h-2 rounded-full", step === 1 ? "bg-primary shadow-[0_0_10px_#10b981]" : "bg-zinc-800")} />
<div className={clsx("w-2 h-2 rounded-full", step === 2 ? "bg-primary shadow-[0_0_10px_#10b981]" : "bg-zinc-800")} />
</div>
<div className="px-3 py-1 bg-zinc-900 border border-white/5 rounded-full">
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-muted">Processo de Escalação</span>
</div>
<span className="text-xs font-bold text-muted uppercase tracking-[0.2em] whitespace-nowrap">
Fase de {step === 1 ? 'Escalação' : 'Distribuição'}
</span>
</div>
{step === 1 ? (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Controls Panel */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* LEFT: Setup de Campo */}
<div className="lg:col-span-4 space-y-6">
<div className="ui-card p-6 space-y-6">
<div className="ui-card p-6 space-y-6 bg-surface shadow-sm border-white/5">
<div className="space-y-1">
<h3 className="text-sm font-bold uppercase tracking-widest text-primary">Configuração</h3>
<p className="text-xs text-muted">Ajuste os parâmetros do sorteio.</p>
<h3 className="text-lg font-black uppercase italic tracking-tighter text-primary">Setup de Campo</h3>
<p className="text-[10px] text-muted font-bold uppercase tracking-widest">Sorteio Inteligente TemFut</p>
</div>
<div className="space-y-4">
<div className="ui-form-field">
<label className="text-label ml-0.5">Data da Partida</label>
<input
type="date"
value={matchDate}
onChange={(e) => setMatchDate(e.target.value)}
className="ui-input w-full h-12"
/>
<div className="space-y-5">
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted block ml-0.5">Data do Evento</label>
<DateTimePicker value={matchDate} onChange={setMatchDate} />
</div>
<div className="ui-form-field">
<label className="text-label ml-0.5">Local / Arena</label>
<select
value={selectedArenaId}
onChange={(e) => {
setSelectedArenaId(e.target.value)
const arena = arenas?.find(a => a.id === e.target.value)
if (arena) setLocation(arena.name)
}}
className="ui-input w-full h-12 bg-surface-raised"
style={{ appearance: 'none' }} // Custom arrow if needed, but default is fine for now
>
<option value="" className="bg-surface-raised text-muted">Selecione um local...</option>
{arenas?.map(arena => (
<option key={arena.id} value={arena.id} className="bg-surface-raised text-foreground">
{arena.name}
</option>
))}
</select>
{/* Fallback hidden input or just use location state if manual entry is needed later */}
</div>
<div className="ui-form-field">
<label className="text-label ml-0.5">Seed de Transparência</label>
<div className="flex gap-2">
<input
type="text"
value={drawSeed}
onChange={(e) => setDrawSeed(e.target.value.toUpperCase())}
className="ui-input flex-1 h-12 font-mono text-sm uppercase tracking-widest"
placeholder="SEED123"
/>
<button
onClick={generateNewSeed}
className="p-3 border border-border rounded-md hover:bg-white/5 transition-colors"
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted block ml-0.5">Arena/Gramado</label>
<div className="relative">
<select
value={selectedArenaId}
onChange={(e) => {
setSelectedArenaId(e.target.value)
const arena = arenas?.find(a => a.id === e.target.value)
if (arena) setLocation(arena.name)
}}
className="ui-input w-full h-10 bg-zinc-950/50 border-white/10 appearance-none focus:border-primary px-4 rounded-xl text-sm"
>
<RefreshCw className="w-5 h-5 text-muted" />
</button>
<option value="">Selecione o local...</option>
{arenas?.map(arena => <option key={arena.id} value={arena.id}>{arena.name}</option>)}
</select>
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-muted pointer-events-none" />
</div>
</div>
<div className="ui-form-field">
<label className="text-label ml-0.5">Número de Times</label>
<div className="grid grid-cols-3 gap-2">
{[2, 3, 4].map(n => (
<button
key={n}
onClick={() => setTeamCount(n)}
className={clsx(
"py-3 text-sm font-bold rounded-md border transition-all",
teamCount === n ? "bg-primary text-background border-primary" : "bg-transparent border-border text-muted"
)}
>
{n}
</button>
))}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted block ml-0.5">Times</label>
<div className="flex bg-zinc-950 p-1 rounded-xl border border-white/10">
{[2, 3, 4].map(n => (
<button key={n} onClick={() => setTeamCount(n)} className={clsx("flex-1 py-1.5 text-xs font-black rounded-lg transition-all", teamCount === n ? "bg-white text-black" : "text-muted hover:text-white")}>{n}</button>
))}
</div>
</div>
</div>
<div className="ui-form-field">
<label className="text-label ml-0.5">Modo de Sorteio</label>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setDrawMode('balanced')}
className={clsx(
"py-3 text-xs font-bold rounded-md border transition-all uppercase",
drawMode === 'balanced' ? "bg-primary text-background border-primary" : "bg-transparent border-border text-muted"
)}
>
Equilibrado
</button>
<button
onClick={() => setDrawMode('random')}
className={clsx(
"py-3 text-xs font-bold rounded-md border transition-all uppercase",
drawMode === 'random' ? "bg-primary text-background border-primary" : "bg-transparent border-border text-muted"
)}
>
Aleatório
</button>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-muted block ml-0.5">Equilibrado</label>
<div className="flex bg-zinc-950 p-1 rounded-xl border border-white/10">
<button onClick={() => setDrawMode('balanced')} className={clsx("flex-1 py-1.5 text-[9px] font-black uppercase rounded-lg transition-all", drawMode === 'balanced' ? "bg-primary text-black" : "text-muted")}>Sim</button>
<button onClick={() => setDrawMode('random')} className={clsx("flex-1 py-1.5 text-[9px] font-black uppercase rounded-lg transition-all", drawMode === 'random' ? "bg-primary text-black" : "text-muted")}>Não</button>
</div>
</div>
</div>
</div>
<button
onClick={performDraw}
disabled={selectedPlayers.length < teamCount}
className="ui-button w-full h-14 text-sm font-bold shadow-lg shadow-emerald-500/10"
>
<Shuffle className="w-5 h-5 mr-2" /> Sortear Atletas
<button onClick={performDraw} disabled={selectedPlayers.length < teamCount} className="ui-button w-full h-11 text-xs font-black uppercase tracking-[0.2em] bg-white text-black hover:bg-zinc-100 shadow-md transition-all group">
<Shuffle className="w-4 h-4 mr-2 group-hover:rotate-180 transition-transform duration-500" /> Gerar Sorteio
</button>
</div>
{scheduledMatchId && (
<div className="p-4 bg-primary/5 border border-primary/20 rounded-lg space-y-2">
<p className="text-[10px] font-bold uppercase text-primary tracking-wider">Evento Agendado</p>
<p className="text-[11px] text-muted leading-relaxed">
Importamos automaticamente {selectedPlayers.length} atletas que confirmaram presença pelo link público.
</p>
</div>
)}
</div>
{/* Players Selection Grid */}
{/* RIGHT: Lista de Chamada */}
<div className="lg:col-span-8 space-y-6">
<div className="ui-card p-6">
<div className="flex items-center justify-between mb-6">
<div className="space-y-1">
<h3 className="text-sm font-bold uppercase tracking-widest text-primary">Convocação</h3>
<p className="text-xs text-muted">Selecione os atletas presentes em campo.</p>
<div className="ui-card p-6 bg-surface shadow-sm border-white/5">
<div className="flex items-center justify-between mb-6 border-b border-white/5 pb-4">
<div>
<h3 className="text-xl font-black uppercase italic tracking-tighter leading-none">Lista de Chamada</h3>
<p className="text-[10px] text-muted font-bold uppercase tracking-widest mt-1">Confirme os atletas presentes</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold">{selectedPlayers.length}</span>
<span className="text-xs font-bold text-muted uppercase ml-2 tracking-widest">Atletas</span>
<div className="flex items-center gap-6">
<div className="text-right border-r border-white/5 pr-6">
<span className="text-2xl font-black text-primary leading-none">{selectedPlayers.length}</span>
<p className="text-[9px] font-bold uppercase text-muted tracking-widest">Atletas</p>
</div>
<button onClick={() => setSelectedPlayers(group.players.map((p: any) => p.id))} className="text-[9px] font-black uppercase text-muted hover:text-white px-4 py-1.5 border border-white/10 rounded-full transition-all bg-black/40">Selecionar Todos</button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-2 gap-3 max-h-[500px] overflow-y-auto pr-2 custom-scrollbar">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 max-h-[500px] overflow-y-auto pr-4 custom-scrollbar">
{group.players.map((p: any) => (
<button
key={p.id}
onClick={() => togglePlayer(p.id)}
className={clsx(
"flex items-center justify-between p-3 rounded-lg border transition-all text-left group",
selectedPlayers.includes(p.id)
? "bg-primary/5 border-primary shadow-sm"
: "bg-surface border-border hover:border-white/20"
"flex items-center gap-4 p-3 rounded-2xl border transition-all text-left group relative",
selectedPlayers.includes(p.id) ? "bg-primary/5 border-primary" : "bg-black/20 border-white/5 hover:border-white/10"
)}
>
<div className="flex items-center gap-3">
<div className={clsx(
"w-10 h-10 rounded-lg border flex items-center justify-center font-mono font-bold text-xs transition-all",
selectedPlayers.includes(p.id) ? "bg-primary text-background border-primary" : "bg-surface-raised border-border"
)}>
{p.number !== null && p.number !== undefined ? p.number : getInitials(p.name)}
</div>
<div>
<p className="text-sm font-bold tracking-tight">{p.name}</p>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-muted font-bold uppercase tracking-wider">{p.position}</span>
<div className="flex gap-0.5">
{[...Array(5)].map((_, i) => (
<Star key={i} className={clsx(
"w-2.5 h-2.5",
i < p.level ? "text-primary fill-primary" : "text-border fill-transparent"
)} />
))}
</div>
<div className={clsx("w-10 h-10 rounded-xl flex items-center justify-center font-black text-xs border transition-all", selectedPlayers.includes(p.id) ? "bg-primary text-black border-primary shadow-[0_0_10px_rgba(16,185,129,0.2)]" : "bg-zinc-950 border-white/10 text-muted")}>
{p.number || getInitials(p.name)}
</div>
<div className="flex-1">
<p className="text-sm font-black uppercase tracking-tight group-hover:text-primary transition-colors line-clamp-1">{p.name}</p>
<div className="flex items-center gap-3 mt-1 opacity-40">
<span className="text-[9px] font-black uppercase">{p.position}</span>
<div className="flex gap-0.5">
{[...Array(5)].map((_, s) => <Star key={s} className={clsx("w-2.5 h-2.5", s < (p.level || 0) ? "fill-primary text-primary" : "text-white/10")} />)}
</div>
</div>
</div>
<div className={clsx(
"w-5 h-5 rounded-full border flex items-center justify-center transition-all",
selectedPlayers.includes(p.id) ? "bg-primary border-primary" : "border-border"
)}>
{selectedPlayers.includes(p.id) && <Check className="w-3 h-3 text-background font-bold" />}
</div>
{selectedPlayers.includes(p.id) && <Check className="w-4 h-4 text-primary" />}
</button>
))}
</div>
</div>
{/* Temp Draw Overlay / Result Preview */}
<AnimatePresence>
{teams.length > 0 && (
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
className="ui-card p-6 bg-surface-raised border-primary/20 shadow-xl space-y-6"
>
<div className="flex items-center justify-between border-b border-white/5 pb-4">
<div className="space-y-1">
<h3 className="text-sm font-bold uppercase tracking-widest text-primary">Sorteio Concluído</h3>
<p className="text-xs text-muted font-mono">HASH: {drawSeed}</p>
</div>
<button
onClick={handleConfirm}
disabled={isSaving}
className="ui-button h-12 px-8 font-bold text-sm"
>
{isSaving ? 'Salvando...' : 'Confirmar & Ver Capas'}
</button>
{/* PREVIEW DO SORTEIO SECTION */}
{teams.length > 0 && (
<motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} className="space-y-6">
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-2">
<div className="w-1 h-4 bg-primary rounded-full" />
<h3 className="text-sm font-black uppercase italic tracking-tighter text-primary">Resultado Preview</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{teams.map((t, i) => (
<div key={i} className="ui-card p-4 bg-background/50 border-white/5">
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: t.color }} />
<h4 className="text-xs font-bold uppercase tracking-widest">{t.name}</h4>
</div>
<div className="space-y-2">
{t.players.map((p: any) => (
<div key={p.id} className="text-xs flex items-center justify-between py-2 border-b border-white/5 last:border-0">
<div className="flex items-center gap-3">
<span className="font-mono text-primary font-bold w-4">{p.number !== null && p.number !== undefined ? p.number : getInitials(p.name)}</span>
<span className="font-medium">{p.name}</span>
</div>
<span className="text-[10px] uppercase font-bold text-muted">{p.position}</span>
</div>
))}
</div>
<button onClick={handleConfirm} disabled={isSaving} className="ui-button px-8 h-10 text-xs font-black uppercase tracking-widest bg-white text-black shadow-md">
{isSaving ? 'Salvando...' : 'Confirmar & Ver Capas'}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{teams.map((t, i) => (
<div key={i} className="ui-card p-4 bg-surface shadow-sm border-white/5 relative overflow-hidden">
<div className="flex items-center gap-3 mb-4">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: t.color, boxShadow: `0 0 10px ${t.color}` }} />
<h4 className="text-sm font-black uppercase italic">{t.name}</h4>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="space-y-1 font-sans">
{t.players.map((p: any) => (
<div key={p.id} className="flex items-center justify-between py-2 border-b border-white/[0.03] last:border-0 hover:bg-white/[0.02] px-2 rounded-lg transition-all">
<span className="text-xs font-bold text-white/80 uppercase line-clamp-1">{p.name}</span>
<span className="text-[9px] font-black uppercase text-muted bg-white/5 px-2 py-0.5 rounded-md">{p.position}</span>
</div>
))}
</div>
</div>
))}
</div>
</motion.div>
)}
</div>
</div>
) : (
/* Step 2: Custom Download Cards */
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-1000">
<div className="flex items-center justify-between px-2">
<div className="space-y-1">
<h2 className="text-3xl font-bold tracking-tight">Capas Exclusivas</h2>
<p className="text-muted text-sm flex items-center gap-2">
Sorteio Auditado com Seed <span className="text-primary font-mono font-bold">{drawSeed}</span>
</p>
/* STEP 2: CANVAS PREMIUM COVERS */
<div className="space-y-12 animate-in fade-in slide-in-from-bottom-10 duration-1000 pb-20">
<div className="flex flex-col xl:flex-row items-center justify-between gap-8 px-8 bg-zinc-900/40 p-8 rounded-[2rem] border border-white/5 backdrop-blur-md shadow-xl">
<div className="flex flex-col md:flex-row items-center gap-8">
<div className="space-y-1 text-center md:text-left">
<h2 className="text-3xl font-black uppercase italic tracking-tighter leading-none">Capas de Convocação</h2>
<p className="text-[10px] text-muted font-bold uppercase tracking-[0.3em]">Canvas Engine v3.0 HD</p>
</div>
{/* Card Customization Toolbar */}
<div className="flex bg-black/60 p-1.5 rounded-2xl border border-white/5 backdrop-blur-sm">
{[
{ key: 'showNumbers', label: 'Nº' },
{ key: 'showPositions', label: 'Pos' },
{ key: 'showStars', label: 'Nível' },
{ key: 'showSponsors', label: 'Patr' },
].map((opt) => (
<button
key={opt.key}
onClick={() => setCardOptions(prev => ({ ...prev, [opt.key]: !(prev as any)[opt.key] }))}
className={clsx(
"px-4 py-2 text-[10px] font-black uppercase tracking-widest rounded-xl transition-all flex items-center gap-2",
(cardOptions as any)[opt.key] ? "bg-primary text-black" : "text-muted hover:text-white"
)}
>
<div className={clsx("w-3 h-3 rounded-sm border flex items-center justify-center", (cardOptions as any)[opt.key] ? "border-black bg-black" : "border-muted")}>
{(cardOptions as any)[opt.key] && <Check className="w-2.5 h-2.5 text-primary" />}
</div>
{opt.label}
</button>
))}
</div>
</div>
<div className="flex items-center gap-3">
<button onClick={() => setStep(1)} className="ui-button px-5 h-10 text-[9px] font-black uppercase tracking-widest border-white/10 hover:bg-white/5 transition-all">Voltar Sorteio</button>
<button onClick={handleFinish} className="ui-button px-8 h-10 text-[9px] font-black uppercase tracking-widest bg-white text-black hover:bg-zinc-200">Finalizar Pelada</button>
</div>
<button
onClick={handleFinish}
disabled={isSaving}
className="ui-button px-10 h-12 text-sm font-bold uppercase tracking-[0.2em]"
>
Finalizar Registro
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start justify-items-center">
{teams.map((t, i) => (
<div key={i} className="space-y-4">
<div
id={`team-${i}`}
className="aspect-[4/5] bg-background rounded-2xl border border-white/10 p-8 flex flex-col items-center justify-between shadow-2xl overflow-hidden relative"
>
{/* Abstract Patterns */}
<div className="absolute top-0 right-0 w-48 h-48 bg-primary/10 blur-[80px] rounded-full -mr-24 -mt-24" />
<div className="absolute bottom-0 left-0 w-32 h-32 bg-primary/5 blur-[60px] rounded-full -ml-16 -mb-16" />
<div className="text-center relative z-10 w-full">
<div className="w-14 h-14 bg-surface-raised rounded-2xl border border-white/10 flex items-center justify-center mx-auto mb-6 shadow-xl">
<Trophy className="w-6 h-6 text-primary" />
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-primary uppercase tracking-[0.4em]">TemFut Pro</p>
<h3 className="text-2xl font-black uppercase tracking-tighter">{t.name}</h3>
<div className="h-1.5 w-12 bg-primary mx-auto mt-4 rounded-full" />
</div>
</div>
<div className="w-full space-y-3 relative z-10 flex-1 flex flex-col justify-center my-8 text-sm px-2">
{t.players.map((p: any) => (
<div key={p.id} className="flex items-center justify-between py-2.5 border-b border-white/5 last:border-0">
<div className="flex items-center gap-4">
<span className="font-mono text-primary font-black text-sm w-6">
{p.number !== null && p.number !== undefined ? p.number : getInitials(p.name)}
</span>
<div className="flex flex-col">
<span className="font-bold text-xs uppercase tracking-tight">{p.name}</span>
<div className="flex gap-0.5 mt-1">
{[...Array(5)].map((_, i) => (
<Star key={i} className={clsx(
"w-2 h-2",
i < p.level ? "text-primary fill-primary" : "text-white/10 fill-transparent"
)} />
))}
</div>
</div>
</div>
<div className="text-right">
<p className="text-[9px] font-black uppercase text-primary tracking-widest">{p.position}</p>
</div>
</div>
))}
</div>
<div className="w-full flex items-center justify-between relative z-10">
<p className="text-[8px] text-muted font-black uppercase tracking-[0.3em] font-mono">{drawSeed}</p>
<p className="text-[10px] text-muted font-bold uppercase tracking-widest">{matchDate}</p>
</div>
</div>
<button
onClick={() => downloadImage(`team-${i}`)}
className="ui-button-ghost w-full py-4 text-xs font-bold uppercase tracking-widest hover:border-primary/50"
>
<Download className="w-5 h-5 mr-2" /> Baixar Card de Time
</button>
</div>
<CanvasTeamPreview
key={i}
team={t}
group={group}
date={matchDate}
location={location}
drawSeed={drawSeed}
sponsors={sponsors}
options={cardOptions}
/>
))}
</div>
<div className="flex items-center justify-center pt-10 border-t border-white/5 opacity-40">
<div className="flex items-center gap-4">
<Shield className="w-5 h-5 text-primary" />
<span className="text-[10px] font-black uppercase tracking-[0.5em]">Gerado via TemFut Pixel Perfect Engine</span>
</div>
</div>
</div>
)}
</div>

View File

@@ -1,6 +1,6 @@
'use client'
import React, { useState, useMemo } from 'react'
import React, { useState, useMemo, useEffect } from 'react'
import { Calendar, Users, Trophy, ChevronRight, X, Clock, ExternalLink, Star, Link as LinkIcon, MapPin, Share2, Shuffle, Trash2, MessageCircle, Repeat, Search, LayoutGrid, List, Check } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { clsx } from 'clsx'
@@ -19,7 +19,7 @@ const getInitials = (name: string) => {
.slice(0, 2)
}
export function MatchHistory({ matches, players = [] }: { matches: any[], players?: any[] }) {
export function MatchHistory({ matches, players = [], groupName = 'Pelada' }: { matches: any[], players?: any[], groupName?: string }) {
const [selectedMatch, setSelectedMatch] = useState<any | null>(null)
const [modalTab, setModalTab] = useState<'confirmed' | 'unconfirmed'>('confirmed')
@@ -28,6 +28,12 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list') // Default to list for history
const [currentPage, setCurrentPage] = useState(1)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const [copySuccess, setCopySuccess] = useState<string | null>(null)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Confirmation Modal State
const [deleteModal, setDeleteModal] = useState<{
@@ -151,37 +157,69 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
}
}
const shareMatchLink = (match: any) => {
const copyMatchLink = (match: any, silent = false) => {
const url = `${window.location.origin}/match/${match.id}/confirmacao`
navigator.clipboard.writeText(url)
if (!silent) setCopySuccess('Link de confirmação copiado!')
setTimeout(() => setCopySuccess(null), 2000)
}
const shareMatchWhatsApp = (match: any) => {
const url = `${window.location.origin}/match/${match.id}/confirmacao`
const dateStr = new Date(match.date).toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'short' })
const timeStr = new Date(match.date).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
const text = `⚽ *CONVOCAÇÃO: ${match.group?.name || 'TEMFUT'}* ⚽\n\n` +
`📍 *Local:* ${match.location || 'A definir'}\n` +
`📅 *Data:* ${dateStr} às ${timeStr}\n\n` +
`Confirme sua presença pelo link abaixo:\n🔗 ${url}`
`Confirmem presença pelo link oficial:\n🔗 ${url}`
navigator.clipboard.writeText(url)
window.open(`https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`, '_blank')
}
const shareWhatsAppList = (match: any) => {
const confirmed = (match.attendances || []).filter((a: any) => a.status === 'CONFIRMED')
const unconfirmed = players.filter(p => !match.attendances?.find((a: any) => a.playerId === p.id && a.status === 'CONFIRMED'))
const confirmedIds = new Set(confirmed.map((a: any) => a.playerId))
const pending = players.filter(p => !confirmedIds.has(p.id))
const url = `${window.location.origin}/match/${match.id}/confirmacao`
const text = `⚽ *CONVOCAÇÃO: ${match.group?.name || 'TEMFUT'}* ⚽\n\n` +
`📍 *Local:* ${match.location || 'A definir'}\n` +
`📅 *Data:* ${new Date(match.date).toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'short' })} às ${new Date(match.date).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}\n\n` +
const dateStr = new Date(match.date).toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'short' })
const timeStr = new Date(match.date).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
const finalGroupName = match.group?.name || groupName
const text = `⚽ *LISTA DE PRESENÇA: ${finalGroupName.toUpperCase()}* ⚽\n\n` +
`📅 *JOGO:* ${dateStr} às ${timeStr}\n` +
`📍 *LOCAL:* ${match.location || 'A definir'}\n\n` +
`✅ *CONFIRMADOS (${confirmed.length}/${match.maxPlayers || '∞'}):*\n` +
confirmed.map((a: any, i: number) => `${i + 1}. ${a.player.name}`).join('\n') +
`\n\n❌ *AGUARDANDO:* \n` +
unconfirmed.map((p: any) => `- ${p.name}`).join('\n') +
`\n\n🔗 *Confirme pelo link:* ${url}`
(confirmed.length > 0
? confirmed.map((a: any) => `${a.player.name}`).join('\n')
: "_Nenhuma confirmação ainda_") +
`\n\n *AGUARDANDO:* \n` +
(pending.length > 0
? pending.map((p: any) => `▫️ ${p.name}`).join('\n')
: "_Todos confirmados!_") +
`\n\n🔗 *Confirme sua presença aqui:* ${url}`
navigator.clipboard.writeText(text)
const whatsappUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`
window.open(whatsappUrl, '_blank')
setCopySuccess('Lista formatada copiada!')
setTimeout(() => setCopySuccess(null), 2000)
window.open(`https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`, '_blank')
}
const openMatchLink = (match: any) => {
const url = `${window.location.origin}/match/${match.id}/confirmacao`
window.open(url, '_blank')
}
const copyAgendaLink = () => {
const url = `${window.location.origin}/agenda`
navigator.clipboard.writeText(url)
setCopySuccess('Link da agenda copiado!')
setTimeout(() => setCopySuccess(null), 2000)
}
return (
@@ -191,14 +229,26 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
<h2 className="text-xl font-bold tracking-tight">Histórico de Partidas</h2>
<p className="text-xs text-muted font-medium">Gerencie o histórico e agende novos eventos.</p>
</div>
<AnimatePresence>
{copySuccess && (
<motion.div
initial={{ opacity: 0, y: -20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.9 }}
className="fixed top-12 left-1/2 -translate-x-1/2 z-[200] bg-primary text-background px-6 py-2.5 rounded-full font-black text-[10px] uppercase tracking-[0.2em] shadow-2xl flex items-center gap-2 border border-white/20"
>
<Check className="w-3 h-3" /> {copySuccess}
</motion.div>
)}
</AnimatePresence>
<div className="flex flex-col sm:flex-row items-center gap-4">
{selectedIds.size > 0 ? (
<div className="flex items-center gap-2 w-full sm:w-auto animate-in fade-in slide-in-from-right-4">
<span className="text-xs font-bold text-muted uppercase tracking-wider px-3">{selectedIds.size} selecionados</span>
<div className="flex items-center gap-2 w-full sm:w-auto animate-in fade-in slide-in-from-right-4 bg-surface-raised border border-border rounded-xl px-2 pr-2 py-1">
<span className="text-[10px] font-black text-muted uppercase tracking-[0.2em] px-3">{selectedIds.size} SELECIONADOS</span>
<button
onClick={handleBulkDelete}
className="ui-button bg-red-500/10 text-red-500 hover:bg-red-500/20 border-red-500/20 h-10 w-full sm:w-auto"
className="ui-button bg-red-500/10 text-red-500 hover:bg-red-500/20 border-red-500/20 h-9 px-4 rounded-lg"
>
<Trash2 className="w-4 h-4 mr-2" /> Excluir
</button>
@@ -240,15 +290,27 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
<AnimatePresence mode='popLayout'>
{paginatedMatches.length > 0 && (
<div className={clsx("col-span-full flex items-center px-2 mb-2", viewMode === 'grid' ? "justify-end" : "")}>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedIds.size === paginatedMatches.length && paginatedMatches.length > 0}
onChange={toggleSelectAll}
className="w-4 h-4 rounded border-border text-primary bg-background"
/>
<span className="text-[10px] font-bold uppercase text-muted">Selecionar Todos</span>
</div>
<button
onClick={toggleSelectAll}
className="flex items-center gap-2 group cursor-pointer"
>
<div className={clsx(
"w-5 h-5 rounded-lg border-2 flex items-center justify-center transition-all duration-200",
selectedIds.size === paginatedMatches.length && paginatedMatches.length > 0
? "bg-primary border-primary shadow-[0_0_10px_rgba(16,185,129,0.4)]"
: "border-muted/30 bg-surface/50 group-hover:border-primary/50"
)}>
{selectedIds.size === paginatedMatches.length && paginatedMatches.length > 0 && (
<Check className="w-3.5 h-3.5 text-background font-bold stroke-[3]" />
)}
</div>
<span className={clsx(
"text-[10px] font-bold uppercase tracking-widest transition-colors",
selectedIds.size === paginatedMatches.length ? "text-primary" : "text-muted group-hover:text-foreground"
)}>
Selecionar Todos
</span>
</button>
</div>
)}
@@ -269,23 +331,28 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
)}
>
{/* Checkbox for selection */}
<div onClick={(e) => e.stopPropagation()} className="absolute top-4 right-4 z-20">
<input
type="checkbox"
checked={selectedIds.has(match.id)}
onChange={() => toggleSelection(match.id)}
className={clsx(
"w-5 h-5 rounded border-border text-primary bg-surface transition-all",
selectedIds.has(match.id) || "opacity-0 group-hover:opacity-100"
)}
/>
<div
onClick={(e) => {
e.stopPropagation()
toggleSelection(match.id)
}}
className={clsx("z-20 p-2", viewMode === 'grid' ? "absolute top-4 right-4 -mr-2 -mt-2" : "mr-4 -ml-2")}
>
<div className={clsx(
"w-5 h-5 rounded-lg border-2 flex items-center justify-center transition-all duration-200",
selectedIds.has(match.id)
? "bg-primary border-primary shadow-[0_0_10px_rgba(16,185,129,0.4)] scale-110"
: "border-muted/30 bg-surface/50 opacity-0 group-hover:opacity-100 hover:border-primary/50"
)}>
{selectedIds.has(match.id) && <Check className="w-3.5 h-3.5 text-background font-bold stroke-[3]" />}
</div>
</div>
<div className={clsx("flex items-center gap-4", viewMode === 'list' ? "flex-1" : "w-full")}>
<div className="p-3 bg-surface-raised border border-border rounded-lg text-center min-w-[56px] shrink-0">
<p className="text-sm font-bold leading-none">{new Date(match.date).getDate()}</p>
<p className="text-sm font-bold leading-none">{mounted ? new Date(match.date).getDate() : '...'}</p>
<p className="text-[10px] text-muted uppercase font-medium mt-1">
{new Date(match.date).toLocaleDateString('pt-BR', { month: 'short' }).replace('.', '')}
{mounted ? new Date(match.date).toLocaleDateString('pt-BR', { month: 'short' }).replace('.', '') : '...'}
</p>
</div>
@@ -316,7 +383,7 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
<div className="w-1 h-1 rounded-full bg-border" />
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{new Date(match.date).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
{mounted ? new Date(match.date).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '--:--'}
</div>
{/* Actions only in List Mode here, else in modal */}
@@ -326,12 +393,12 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
<button
onClick={(e) => {
e.stopPropagation()
shareWhatsAppList(match)
copyMatchLink(match)
}}
className="p-1.5 text-primary hover:bg-primary/10 rounded transition-colors"
title="Copiar Link"
title="Copiar Link de Confirmação"
>
<Share2 className="w-4 h-4" />
<LinkIcon className="w-4 h-4" />
</button>
)}
<ChevronRight className="w-4 h-4" />
@@ -352,27 +419,29 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center items-center gap-4 mt-8">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="p-2 rounded-lg border border-border hover:bg-surface-raised disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-4 h-4 rotate-180" />
</button>
<span className="text-xs font-bold text-muted uppercase tracking-widest">
Página {currentPage} de {totalPages}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="p-2 rounded-lg border border-border hover:bg-surface-raised disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
)}
{
totalPages > 1 && (
<div className="flex justify-center items-center gap-4 mt-8">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="p-2 rounded-lg border border-border hover:bg-surface-raised disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-4 h-4 rotate-180" />
</button>
<span className="text-xs font-bold text-muted uppercase tracking-widest">
Página {currentPage} de {totalPages}
</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="p-2 rounded-lg border border-border hover:bg-surface-raised disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
)
}
{/* Modal Overlay */}
<AnimatePresence>
@@ -400,12 +469,12 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
</span>
</div>
<p className="text-sm text-muted">
{new Date(selectedMatch.date).toLocaleDateString('pt-BR', {
{mounted ? new Date(selectedMatch.date).toLocaleDateString('pt-BR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}
}) : 'Carregando data...'}
</p>
</div>
<div className="flex items-center gap-2">
@@ -575,16 +644,28 @@ export function MatchHistory({ matches, players = [] }: { matches: any[], player
</button>
<div className="flex flex-col sm:flex-row gap-2 order-1 sm:order-2 w-full sm:w-auto">
<button
onClick={() => shareWhatsAppList(selectedMatch)}
className="ui-button-ghost py-3 border-primary/20 text-primary hover:bg-primary/5 h-12 sm:h-auto"
onClick={() => copyMatchLink(selectedMatch)}
className="ui-button-ghost py-3 border-white/10 text-muted hover:text-white h-12 sm:h-auto"
title="Apenas copiar o link exclusivo"
>
<MessageCircle className="w-4 h-4 mr-2" /> Copiar Lista
<LinkIcon className="w-4 h-4 mr-2" /> Copiar Link
</button>
<button
onClick={() => shareMatchLink(selectedMatch)}
className="ui-button py-3 h-12 sm:h-auto"
onClick={() => openMatchLink(selectedMatch)}
className="ui-button-ghost py-3 border-primary/20 text-primary hover:bg-primary/5 h-12 sm:h-auto"
title="Abrir página de visualização do evento"
>
<LinkIcon className="w-4 h-4 mr-2" /> Compartilhar Link
<ExternalLink className="w-4 h-4 mr-2" /> Ver Evento
</button>
<button
onClick={() => shareWhatsAppList(selectedMatch)}
className="ui-button py-2.5 h-12 sm:h-auto shadow-lg shadow-emerald-500/20 bg-emerald-600 hover:bg-emerald-700 border-none text-white font-bold"
title="Copiar lista e enviar para o WhatsApp"
>
<svg className="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z" />
</svg>
WhatsApp
</button>
</div>
</div>

View File

@@ -2,18 +2,20 @@
import { useState } from 'react'
import { motion } from 'framer-motion'
import { Users, Mail, Lock, LogIn, AlertCircle, Eye, EyeOff } from 'lucide-react'
import { Users, Mail, Lock, LogIn, AlertCircle, Eye, EyeOff, LayoutPanelLeft, Trophy } from 'lucide-react'
import { loginPeladaAction } from '@/app/actions/auth'
import { ThemeWrapper } from '@/components/ThemeWrapper'
interface Props {
slug: string
groupName?: string
group: any
}
export default function PeladaLoginPage({ slug, groupName }: Props) {
export default function PeladaLoginPage({ slug, group }: Props) {
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [email, setEmail] = useState('')
async function handleForm(formData: FormData) {
setLoading(true)
@@ -25,82 +27,101 @@ export default function PeladaLoginPage({ slug, groupName }: Props) {
setError(result.error)
setLoading(false)
}
// Se der certo, a Action redireciona automaticamente
}
return (
<div className="min-h-screen bg-zinc-950 flex items-center justify-center p-4">
<div className="bg-zinc-900 border border-zinc-800 rounded-3xl p-8 shadow-2xl w-full max-w-md relative overflow-hidden">
<div className="text-center mb-8 relative z-10">
<div className="w-16 h-16 rounded-2xl bg-emerald-500 flex items-center justify-center mx-auto mb-4 shadow-lg shadow-emerald-500/20">
<Users className="w-8 h-8 text-white" />
</div>
<h1 className="text-2xl font-bold text-white uppercase tracking-tight">
{groupName || slug}
</h1>
<p className="text-zinc-500 text-sm mt-1">Acesse o painel de gestão</p>
</div>
<ThemeWrapper primaryColor={group.primaryColor}>
<div className="min-h-screen bg-zinc-950 flex items-center justify-center p-4">
<div className="bg-zinc-900 border border-zinc-800 rounded-3xl p-8 shadow-2xl w-full max-w-md relative overflow-hidden">
{/* Decorative Background Glow */}
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/10 blur-[50px] rounded-full -mr-16 -mt-16 pointer-events-none" />
{error && (
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3 text-red-400 text-sm">
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<p>{error}</p>
</div>
)}
<form action={handleForm} className="space-y-4">
<input type="hidden" name="slug" value={slug} />
<div className="space-y-1">
<label className="text-[10px] font-bold text-zinc-500 uppercase ml-1">Email</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-600" />
<input
name="email"
type="email"
required
placeholder="seu@email.com"
className="w-full pl-12 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white focus:border-emerald-500 transition-all outline-none"
/>
<div className="text-center mb-8 relative z-10">
<div className="w-20 h-20 flex items-center justify-center mx-auto mb-4 overflow-hidden">
{group.logoUrl ? (
<img src={group.logoUrl} alt={group.name} className="w-full h-full object-contain" />
) : (
<div className="w-full h-full rounded-2xl bg-zinc-800 flex items-center justify-center border border-white/5">
<Trophy className="w-10 h-10 text-zinc-500" />
</div>
)}
</div>
<h1 className="text-2xl font-black text-white uppercase tracking-tight">
{group.name || slug}
</h1>
<p className="text-zinc-500 text-[10px] font-bold uppercase tracking-[0.2em] mt-2">Área Administrativa</p>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-zinc-500 uppercase ml-1">Senha</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-600" />
<input
name="password"
type={showPassword ? 'text' : 'password'}
required
placeholder="••••••••"
className="w-full pl-12 pr-12 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-white focus:border-emerald-500 transition-all outline-none font-mono"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-white"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
{error && (
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3 text-red-500 text-xs font-bold uppercase">
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<p>{error}</p>
</div>
)}
<form action={handleForm} className="space-y-4">
<input type="hidden" name="slug" value={slug} />
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted uppercase ml-1 tracking-widest">Acesso do Gestor</label>
<div className="relative group">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-600 group-focus-within:text-primary transition-colors" />
<input
name="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="seu@email.com"
className="w-full pl-12 pr-4 py-4 bg-zinc-950 border border-zinc-800 rounded-2xl text-white focus:border-primary transition-all outline-none text-sm font-medium"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-muted uppercase ml-1 tracking-widest">Senha Secreta</label>
<div className="relative group">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-600 group-focus-within:text-primary transition-colors" />
<input
name="password"
type={showPassword ? 'text' : 'password'}
required
placeholder="••••••••"
className="w-full pl-12 pr-12 py-4 bg-zinc-950 border border-zinc-800 rounded-2xl text-white focus:border-primary transition-all outline-none font-mono text-sm"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-white transition-colors"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-4 bg-primary hover:bg-primary/90 text-white font-black rounded-2xl transition-all disabled:opacity-50 shadow-lg shadow-primary/20 flex items-center justify-center gap-3 mt-6 uppercase tracking-widest text-xs"
>
{loading ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<>
<LogIn className="w-5 h-5" />
Entrar no Painel
</>
)}
</button>
</form>
<div className="mt-8 text-center border-t border-zinc-800 pt-6">
<a href={`http://localhost`} className="text-[10px] text-zinc-600 hover:text-primary uppercase font-black tracking-[0.2em] transition-colors">
Voltar para TemFut
</a>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-4 bg-emerald-500 hover:bg-emerald-400 text-zinc-950 font-black rounded-xl transition-all disabled:opacity-50"
>
{loading ? 'CARREGANDO...' : 'ENTRAR NO TIME'}
</button>
</form>
<div className="mt-8 text-center border-t border-zinc-800 pt-6">
<a href="http://localhost" className="text-xs text-zinc-600 hover:text-zinc-400 uppercase font-bold tracking-widest">
Voltar para TemFut
</a>
</div>
</div>
</div>
</ThemeWrapper>
)
}

View File

@@ -0,0 +1,159 @@
'use client'
import { useState } from 'react'
import { User, CreditCard, Shield, Crown, Trophy, Star } from 'lucide-react'
import { ProfileForm } from '@/components/ProfileForm'
interface ProfileClientProps {
group: any
leader: any
}
export function ProfileClient({ group, leader }: ProfileClientProps) {
const [activeTab, setActiveTab] = useState<'personal' | 'subscription'>('personal')
const planConfig = {
FREE: { name: 'Grátis', icon: Star, color: 'text-zinc-500', gradient: 'from-zinc-500/10 to-transparent' },
BASIC: { name: 'Básico', icon: Shield, color: 'text-blue-500', gradient: 'from-blue-500/10 to-transparent' },
PRO: { name: 'Pro Premium', icon: Crown, color: 'text-amber-500', gradient: 'from-amber-500/10 to-transparent' }
}
const currentPlan = planConfig[group.plan as keyof typeof planConfig] || planConfig.FREE
return (
<div className="grid grid-cols-1 md:grid-cols-12 gap-8">
{/* Sidebar Navigation (Tabs) */}
<aside className="md:col-span-4 space-y-4">
<div className="ui-card p-6 text-center shadow-xl relative overflow-hidden">
{/* Background Pattern */}
<div className={`absolute top-0 inset-x-0 h-1 bg-gradient-to-r ${currentPlan.gradient}`} />
<div className="w-20 h-20 bg-surface-raised rounded-full flex items-center justify-center mx-auto mb-4 border border-border mt-2">
<User className="w-10 h-10 text-muted" />
</div>
<h3 className="font-bold text-lg">{leader?.name || 'Administrador'}</h3>
<p className="text-[10px] text-primary font-black uppercase tracking-[0.2em] mt-1">Líder TemFut</p>
<div className="mt-6 pt-4 border-t border-border flex items-center justify-center gap-2">
<currentPlan.icon className={`w-4 h-4 ${currentPlan.color}`} />
<span className="text-[10px] font-black uppercase tracking-widest">{currentPlan.name}</span>
</div>
</div>
<div className="flex flex-col gap-2">
<button
onClick={() => setActiveTab('personal')}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-black uppercase tracking-widest transition-all text-left ${activeTab === 'personal'
? 'bg-primary/10 text-primary border border-primary/20 shadow-sm'
: 'text-muted hover:text-foreground hover:bg-surface-raised'
}`}
>
<User className="w-4 h-4" />
Dados Pessoais
</button>
<button
onClick={() => setActiveTab('subscription')}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-black uppercase tracking-widest transition-all text-left ${activeTab === 'subscription'
? 'bg-amber-500/10 text-amber-500 border border-amber-500/20 shadow-sm'
: 'text-muted hover:text-foreground hover:bg-surface-raised'
}`}
>
<CreditCard className="w-4 h-4" />
Assinatura & Pagamentos
</button>
</div>
</aside>
{/* Main Content */}
<main className="md:col-span-8">
{activeTab === 'personal' ? (
<div className="ui-card p-8 animate-in fade-in slide-in-from-right-4 duration-500 shadow-xl">
<div className="flex items-center gap-3 mb-8 pb-4 border-b border-border">
<div className="p-2 bg-primary/10 rounded-lg">
<User className="w-5 h-5 text-primary" />
</div>
<div>
<h4 className="font-bold">Informações do Atleta</h4>
<p className="text-xs text-muted">Ajuste como você será exibido nas peladas.</p>
</div>
</div>
{!leader ? (
<div className="py-12 text-center space-y-6">
<div className="w-16 h-16 bg-surface-raised rounded-full flex items-center justify-center mx-auto border border-dashed border-border">
<User className="w-8 h-8 text-muted" />
</div>
<div className="space-y-2">
<h5 className="font-bold">Perfil não encontrado</h5>
<p className="text-xs text-muted max-w-xs mx-auto">Parece que você ainda não tem um cadastro de atleta neste grupo.</p>
</div>
<button
onClick={async () => {
// Simple action to create the leader profile if missing
window.location.href = '/dashboard/players'
}}
className="ui-button px-6 h-10 text-xs"
>
Criar meu Perfil
</button>
</div>
) : (
<ProfileForm player={leader} />
)}
</div>
) : (
<div className="ui-card p-8 animate-in fade-in slide-in-from-right-4 duration-500 shadow-xl">
<div className="flex items-center gap-3 mb-8 pb-4 border-b border-border">
<div className="p-2 bg-amber-500/10 rounded-lg">
<CreditCard className="w-5 h-5 text-amber-500" />
</div>
<div>
<h4 className="font-bold text-amber-500 uppercase tracking-tight">Sua Assinatura</h4>
<p className="text-xs text-muted">Status do seu plano no TemFut.</p>
</div>
</div>
<div className="space-y-6">
<div className="p-8 bg-surface-raised rounded-3xl border border-border relative overflow-hidden group">
<div className="absolute top-0 right-0 w-48 h-48 bg-primary/5 blur-3xl rounded-full -mr-24 -mt-24 group-hover:bg-primary/10 transition-colors" />
<div className="flex items-center justify-between relative z-10">
<div className="space-y-2">
<p className="text-[10px] font-black text-muted uppercase tracking-[0.4em]">Plano Atual</p>
<h5 className={`text-3xl font-black uppercase tracking-tighter ${currentPlan.color}`}>{currentPlan.name}</h5>
</div>
<div className={`w-16 h-16 rounded-2xl bg-surface border border-border flex items-center justify-center shadow-2xl`}>
<currentPlan.icon className={`w-8 h-8 ${currentPlan.color}`} />
</div>
</div>
<div className="mt-12 grid grid-cols-1 sm:grid-cols-2 gap-8 relative z-10">
<div className="space-y-1">
<p className="text-[9px] text-muted font-black uppercase tracking-widest">Próximo Vencimento</p>
<p className="text-sm font-black">{group.planExpiresAt ? new Date(group.planExpiresAt).toLocaleDateString() : 'Sem Expiração'}</p>
</div>
<div className="space-y-1">
<p className="text-[9px] text-muted font-black uppercase tracking-widest">Investimento</p>
<p className="text-sm font-black italic">{group.plan === 'FREE' ? 'Gratuito' : 'R$ 29,90 / mês'}</p>
</div>
</div>
<div className="mt-10 pt-6 border-t border-white/5 flex items-center justify-between relative z-10">
<span className="text-[10px] font-bold text-zinc-500 uppercase">Pelada: {group.name}</span>
<button className="text-[10px] font-black uppercase text-primary hover:underline transition-all">Alterar Plano</button>
</div>
</div>
<div className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-xl flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-emerald-500/10 flex items-center justify-center">
<Shield className="w-4 h-4 text-emerald-500" />
</div>
<span className="text-xs text-zinc-500 font-medium">Sua infraestrutura está replicada em alta disponibilidade.</span>
</div>
</div>
</div>
)}
</main>
</div>
)
}

View File

@@ -0,0 +1,121 @@
'use client'
import { useState } from 'react'
import { Star, Save, Check, Loader2 } from 'lucide-react'
import { updatePlayer } from '@/actions/player'
import { clsx } from 'clsx'
export function ProfileForm({ player }: { player: any }) {
const [isSaving, setIsSaving] = useState(false)
const [success, setSuccess] = useState(false)
const [formData, setFormData] = useState({
name: player?.name || '',
position: player?.position || 'MEI',
level: player?.level || 3,
number: player?.number || ''
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!player?.id) return
setIsSaving(true)
try {
await updatePlayer(player.id, {
name: formData.name,
position: formData.position,
level: formData.level,
number: formData.number ? parseInt(formData.number.toString()) : null
})
setSuccess(true)
setTimeout(() => setSuccess(false), 3000)
} catch (error) {
console.error(error)
} finally {
setIsSaving(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="ui-form-field">
<label className="text-[10px] font-black text-muted uppercase tracking-widest ml-1">Nome de Exibição</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
className="ui-input h-12"
placeholder="Nome como aparecerá na ficha"
/>
</div>
<div className="ui-form-field">
<label className="text-[10px] font-black text-muted uppercase tracking-widest ml-1">Posição Preferida</label>
<select
value={formData.position}
onChange={(e) => setFormData(prev => ({ ...prev, position: e.target.value }))}
className="ui-input h-12 bg-surface"
>
<option value="GOL">Goleiro</option>
<option value="ZAG">Zagueiro</option>
<option value="LAT">Lateral</option>
<option value="MEI">Meio-Campo</option>
<option value="ATA">Atacante</option>
</select>
</div>
<div className="ui-form-field">
<label className="text-[10px] font-black text-muted uppercase tracking-widest ml-1">Número da Camisa</label>
<input
type="number"
value={formData.number}
onChange={(e) => setFormData(prev => ({ ...prev, number: e.target.value }))}
className="ui-input h-12"
placeholder="Ex: 10"
/>
</div>
<div className="ui-form-field">
<label className="text-[10px] font-black text-muted uppercase tracking-widest ml-1">Nível Técnico (Estrelas)</label>
<div className="flex items-center gap-2 h-12 px-4 bg-surface-raised border border-border rounded-md">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => setFormData(prev => ({ ...prev, level: star }))}
className="focus:outline-none transition-transform active:scale-90"
>
<Star
className={clsx(
"w-5 h-5 transition-colors",
star <= formData.level ? "text-primary fill-primary" : "text-zinc-700 hover:text-zinc-500"
)}
/>
</button>
))}
</div>
</div>
</div>
<div className="pt-6 flex justify-end">
<button
type="submit"
disabled={isSaving}
className="ui-button px-10 h-12 min-w-[180px]"
>
{isSaving ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : success ? (
<>
<Check className="w-4 h-4" /> Salvo!
</>
) : (
<>
<Save className="w-4 h-4" /> Salvar Perfil
</>
)}
</button>
</div>
</form>
)
}

View File

@@ -1,21 +1,23 @@
'use client'
import { useState } from 'react'
import { MapPin, Palette } from 'lucide-react'
import { MapPin, Palette, Briefcase } from 'lucide-react'
import { clsx } from 'clsx'
import { motion, AnimatePresence } from 'framer-motion'
interface SettingsTabsProps {
branding: React.ReactNode
arenas: React.ReactNode
sponsors: React.ReactNode
}
export function SettingsTabs({ branding, arenas }: SettingsTabsProps) {
const [activeTab, setActiveTab] = useState<'branding' | 'arenas'>('branding')
export function SettingsTabs({ branding, arenas, sponsors }: SettingsTabsProps) {
const [activeTab, setActiveTab] = useState<'branding' | 'arenas' | 'sponsors'>('branding')
const tabs = [
{ id: 'branding', label: 'Identidade Visual', icon: Palette },
{ id: 'arenas', label: 'Locais & Arenas', icon: MapPin },
{ id: 'sponsors', label: 'Patrocínios', icon: Briefcase },
] as const
return (
@@ -62,6 +64,17 @@ export function SettingsTabs({ branding, arenas }: SettingsTabsProps) {
{arenas}
</motion.div>
)}
{activeTab === 'sponsors' && (
<motion.div
key="sponsors"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
{sponsors}
</motion.div>
)}
</AnimatePresence>
</div>
</div>

View File

@@ -10,7 +10,8 @@ import {
Home,
Search,
Banknote,
LogOut
LogOut,
Eye
} from 'lucide-react'
import { clsx } from 'clsx'
@@ -23,6 +24,7 @@ export function Sidebar({ group }: { group: any }) {
{ name: 'Partidas', href: '/dashboard/matches', icon: Calendar },
{ name: 'Jogadores', href: '/dashboard/players', icon: Users },
{ name: 'Financeiro', href: '/dashboard/financial', icon: Banknote },
{ name: 'Agenda Pública', href: '/agenda', icon: Eye },
{ name: 'Configurações', href: '/dashboard/settings', icon: Settings },
]
@@ -84,8 +86,11 @@ export function Sidebar({ group }: { group: any }) {
<span>Sair</span>
</button>
<div className="flex items-center gap-3 px-2">
<div className="w-8 h-8 rounded-full bg-surface-raised border border-border flex items-center justify-center overflow-hidden">
<Link
href="/dashboard/profile"
className="flex items-center gap-3 px-2 py-2 hover:bg-surface-raised rounded-xl transition-all group"
>
<div className="w-8 h-8 rounded-full bg-surface-raised border border-border flex items-center justify-center overflow-hidden group-hover:border-primary/50 transition-colors">
{group.logoUrl ? (
<img src={group.logoUrl} alt="" className="w-full h-full object-cover" />
) : (
@@ -93,10 +98,10 @@ export function Sidebar({ group }: { group: any }) {
)}
</div>
<div className="overflow-hidden">
<p className="text-xs font-semibold truncate">{group.name}</p>
<p className="text-[10px] text-muted truncate">Plano Free</p>
<p className="text-xs font-semibold truncate group-hover:text-primary transition-colors">{group.name}</p>
<p className="text-[10px] text-muted truncate">Meu Perfil</p>
</div>
</div>
</Link>
</div>
</aside>
)

View File

@@ -0,0 +1,184 @@
'use client'
import { useState, useTransition } from 'react'
import { createSponsor, deleteSponsor } from '@/actions/sponsor'
import { Briefcase, Plus, Trash2, Loader2, Image as ImageIcon } from 'lucide-react'
import type { Sponsor } from '@prisma/client'
import { DeleteConfirmationModal } from '@/components/DeleteConfirmationModal'
interface SponsorsManagerProps {
groupId: string
sponsors: Sponsor[]
}
export function SponsorsManager({ groupId, sponsors }: SponsorsManagerProps) {
const [isPending, startTransition] = useTransition()
const [filePreview, setFilePreview] = useState<string | null>(null)
const [deleteModal, setDeleteModal] = useState<{
isOpen: boolean
sponsorId: string | null
isDeleting: boolean
}>({
isOpen: false,
sponsorId: null,
isDeleting: false
})
const handleDelete = (id: string) => {
setDeleteModal({
isOpen: true,
sponsorId: id,
isDeleting: false
})
}
const confirmDelete = () => {
if (!deleteModal.sponsorId) return
setDeleteModal(prev => ({ ...prev, isDeleting: true }))
startTransition(async () => {
try {
await deleteSponsor(deleteModal.sponsorId!)
setDeleteModal({ isOpen: false, sponsorId: null, isDeleting: false })
} catch (error) {
console.error(error)
alert('Erro ao excluir patrocinador.')
setDeleteModal(prev => ({ ...prev, isDeleting: false }))
}
})
}
return (
<div className="ui-card p-8 space-y-8">
<header>
<h3 className="font-semibold text-lg flex items-center gap-2">
<Briefcase className="w-5 h-5 text-primary" />
Patrocinadores
</h3>
<p className="text-muted text-sm">
Adicione os parceiros que apoiam o seu futebol para dar destaque nas capas exclusivas.
</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{sponsors.map((sponsor) => (
<div key={sponsor.id} className="flex items-center justify-between p-4 rounded-lg border border-border bg-surface-raised/50 group hover:border-primary/50 transition-colors">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-surface rounded-lg overflow-hidden flex items-center justify-center border border-white/5">
{sponsor.logoUrl ? (
<img src={sponsor.logoUrl} alt={sponsor.name} className="w-full h-full object-cover" />
) : (
<ImageIcon className="w-5 h-5 text-muted opacity-30" />
)}
</div>
<div>
<p className="font-bold text-foreground uppercase text-xs tracking-widest">{sponsor.name}</p>
<p className="text-[10px] text-muted font-medium mt-0.5">Patrocinador Ativo</p>
</div>
</div>
<button
onClick={() => handleDelete(sponsor.id)}
disabled={isPending}
className="p-2 text-muted hover:text-destructive hover:bg-destructive/10 rounded-lg transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100"
title="Excluir patrocinador"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
{sponsors.length === 0 && (
<div className="col-span-full text-center py-10 px-4 border border-dashed border-border rounded-xl bg-surface/50">
<Briefcase className="w-8 h-8 text-muted mx-auto mb-3 opacity-50" />
<p className="text-muted text-sm uppercase font-bold tracking-widest">Nenhum patrocinador ainda</p>
</div>
)}
</div>
<form action={(formData) => {
const name = formData.get('name') as string
const logoFile = formData.get('logo') as File
if (!name) return
startTransition(async () => {
await createSponsor(formData)
const form = document.getElementById('sponsor-form') as HTMLFormElement
form?.reset()
setFilePreview(null)
})
}} id="sponsor-form" className="pt-8 mt-8 border-t border-border">
<input type="hidden" name="groupId" value={groupId} />
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-end">
<div className="ui-form-field md:col-span-5">
<label className="text-label ml-1">Nome da Empresa</label>
<input
name="name"
placeholder="Ex: Pizzaria do Vale"
className="ui-input w-full"
required
/>
</div>
<div className="ui-form-field md:col-span-5">
<label className="text-label ml-1">Logo do Patrocinador</label>
<div className="relative group">
<input
type="file"
name="logo"
accept="image/*"
className="hidden"
id="sponsor-logo-upload"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
const reader = new FileReader()
reader.onloadend = () => setFilePreview(reader.result as string)
reader.readAsDataURL(file)
}
}}
/>
<label
htmlFor="sponsor-logo-upload"
className="ui-input w-full flex items-center gap-3 cursor-pointer group-hover:border-primary/50 transition-all bg-surface"
>
<div className="w-8 h-8 rounded bg-surface-raised flex items-center justify-center border border-white/5 overflow-hidden">
{filePreview ? (
<img src={filePreview} className="w-full h-full object-cover" />
) : (
<ImageIcon className="w-4 h-4 text-muted" />
)}
</div>
<span className="text-xs text-muted truncate">
{filePreview ? 'Alterar Logo selecionada' : 'Selecionar imagem da logo...'}
</span>
</label>
</div>
</div>
<div className="md:col-span-2">
<button
type="submit"
disabled={isPending}
className="ui-button h-[42px] w-full"
>
{isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Plus className="w-4 h-4 mr-2" />
)}
Adicionar
</button>
</div>
</div>
</form>
<DeleteConfirmationModal
isOpen={deleteModal.isOpen}
onClose={() => setDeleteModal({ isOpen: false, sponsorId: null, isDeleting: false })}
onConfirm={confirmDelete}
isDeleting={deleteModal.isDeleting}
title="Excluir Patrocinador?"
description="Tem certeza que deseja remover este patrocinador? Ele deixará de aparecer nas novas capas geradas."
confirmText="Sim, remover"
/>
</div>
)
}

View File

@@ -11,11 +11,24 @@ export function ThemeWrapper({ primaryColor, children }: ThemeWrapperProps) {
useEffect(() => {
if (primaryColor) {
document.documentElement.style.setProperty('--primary-color', primaryColor)
// Update other derived colors if necessary
// For example, if we needed to convert hex to HSL for other variables
}
}, [primaryColor])
return children || null
const hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` : '16, 185, 129';
};
return (
<>
<style dangerouslySetInnerHTML={{
__html: `
:root {
${primaryColor ? `--primary-color: ${primaryColor};` : ''}
${primaryColor ? `--primary-rgb: ${hexToRgb(primaryColor)};` : ''}
}
`}} />
{children || null}
</>
)
}