-
- {match.status === 'SCHEDULED'
- ? `${(match.attendances || []).filter((a: any) => a.status === 'CONFIRMED').length} confirmados`
- : `${(match.teams || []).reduce((acc: number, t: any) => acc + t.players.length, 0)} jogadores`}
+ {viewMode === 'grid' && (
+
+ e.stopPropagation()}
+ className={clsx(
+ "w-full flex items-center justify-center gap-2 py-2.5 rounded-xl border text-[10px] font-black uppercase tracking-widest transition-all hover:shadow-lg shadow-sm border-white/5",
+ s.color
+ )}
+ >
+
+ {s.actionLabel}
+
+
+ )}
+
+
+
+
+
+ {match.status === 'CONVOCACAO'
+ ? `${(match.attendances || []).filter((a: any) => a.status === 'CONFIRMED').length} confirmados`
+ : `${(match.teams || []).reduce((acc: number, t: any) => acc + t.players.length, 0)} jogadores`}
+
+
+
+ {s.label}
+
+
@@ -388,20 +451,33 @@ export function MatchHistory({ matches, players = [], groupName = 'Pelada' }: {
{/* Actions only in List Mode here, else in modal */}
{viewMode === 'list' && (
-
- {match.status === 'SCHEDULED' && (
+
+ e.stopPropagation()}
+ className={clsx(
+ "flex items-center gap-2 px-4 py-2 rounded-xl border text-[9px] font-black uppercase tracking-[0.15em] transition-all hover:scale-105 active:scale-95 shadow-lg",
+ s.color
+ )}
+ title={s.actionLabel}
+ >
+
+ {s.actionLabel}
+
+
+ {match.status === 'CONVOCACAO' && (
{
e.stopPropagation()
copyMatchLink(match)
}}
- className="p-1.5 text-primary hover:bg-primary/10 rounded transition-colors"
+ className="p-2 text-muted hover:text-primary hover:bg-primary/10 rounded-lg transition-colors border border-transparent hover:border-primary/20"
title="Copiar Link de Confirmação"
>
)}
-
+
)}
@@ -478,6 +554,17 @@ export function MatchHistory({ matches, players = [], groupName = 'Pelada' }: {
+
+ {React.createElement(getStatusInfo(selectedMatch.status).icon, { className: "w-4 h-4" })}
+ {getStatusInfo(selectedMatch.status).actionLabel}
+
handleDeleteMatch(selectedMatch.id)}
className="p-2.5 text-muted hover:text-red-500 transition-colors rounded-lg"
@@ -495,7 +582,7 @@ export function MatchHistory({ matches, players = [], groupName = 'Pelada' }: {
- {selectedMatch.status === 'SCHEDULED' ? (
+ {selectedMatch.status === 'CONVOCACAO' ? (
@@ -631,6 +718,17 @@ export function MatchHistory({ matches, players = [], groupName = 'Pelada' }: {
))}
+
+ {selectedMatch.status === ('SORTEIO' as any) && (
+
+
+ Iniciar Gamificação & Votação
+
+
+ )}
)}
diff --git a/src/components/MatchPodium.tsx b/src/components/MatchPodium.tsx
new file mode 100644
index 0000000..c5d6bda
--- /dev/null
+++ b/src/components/MatchPodium.tsx
@@ -0,0 +1,199 @@
+'use client'
+
+import React from 'react'
+import { motion } from 'framer-motion'
+import { Trophy, Medal, Star, Crown, Zap, Shield, Sparkles } from 'lucide-react'
+import { clsx } from 'clsx'
+
+interface PlayerResult {
+ player: {
+ id: string
+ name: string
+ number?: number | string
+ position?: string
+ level?: number
+ }
+ craque: number
+ pereba: number
+ fairPlay: number
+}
+
+interface MatchPodiumProps {
+ results: PlayerResult[]
+ context?: 'dashboard' | 'public'
+}
+
+export function MatchPodium({ results, context = 'public' }: MatchPodiumProps) {
+ // Calculando saldo e ordenando (caso não venha ordenado)
+ // Critério: (Craque - Pereba) DESC, depois Craque DESC, depois FairPlay DESC
+ const sortedResults = [...results].sort((a, b) => {
+ const scoreA = a.craque - a.pereba
+ const scoreB = b.craque - b.pereba
+ if (scoreB !== scoreA) return scoreB - scoreA
+ if (b.craque !== a.craque) return b.craque - a.craque
+ return b.fairPlay - a.fairPlay
+ })
+
+ const top3 = sortedResults.slice(0, 3)
+ // Reorganizar para ordem visual: 2º, 1º, 3º
+ const podiumOrder = [
+ top3[1], // 2nd Place (Left)
+ top3[0], // 1st Place (Center)
+ top3[2] // 3rd Place (Right)
+ ].filter(Boolean) // Remove undefined if less than 3 players
+
+ const getInitials = (name: string) => name.split(' ').slice(0, 2).map(n => n[0]).join('').toUpperCase()
+
+ const PodiumItem = ({ result, place, index }: { result: PlayerResult, place: 1 | 2 | 3, index: number }) => {
+ if (!result) return
+
+ const isWinner = place === 1
+ return (
+
+ {/* Crown for Winner */}
+ {isWinner && (
+
+
+
+
+ )}
+
+ {/* Avatar / Card */}
+
+ {isWinner &&
}
+
+
+
+ {result.player.number || getInitials(result.player.name)}
+
+
+
+ {/* Badge Place */}
+
+ {place}º LUGAR
+
+
+
+ {/* Name & Stats */}
+
+
+ {result.player.name}
+ {isWinner && }
+
+
+
+
+ Saldo
+ 0 ? "text-emerald-500" : "text-red-500")}>
+ {result.craque - result.pereba}
+
+
+
+
+
+
+ {result.craque}
+
+
+
+ {result.pereba}
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ {/* TOP 3 PODIUM */}
+
+ {/* 2nd Place */}
+ {podiumOrder[0] &&
}
+
+ {/* 1st Place */}
+ {podiumOrder[1] &&
}
+
+ {/* 3rd Place */}
+ {podiumOrder[2] &&
}
+
+
+ {/* OTHER PLAYERS LIST */}
+ {sortedResults.length > 3 && (
+
+ Demais Classificados
+
+ {sortedResults.slice(3).map((res, i) => (
+
+
+
#{i + 4}
+
+
+ {res.player.number || getInitials(res.player.name)}
+
+
{res.player.name}
+
+
+
+
+ 0 ? "text-emerald-500" : "text-white/50")}>
+ {res.craque - res.pereba} pts
+
+
+
+
+ ))}
+
+
+ )}
+
+ )
+}
diff --git a/src/components/MatchScheduler.tsx b/src/components/MatchScheduler.tsx
new file mode 100644
index 0000000..e5c9b83
--- /dev/null
+++ b/src/components/MatchScheduler.tsx
@@ -0,0 +1,301 @@
+'use client'
+
+import React, { useState, useMemo, useEffect } from 'react'
+import { Calendar, MapPin, ArrowRight, Trophy, Repeat, Hash, ChevronRight } from 'lucide-react'
+import { createScheduledMatch } from '@/actions/match'
+import { useRouter } from 'next/navigation'
+import type { Arena } from '@prisma/client'
+import { DateTimePicker } from '@/components/DateTimePicker'
+import { motion, AnimatePresence } from 'framer-motion'
+import { clsx } from 'clsx'
+
+interface MatchSchedulerProps {
+ arenas: Arena[]
+}
+
+export function MatchScheduler({ arenas }: MatchSchedulerProps) {
+ const router = useRouter()
+ const [date, setDate] = useState('')
+ const [location, setLocation] = useState('')
+ const [selectedArenaId, setSelectedArenaId] = useState('')
+ const [maxPlayers, setMaxPlayers] = useState('24')
+ const [isRecurring, setIsRecurring] = useState(false)
+ const [recurrenceInterval, setRecurrenceInterval] = useState<'WEEKLY' | 'MONTHLY' | 'YEARLY'>('WEEKLY')
+ const [recurrenceEndDate, setRecurrenceEndDate] = useState('')
+ const [isSubmitting, setIsSubmitting] = useState(false)
+
+ const previewDates = useMemo(() => {
+ if (!date || !isRecurring) return []
+
+ const dates: Date[] = []
+ const startDate = new Date(date)
+ let currentDate = new Date(startDate)
+
+ // Increment based on interval
+ const advanceDate = (d: Date) => {
+ const next = new Date(d)
+ if (recurrenceInterval === 'WEEKLY') next.setDate(next.getDate() + 7)
+ else if (recurrenceInterval === 'MONTHLY') next.setMonth(next.getMonth() + 1)
+ else if (recurrenceInterval === 'YEARLY') next.setFullYear(next.getFullYear() + 1)
+ return next
+ }
+
+ currentDate = advanceDate(currentDate)
+
+ let endDate: Date
+ if (recurrenceEndDate) {
+ endDate = new Date(`${recurrenceEndDate}T23:59:59`)
+ } else {
+ // Preview next 4 occurrences
+ let previewEnd = new Date(startDate)
+ for (let i = 0; i < 4; i++) previewEnd = advanceDate(previewEnd)
+ endDate = previewEnd
+ }
+
+ while (currentDate <= endDate) {
+ dates.push(new Date(currentDate))
+ currentDate = advanceDate(currentDate)
+ if (dates.length > 10) break
+ }
+
+ return dates
+ }, [date, isRecurring, recurrenceInterval, recurrenceEndDate])
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!date) return
+ setIsSubmitting(true)
+
+ try {
+ await createScheduledMatch(
+ '',
+ date,
+ location,
+ parseInt(maxPlayers) || 0,
+ isRecurring,
+ recurrenceInterval,
+ recurrenceEndDate || undefined,
+ selectedArenaId
+ )
+ router.push('/dashboard/matches')
+ } catch (error) {
+ console.error(error)
+ alert('Erro ao agendar evento')
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const intervals = [
+ { id: 'WEEKLY', label: 'Semanal', desc: 'Toda semana' },
+ { id: 'MONTHLY', label: 'Mensal', desc: 'Todo mês' },
+ { id: 'YEARLY', label: 'Anual', desc: 'Todo ano' },
+ ] as const
+
+ return (
+
+ )
+}
diff --git a/src/components/PlayersList.tsx b/src/components/PlayersList.tsx
index 2106471..efbc27b 100644
--- a/src/components/PlayersList.tsx
+++ b/src/components/PlayersList.tsx
@@ -1,8 +1,8 @@
'use client'
import React, { useState, useMemo } from 'react'
-import { Plus, Trash2, UserPlus, Star, Search, Filter, MoreHorizontal, User, Shield, Target, Zap, ChevronDown, LayoutGrid, List, ChevronRight, Check, X, AlertCircle } from 'lucide-react'
-import { addPlayer, deletePlayer, deletePlayers } from '@/actions/player'
+import { Plus, Trash2, UserPlus, Star, Search, Filter, MoreHorizontal, User, Shield, Target, Zap, ChevronDown, LayoutGrid, List, ChevronRight, Check, X, AlertCircle, Pencil } from 'lucide-react'
+import { addPlayer, deletePlayer, deletePlayers, updatePlayer } from '@/actions/player'
import { motion, AnimatePresence } from 'framer-motion'
import { clsx } from 'clsx'
import { DeleteConfirmationModal } from '@/components/DeleteConfirmationModal'
@@ -16,8 +16,9 @@ export function PlayersList({ group }: { group: any }) {
const [activeTab, setActiveTab] = useState<'ALL' | 'DEF' | 'MEI' | 'ATA'>('ALL')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const [error, setError] = useState
(null)
- const [isAdding, setIsAdding] = useState(false)
+ const [isSaving, setIsSaving] = useState(false)
const [isFormOpen, setIsFormOpen] = useState(false)
+ const [editingPlayer, setEditingPlayer] = useState(null)
// Pagination & Selection
const [currentPage, setCurrentPage] = useState(1)
@@ -40,27 +41,53 @@ export function PlayersList({ group }: { group: any }) {
description: ''
})
- const handleAddPlayer = async (e: React.FormEvent) => {
+ const handleSavePlayer = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
if (!newPlayerName) return
- setIsAdding(true)
+ setIsSaving(true)
try {
const playerNumber = number.trim() === '' ? null : parseInt(number)
- await addPlayer(group.id, newPlayerName, level, playerNumber, position)
- setNewPlayerName('')
- setLevel(3)
- setNumber('')
- setPosition('MEI')
- setIsFormOpen(false)
+
+ if (editingPlayer) {
+ await updatePlayer(editingPlayer.id, {
+ name: newPlayerName,
+ level,
+ number: playerNumber,
+ position
+ })
+ } else {
+ await addPlayer(group.id, newPlayerName, level, playerNumber, position)
+ }
+
+ closeForm()
} catch (err: any) {
setError(err.message)
} finally {
- setIsAdding(false)
+ setIsSaving(false)
}
}
+ const closeForm = () => {
+ setNewPlayerName('')
+ setLevel(3)
+ setNumber('')
+ setPosition('MEI')
+ setEditingPlayer(null)
+ setIsFormOpen(false)
+ setError(null)
+ }
+
+ const openEditForm = (player: any) => {
+ setEditingPlayer(player)
+ setNewPlayerName(player.name)
+ setLevel(player.level)
+ setNumber(player.number?.toString() || '')
+ setPosition(player.position as any)
+ setIsFormOpen(true)
+ }
+
const filteredPlayers = useMemo(() => {
return group.players.filter((p: any) => {
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -174,7 +201,6 @@ export function PlayersList({ group }: { group: any }) {
>
{
- setError(null)
setIsFormOpen(true)
}}
className="ui-button w-full sm:w-auto shadow-lg shadow-primary/20"
@@ -193,7 +219,7 @@ export function PlayersList({ group }: { group: any }) {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
- onClick={() => setIsFormOpen(false)}
+ onClick={closeForm}
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
/>
-
Novo Atleta
-
Adicionar ao elenco
+
+ {editingPlayer ? 'Editar Atleta' : 'Novo Atleta'}
+
+
+ {editingPlayer ? 'Atualizar informações' : 'Adicionar ao elenco'}
+
setIsFormOpen(false)}
+ onClick={closeForm}
className="p-2 text-muted hover:text-foreground rounded-lg transition-colors"
>