diff --git a/src/app/api/team/[teamId]/route.ts b/src/app/api/team/[teamId]/route.ts index 0719027..cbbd66f 100644 --- a/src/app/api/team/[teamId]/route.ts +++ b/src/app/api/team/[teamId]/route.ts @@ -8,16 +8,23 @@ export async function GET( { params }: { params: { teamId: string } }, ) { try { - /* ─── 1) Team holen ─────────────────────────────── */ + /* ─── 1) Team + Invites holen ───────────────────────────── */ const team = await prisma.team.findUnique({ where: { id: params.teamId }, + include: { + invites: { + include: { + user: true, // ⬅ notwendig für eingeladenen Spieler + }, + }, + }, }) if (!team) { return NextResponse.json({ error: 'Team nicht gefunden' }, { status: 404 }) } - /* ─── 2) Alle Steam-IDs sammeln und Users laden ─── */ + /* ─── 2) Aktive + Inaktive Spieler holen ─────────────────── */ const allIds = Array.from( new Set([...team.activePlayers, ...team.inactivePlayers]), ) @@ -33,7 +40,6 @@ export async function GET( }, }) - /* Map steamId → Player */ const byId: Record = Object.fromEntries( users.map(u => [ u.steamId, @@ -47,7 +53,6 @@ export async function GET( ]), ) - /* ─── 3) Arrays umwandeln + sortieren ───────────── */ const activePlayers = team.activePlayers .map(id => byId[id]) .filter(Boolean) @@ -58,7 +63,19 @@ export async function GET( .filter(Boolean) .sort((a, b) => a.name.localeCompare(b.name)) - /* ─── 4) Antwort zusammenbauen ───────────────────── */ + /* ─── 3) Eingeladene Spieler extrahieren ─────────────────── */ + const invitedPlayers: Player[] = team.invites.map(invite => { + const u = invite.user + return { + steamId : u.steamId, + name : u.name ?? 'Unbekannt', + avatar : u.avatar ?? '/assets/img/avatars/default.png', + location : u.location ?? '', + premierRank: u.premierRank ?? 0, + } + }).sort((a, b) => a.name.localeCompare(b.name)) + + /* ─── 4) Antwort zurückgeben ─────────────────────────────── */ const result = { id : team.id, name : team.name, @@ -67,6 +84,7 @@ export async function GET( createdAt : team.createdAt, activePlayers, inactivePlayers, + invitedPlayers, } return NextResponse.json(result) diff --git a/src/app/api/team/available-users/route.ts b/src/app/api/team/available-users/route.ts index 3d017d4..4629e4a 100644 --- a/src/app/api/team/available-users/route.ts +++ b/src/app/api/team/available-users/route.ts @@ -3,15 +3,52 @@ import { prisma } from '@/app/lib/prisma' export async function GET() { try { + // 1. Alle offenen Einladungen (inkl. User-Infos) + const pendingInvites = await prisma.teamInvite.findMany({ + include: { + user: { + select: { + steamId : true, + name : true, + avatar : true, + location : true, + premierRank: true, + team : true, + }, + }, + }, + }) + + // 2. Steam-IDs aus Einladungen extrahieren + const invitedSteamIds = new Set( + pendingInvites + .map(inv => inv.user?.steamId) + .filter(Boolean) + ) + + // 3. Alle Teams laden (nur SteamIDs) + const teams = await prisma.team.findMany({ + select: { + activePlayers: true, + inactivePlayers: true, + }, + }) + + // 4. Steam-IDs aller Teammitglieder sammeln + const teamMemberIds = new Set( + teams.flatMap(team => [...team.activePlayers, ...team.inactivePlayers]) + ) + + // 5. Alle Benutzer laden, die kein Team haben const allUsers = await prisma.user.findMany({ where: { team: null, }, select: { - steamId: true, - name: true, - avatar: true, - location: true, + steamId : true, + name : true, + avatar : true, + location : true, premierRank: true, }, orderBy: { @@ -19,7 +56,13 @@ export async function GET() { }, }) - return NextResponse.json({ users: allUsers }) + // 6. Nur Benutzer behalten, die **nicht** eingeladen und **nicht** bereits im Team sind + const availableUsers = allUsers.filter(user => + !invitedSteamIds.has(user.steamId) && + !teamMemberIds.has(user.steamId) + ) + + return NextResponse.json({ users: availableUsers }) } catch (error) { console.error('Fehler beim Laden der verfügbaren Benutzer:', error) return NextResponse.json({ message: 'Fehler beim Laden' }, { status: 500 }) diff --git a/src/app/api/team/invite/route.ts b/src/app/api/team/invite/route.ts index 7c1095d..78e94a6 100644 --- a/src/app/api/team/invite/route.ts +++ b/src/app/api/team/invite/route.ts @@ -19,7 +19,7 @@ export async function POST(req: NextRequest) { /* Team holen */ const team = await prisma.team.findUnique({ where : { id: teamId }, - select: { name: true }, + select: { name: true, leader: true }, }) if (!team) { return NextResponse.json({ message: 'Team nicht gefunden' }, { status: 404 }) @@ -61,6 +61,12 @@ export async function POST(req: NextRequest) { actionData : notification.actionData ?? undefined, createdAt : notification.createdAt.toISOString(), }) + + await sendServerSSEMessage({ + type: 'team-updated', + teamId, + targetUserIds: team.leader?.steamId, + }) return invite.id }), diff --git a/src/app/api/team/route.ts b/src/app/api/team/route.ts index 7182d20..7a87bc4 100644 --- a/src/app/api/team/route.ts +++ b/src/app/api/team/route.ts @@ -2,6 +2,7 @@ import { getServerSession } from 'next-auth' import { baseAuthOptions } from '@/app/lib/auth' import { prisma } from '@/app/lib/prisma' import { NextResponse } from 'next/server' +import type { InvitedPlayer } from '@/app/types/team' export async function GET() { const session = await getServerSession(baseAuthOptions) @@ -14,25 +15,31 @@ export async function GET() { const team = await prisma.team.findFirst({ where: { OR: [ - { leader: { steamId: session.user.steamId} }, + { leader: { steamId: session.user.steamId } }, { activePlayers: { has: session.user.steamId } }, { inactivePlayers: { has: session.user.steamId } }, ], }, include: { + leader: true, matchesAsTeamA: { include: { teamA: true, teamB: true, - } + }, }, matchesAsTeamB: { include: { teamA: true, teamB: true, - } - } - } + }, + }, + invites: { + include: { + user: true, + }, + }, + }, }) if (!team) { @@ -43,7 +50,13 @@ export async function GET() { const playerData = await prisma.user.findMany({ where: { steamId: { in: steamIds } }, - select: { steamId: true, name: true, avatar: true, location: true, premierRank: true }, + select: { + steamId: true, + name: true, + avatar: true, + location: true, + premierRank: true, + }, }) const activePlayers = team.activePlayers @@ -54,6 +67,25 @@ export async function GET() { .map((id) => playerData.find((m) => m.steamId === id)) .filter(Boolean) + const invitedPlayers: InvitedPlayer[] = Array.from( + new Map( + team.invites.map(invite => { + const u = invite.user + return [ + u.steamId, + { + steamId: u.steamId, + name: u.name ?? 'Unbekannt', + avatar: u.avatar ?? '/assets/img/avatars/default.png', + location: u.location ?? '', + premierRank: u.premierRank ?? 0, + invitationId: invite.id, + } + ] + }) + ).values() + ).sort((a, b) => a.name.localeCompare(b.name)) + const matches = [...team.matchesAsTeamA, ...team.matchesAsTeamB] .filter(m => m.teamA && m.teamB) .sort((a, b) => new Date(a.matchDate).getTime() - new Date(b.matchDate).getTime()) @@ -63,9 +95,10 @@ export async function GET() { id: team.id, name: team.name, logo: team.logo, - leader: team.leaderId, + leader: team.leader?.steamId ?? null, activePlayers, inactivePlayers, + invitedPlayers, matches, }, }) diff --git a/src/app/api/team/update-players/route.ts b/src/app/api/team/update-players/route.ts index 29d9eee..cad309d 100644 --- a/src/app/api/team/update-players/route.ts +++ b/src/app/api/team/update-players/route.ts @@ -5,18 +5,49 @@ import { NextResponse, type NextRequest } from 'next/server' export async function POST(req: NextRequest) { try { - const { teamId, activePlayers, inactivePlayers } = await req.json() + const { teamId, activePlayers, inactivePlayers, invitedPlayers } = await req.json() if (!teamId || !Array.isArray(activePlayers) || !Array.isArray(inactivePlayers)) { return NextResponse.json({ error: 'Ungültige Eingabedaten' }, { status: 400 }) } + // 🟢 Team-Update: aktive & inaktive Spieler await prisma.team.update({ where: { id: teamId }, data: { activePlayers, inactivePlayers }, }) - const allSteamIds = [...activePlayers, ...inactivePlayers] + // 🔄 Einladungen synchronisieren + if (Array.isArray(invitedPlayers)) { + // Zuerst: Bestehende Einladungen für dieses Team laden + const existingInvites = await prisma.teamInvite.findMany({ + where: { teamId }, + }) + + const existingSteamIds = existingInvites.map((invite) => invite.steamId) + const toAdd = invitedPlayers.filter((id: string) => !existingSteamIds.includes(id)) + const toRemove = existingSteamIds.filter((id) => !invitedPlayers.includes(id)) + + // Neue Einladungen erstellen + await prisma.teamInvite.createMany({ + data: toAdd.map((steamId: string) => ({ + teamId, + steamId, + type: 'invite', + })), + skipDuplicates: true, + }) + + // Nicht mehr gelistete Einladungen löschen + await prisma.teamInvite.deleteMany({ + where: { + teamId, + steamId: { in: toRemove }, + }, + }) + } + + const allSteamIds = [...activePlayers, ...inactivePlayers, ...(invitedPlayers || [])] await sendServerSSEMessage({ type: 'team-updated', diff --git a/src/app/api/user/invitations/[action]/route.ts b/src/app/api/user/invitations/[action]/route.ts index 11d7ac6..9a9b418 100644 --- a/src/app/api/user/invitations/[action]/route.ts +++ b/src/app/api/user/invitations/[action]/route.ts @@ -133,6 +133,17 @@ export async function POST( return NextResponse.json({ message: 'Einladung abgelehnt' }) } + if (action === 'revoke') { + await prisma.teamInvite.delete({ where: { id: invitationId } }) + + await prisma.notification.updateMany({ + where: { steamId, actionData: invitationId }, + data: { read: true, actionType: null, actionData: null }, + }) + + return NextResponse.json({ message: 'Einladung gelöscht' }) + } + return NextResponse.json({ message: 'Ungültige Aktion' }, { status: 400 }) } catch (error) { console.error('Fehler bei Einladung:', error) diff --git a/src/app/components/Button.tsx b/src/app/components/Button.tsx index 698dffc..2216bb9 100644 --- a/src/app/components/Button.tsx +++ b/src/app/components/Button.tsx @@ -10,7 +10,7 @@ type ButtonProps = { modalId?: string color?: 'blue' | 'red' | 'gray' | 'green' | 'teal' | 'transparent' variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'white' | 'link' - size?: 'sm' | 'md' | 'lg' + size?: 'xs' |'sm' | 'md' | 'lg' className?: string dropDirection?: "up" | "down" | "auto" disabled?: boolean @@ -47,6 +47,7 @@ const Button = forwardRef(function Button( : {} const sizeClasses: Record = { + xs: 'py-1 px-2', sm: 'py-2 px-3', md: 'py-3 px-4', lg: 'p-4 sm:p-5', diff --git a/src/app/components/DroppableZone.tsx b/src/app/components/DroppableZone.tsx index 1be816d..f910c9e 100644 --- a/src/app/components/DroppableZone.tsx +++ b/src/app/components/DroppableZone.tsx @@ -59,12 +59,7 @@ export function DroppableZone({ {/* Hier sitzt der Droppable-Ref */}
-
+
{children}
diff --git a/src/app/components/InvitePlayersModal.tsx b/src/app/components/InvitePlayersModal.tsx index 8437a62..6efabea 100644 --- a/src/app/components/InvitePlayersModal.tsx +++ b/src/app/components/InvitePlayersModal.tsx @@ -23,68 +23,67 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir const [allUsers, setAllUsers] = useState([]) const [selectedIds, setSelectedIds] = useState([]) - const [isLoading, setIsLoading] = useState(false); + const [invitedIds, setInvitedIds] = useState([]) + const [isLoading, setIsLoading] = useState(false) const [isSuccess, setIsSuccess] = useState(false) const [sentCount, setSentCount] = useState(0) const [searchTerm, setSearchTerm] = useState('') const usersPerPage = 9 const [currentPage, setCurrentPage] = useState(1) - useEffect(() => { if (show) { fetchUsersNotInTeam() - setIsSuccess(false) // Status zurücksetzen beim Öffnen + setIsSuccess(false) + setInvitedIds([]) } }, [show]) const fetchUsersNotInTeam = async () => { try { - setIsLoading(true); + setIsLoading(true) const res = await fetch('/api/team/available-users') const data = await res.json() setAllUsers(data.users || []) } catch (err) { console.error('Fehler beim Laden der Benutzer:', err) - } - finally { - setIsLoading(false); + } finally { + setIsLoading(false) } } const handleSelect = (steamId: string) => { setSelectedIds((prev) => - prev.includes(steamId) ? prev.filter((id) => id !== steamId) : [...prev, steamId] + prev.includes(steamId) + ? prev.filter((id) => id !== steamId) + : [...prev, steamId] ) } - // Entferne das setTimeout aus handleInvite komplett! - const handleInvite = async () => { if (selectedIds.length === 0 || !steamId) return try { - const url = directAdd ? '/api/team/add-players' - : '/api/team/invite' + const url = directAdd ? '/api/team/add-players' : '/api/team/invite' const body = directAdd ? { teamId: team.id, steamIds: selectedIds } - : { teamId: team.id, userIds: selectedIds, invitedBy: steamId } - + : { teamId: team.id, userIds: selectedIds, invitedBy: steamId } + const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) - if (!res.ok) { const error = await res.json() console.error('Fehler beim Einladen:', error.message) } else { - setSentCount(selectedIds.length) // 👈 speichere Anzahl - setIsSuccess(true) // ✅ Erfolg markieren - setSelectedIds([]) // Optional: Selektion leeren - onSuccess() // ⚡ Nur Success-Callback, kein Schließen hier! + setSentCount(selectedIds.length) + setInvitedIds(selectedIds) // 👈 Einladungsliste speichern + setIsSuccess(true) + setSelectedIds([]) // ⛔ nicht zu früh löschen! + onSuccess() } } catch (err) { console.error('Fehler beim Einladen:', err) @@ -99,7 +98,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir window.HSOverlay.close(modalEl) } onClose() - }, 1500) + }, 2000) return () => clearTimeout(timeout) } @@ -118,8 +117,12 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir const bSelected = selectedIds.includes(b.steamId) ? -1 : 0 return aSelected - bSelected }) - - const unselectedUsers = filteredUsers.filter((user) => !selectedIds.includes(user.steamId)) + + const unselectedUsers = filteredUsers.filter((user) => + !selectedIds.includes(user.steamId) && + (!isSuccess || !invitedIds.includes(user.steamId)) + ) + const totalPages = Math.ceil(unselectedUsers.length / usersPerPage) const startIdx = (currentPage - 1) * usersPerPage const paginatedUsers = unselectedUsers.slice(startIdx, startIdx + usersPerPage) @@ -127,100 +130,41 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir return (

{directAdd - ? 'Wähle Benutzer aus, die du direkt zum Team hinzufügen möchtest:' - : 'Wähle Benutzer aus, die du in dein Team einladen möchtest:'} + ? 'Wähle Spieler aus, die du direkt zum Team hinzufügen möchtest:' + : 'Wähle Spieler aus, die du in dein Team einladen möchtest:'}

+ {/* Ausgewählte Benutzer anzeigen */} {selectedIds.length > 0 && ( - <> -
-

- Ausgewählte Mitglieder: -

-
- - {selectedIds.map((id) => { - const user = allUsers.find((u) => u.steamId === id) - if (!user) return null - return ( - - - - ) - })} - -
-
- - )} - setSearchTerm(e.target.value)} - className="mt-2 w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring focus:ring-blue-400 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-100" - /> - {isSuccess && ( -
- {directAdd - ? `${sentCount} Mitglied${sentCount === 1 ? '' : 'er'} hinzugefügt!` - : `${sentCount} Einladung${sentCount === 1 ? '' : 'en'} versendet!`} -
- )} -
- {isLoading ? ( - - ) : filteredUsers.length === 0 ? ( -
- {allUsers.length === 0 - ? directAdd - ? 'Keine Benutzer verfügbar :(' - : 'Niemand zum Einladen verfügbar :(' - : 'Keine Benutzer gefunden.'} -
- ) : ( - <> - - {paginatedUsers - .filter((user) => !selectedIds.includes(user.steamId)) - .map((user) => ( +
+

+ Ausgewählte Spieler: +

+
+ + {selectedIds.map((id) => { + const user = allUsers.find((u) => u.steamId === id) + if (!user) return null + return ( - ))} + ) + })} -
+
+
+ )} + + setSearchTerm(e.target.value)} + className="mt-2 w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring focus:ring-blue-400 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-100" + /> + + {isSuccess && ( +
+ {directAdd + ? `${sentCount} Mitglied${sentCount === 1 ? '' : 'er'} hinzugefügt!` + : `${sentCount} Einladung${sentCount === 1 ? '' : 'en'} versendet!`} +
+ )} + +
+ {isLoading ? ( + + ) : filteredUsers.length === 0 ? ( +
+ {allUsers.length === 0 + ? directAdd + ? 'Keine Benutzer verfügbar :(' + : 'Niemand zum Einladen verfügbar :(' + : 'Keine Benutzer gefunden.'} +
+ ) : ( + <> + + {!isSuccess && paginatedUsers.map((user) => ( + + + + ))} + {isSuccess && + invitedIds.map((id) => { + const user = allUsers.find((u) => u.steamId === id) + if (!user) return null + return ( + + + + ) + })} + + + { !isSuccess && ( +
setCurrentPage(page)} />
+ ) + } )}
diff --git a/src/app/components/LeaveTeamModal.tsx b/src/app/components/LeaveTeamModal.tsx index 04d2f67..db0bc9e 100644 --- a/src/app/components/LeaveTeamModal.tsx +++ b/src/app/components/LeaveTeamModal.tsx @@ -5,7 +5,7 @@ import Modal from './Modal' import MiniCard from './MiniCard' import { useSession } from 'next-auth/react' import { Player, Team } from '../types/team' -import { useTeamManager } from '../hooks/useTeamManager' +import { leaveTeam } from '../lib/team-actions' type Props = { show: boolean @@ -20,7 +20,6 @@ export default function LeaveTeamModal({ show, onClose, onSuccess, team }: Props const [newLeaderId, setNewLeaderId] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) - const { leaveTeam } = useTeamManager({}, null) useEffect(() => { if (show && team.leader) { diff --git a/src/app/components/MatchDetails.tsx b/src/app/components/MatchDetails.tsx index abae46f..9d6294d 100644 --- a/src/app/components/MatchDetails.tsx +++ b/src/app/components/MatchDetails.tsx @@ -85,16 +85,16 @@ export function MatchDetails ({ match }: { match: Match }) { {sorted.map(p => ( router.push(`/profile/${p.user.steamId}`)} > - + router.push(`/profile/${p.user.steamId}`)} > {p.user.name} - {p.user.name ?? 'Unbekannt'} +
+ {p.user.name ?? 'Unbekannt'} +
diff --git a/src/app/components/MiniCard.tsx b/src/app/components/MiniCard.tsx index 78377e8..ab949c6 100644 --- a/src/app/components/MiniCard.tsx +++ b/src/app/components/MiniCard.tsx @@ -1,8 +1,11 @@ +// MiniCard.tsx 'use client' import Button from './Button' import Image from 'next/image' import PremierRankBadge from './PremierRankBadge' +import { revokeInvitation } from '../lib/team-actions' +import { motion, AnimatePresence } from 'framer-motion' type MiniCardProps = { title: string @@ -24,6 +27,9 @@ type MiniCardProps = { hideOverlay?: boolean isSelectable?: boolean isAdmin?: boolean + message?: string + isInvite?: boolean + invitationId?: string } export default function MiniCard({ @@ -46,6 +52,9 @@ export default function MiniCard({ hideOverlay = false, isSelectable = true, isAdmin = false, + message, + isInvite = false, + invitationId }: MiniCardProps) { //const isSelectable = typeof onSelect === 'function' const canEdit = (isAdmin || currentUserSteamId === teamLeaderSteamId) && steamId !== teamLeaderSteamId @@ -65,6 +74,14 @@ export default function MiniCard({ if (isSelectable) onSelect?.(steamId) } + const handleRevokeClick = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + if (invitationId) { + revokeInvitation(invitationId) + } + } + const handleKickClick = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() @@ -84,12 +101,12 @@ export default function MiniCard({ return (
{canEdit && !hideActions && !hideOverlay && ( -
{title}
-
{typeof onPromote === 'function' && (
@@ -138,6 +155,21 @@ export default function MiniCard({ 🌐 )} */ } + + {message && ( + + + {message} + + + )}
) diff --git a/src/app/components/MiniCardDummy.tsx b/src/app/components/MiniCardDummy.tsx index 749e1db..41c9591 100644 --- a/src/app/components/MiniCardDummy.tsx +++ b/src/app/components/MiniCardDummy.tsx @@ -9,7 +9,7 @@ export default function MiniCardDummy({ title, onClick, children }: MiniCardDumm
{/* Body */} -
{children}
+
{children}
{/* Footer */}
diff --git a/src/app/components/Sidebar.tsx b/src/app/components/Sidebar.tsx index b9284e3..f4051c3 100644 --- a/src/app/components/Sidebar.tsx +++ b/src/app/components/Sidebar.tsx @@ -168,7 +168,7 @@ export default function Sidebar({ children }: { children?: React.ReactNode }) {
-
+
{children}
diff --git a/src/app/components/SortableMiniCard.tsx b/src/app/components/SortableMiniCard.tsx index 61136a3..e9cedff 100644 --- a/src/app/components/SortableMiniCard.tsx +++ b/src/app/components/SortableMiniCard.tsx @@ -1,3 +1,4 @@ +// SortableMiniCard.tsx 'use client' import { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable' diff --git a/src/app/components/Table.tsx b/src/app/components/Table.tsx index 2c844c4..ec33d33 100644 --- a/src/app/components/Table.tsx +++ b/src/app/components/Table.tsx @@ -48,17 +48,21 @@ function Row({ function Cell({ children, as: Component = 'td', + hoverable = false, className = '', + ...rest }: { children?: ReactNode as?: 'td' | 'th' + hoverable?: boolean className?: string -}) { +} & React.HTMLAttributes) { + const hoverClass = hoverable ? 'hover:bg-gray-100 dark:hover:bg-neutral-700 cursor-pointer' : '' const baseClass = Component === 'th' - ? 'px-6 py-3 text-start text-xs font-medium text-gray-500 uppercase dark:text-neutral-400' - : 'px-6 py-4 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200' - return {children} + ? 'px-6 py-3 text-start font-medium text-xs text-gray-500 uppercase dark:text-neutral-400' + : 'px-6 py-3 whitespace-nowrap text-sm text-gray-800 dark:text-neutral-200' + return {children} } // 📦 Zusammensetzen: diff --git a/src/app/components/TeamCardComponent.tsx b/src/app/components/TeamCardComponent.tsx index 230c992..b1784f7 100644 --- a/src/app/components/TeamCardComponent.tsx +++ b/src/app/components/TeamCardComponent.tsx @@ -1,15 +1,21 @@ 'use client' -import { forwardRef, useState } from 'react' +import { forwardRef, useEffect, useState } from 'react' import { useSession } from 'next-auth/react' -import { useTeamManager } from '../hooks/useTeamManager' import TeamInvitationView from './TeamInvitationView' import TeamMemberView from './TeamMemberView' import NoTeamView from './NoTeamView' import LoadingSpinner from '@/app/components/LoadingSpinner' import CreateTeamButton from './CreateTeamButton' +import { + acceptInvitation, + rejectInvitation, + markOneAsRead +} from '@/app/lib/team-actions' +import { Player, Team } from '../types/team' + type Props = { refetchKey?: string } @@ -18,41 +24,86 @@ type Props = { function TeamCardComponent(props: Props, ref: any) { const { data: session } = useSession() const steamId = session?.user?.steamId ?? '' - const [refetchKey, setRefetchKey] = useState() + const [isLoading, setIsLoading] = useState(true) - const teamManager = useTeamManager({ ...props, refetchKey }, ref) + const [team, setTeam] = useState(null) + const [activePlayers, setActivePlayers] = useState([]) + const [inactivePlayers, setInactivePlayers] = useState([]) + const [invitedPlayers, setInvitedPlayers] = useState([]) + const [pendingInvitation, setPendingInvitation] = useState(null) + const [activeDragItem, setActiveDragItem] = useState(null) + const [isDragging, setIsDragging] = useState(false) + const [showLeaveModal, setShowLeaveModal] = useState(false) + const [showInviteModal, setShowInviteModal] = useState(false) - // 1. Loading - if (teamManager.isLoading) return + const loadTeam = async () => { + setIsLoading(true) + try { + const res = await fetch('/api/team') + const data = await res.json() - // 2. Pending invitation - if ( - !teamManager.team && - teamManager.pendingInvitation && - teamManager.pendingInvitation.type === 'team-invite' - ) { - const notificationId = teamManager.pendingInvitation.id + if (data.team) { + setTeam(data.team) + setActivePlayers(data.team.activePlayers || []) + setInactivePlayers(data.team.inactivePlayers || []) + setInvitedPlayers(data.team.invitedPlayers || []) + } else { + setTeam(null) + } + + // Einladung nur laden, wenn kein Team vorhanden + if (!data.team) { + const inviteRes = await fetch('/api/user/invitations') + if (inviteRes.ok) { + const inviteData = await inviteRes.json() + const teamInvite = inviteData.invitations?.find( + (i: any) => i.type === 'team-invite' + ) + setPendingInvitation(teamInvite ?? null) + } + } + } catch (err) { + console.error('Fehler beim Laden des Teams:', err) + } finally { + setIsLoading(false) + } + } + + const handleMarkOneAsRead = async (id: string): Promise => { + await markOneAsRead(id) + } + + useEffect(() => { + loadTeam() + }, [refetchKey]) + + if (isLoading) return + + // 1. Pending invitation + if (!team && pendingInvitation) { + const notificationId = pendingInvitation.id return ( { if (action === 'accept') { - await teamManager.acceptInvitation(invitationId) + await acceptInvitation(invitationId) } else { - await teamManager.rejectInvitation(invitationId) + await rejectInvitation(invitationId) } - await teamManager.markOneAsRead(notificationId) + await markOneAsRead(notificationId) + await loadTeam() }} /> ) } - // 3. Kein Team → Hinweis + CreateTeamButton - if (!teamManager.team) { + // 2. Kein Team + if (!team) { return (
@@ -63,7 +114,7 @@ function TeamCardComponent(props: Props, ref: any) { ) } - // 4. Team vorhanden → Member view + // 3. Team vorhanden return (
@@ -77,9 +128,21 @@ function TeamCardComponent(props: Props, ref: any) {
diff --git a/src/app/components/TeamMemberView.tsx b/src/app/components/TeamMemberView.tsx index f3c317a..1d039cd 100644 --- a/src/app/components/TeamMemberView.tsx +++ b/src/app/components/TeamMemberView.tsx @@ -1,3 +1,4 @@ +// TeamMemberView.tsx 'use client' import { useEffect, useState } from 'react' @@ -14,7 +15,12 @@ import { Player, Team } from '../types/team' import { useSession } from 'next-auth/react' import { useSSE } from '@/app/lib/useSSEStore' import { AnimatePresence, motion } from 'framer-motion' -import { useTeamManager } from '../hooks/useTeamManager' +import { + leaveTeam, + reloadTeam, + renameTeam, + revokeInvitation, +} from '@/app/lib/team-actions' import Button from './Button' import Image from 'next/image' import TeamPremierRankBadge from './TeamPremierRankBadge' @@ -38,6 +44,8 @@ type Props = { adminMode?: boolean } +type InvitedPlayer = Player & { invitationId: string } + export default function TeamMemberView({ team, activePlayers, @@ -64,7 +72,7 @@ export default function TeamMemberView({ const canManage = adminMode || isLeader const canInvite = isLeader && !adminMode const canAddDirect = adminMode - const { leaveTeam, reloadTeam, renameTeam, deleteTeam } = useTeamManager({}, null) + //const { leaveTeam, reloadTeam, renameTeam, revokeInvitation } = useTeamManager({}, null) const [showRenameModal, setShowRenameModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const [isEditingName, setIsEditingName] = useState(false) @@ -74,6 +82,7 @@ export default function TeamMemberView({ const [logoFile, setLogoFile] = useState(null) const [teamState, setTeamState] = useState(team) const [saveSuccess, setSaveSuccess] = useState(false) + const [invitedPlayers, setInvitedPlayers] = useState([]) useEffect(() => { if (session?.user?.steamId) { @@ -85,6 +94,17 @@ export default function TeamMemberView({ setTeamState(team) }, [team]) + useEffect(() => { + if (team?.invitedPlayers?.length) { + const unique = Array.from( + new Map(team.invitedPlayers.map(p => [p.steamId, p])).values() + ) + setInvitedPlayers(unique.sort((a, b) => a.name.localeCompare(b.name))) + } else { + setInvitedPlayers([]) + } + }, [team]) + useEffect(() => { if (!source || !teamState?.id) return @@ -108,13 +128,13 @@ export default function TeamMemberView({ /* EIN Aufruf genügt – holt Team + Spieler + setzt States */ fetch(`/api/team/${encodeURIComponent(data.teamId)}`) .then(r => r.json()) - .then(fresh => { - setTeamState(fresh) - setactivePlayers((fresh.activePlayers ?? []) - .sort((a: Player, b: Player) => a.name.localeCompare(b.name))) - setInactivePlayers((fresh.inactivePlayers ?? []) - .sort((a: Player, b: Player) => a.name.localeCompare(b.name))) - }) + .then(({ team }) => { + if (!team) return + setTeamState(team) + setactivePlayers(team.activePlayers.sort((a: Player, b: Player) => a.name.localeCompare(b.name))) + setInactivePlayers(team.inactivePlayers.sort((a: Player, b: Player) => a.name.localeCompare(b.name))) + setInvitedPlayers(team.invitedPlayers.sort((a: Player, b: Player) => a.name.localeCompare(b.name))) + }) } catch (err) { console.error('SSE parse error:', err) } @@ -140,7 +160,6 @@ export default function TeamMemberView({ } }, [source, teamState?.id, reloadTeam]) - const handleDragStart = (event: any) => { const id = event.active.id const item = activePlayers.find(p => p.steamId === id) || inactivePlayers.find(p => p.steamId === id) @@ -210,6 +229,17 @@ export default function TeamMemberView({ setTimeout(() => setSaveSuccess(false), 3000) // 3 Sekunden sichtbar } + const handleReload = async () => { + if (!teamState?.id) return + const updated = await reloadTeam(teamState.id) + if (!updated) return + + setTeamState(updated) + setactivePlayers(updated.activePlayers) + setInactivePlayers(updated.inactivePlayers) + setInvitedPlayers(updated.invitedPlayers) + } + const confirmKick = async () => { if (!kickCandidate) return @@ -243,7 +273,7 @@ export default function TeamMemberView({ return } - await reloadTeam() + await handleReload() } catch (err) { console.error('Fehler bei Leader-Übertragung:', err) } @@ -252,7 +282,7 @@ export default function TeamMemberView({ if (!teamState) return null if (!adminMode && !currentUserSteamId) return null - const manageSteam = adminMode ? teamState.leader : currentUserSteamId + const manageSteam: string = adminMode ? teamState.leader ?? '' : currentUserSteamId const renderMemberList = (players: Player[]) => ( @@ -349,7 +379,7 @@ export default function TeamMemberView({ }) if (res.ok) { - await reloadTeam() + await handleReload() } else { alert('Fehler beim Hochladen des Logos.') } @@ -379,7 +409,7 @@ export default function TeamMemberView({ await renameTeam(teamState.id, editedName) setTeamState((prev) => prev ? { ...prev, teamname: editedName } : prev) setIsEditingName(false) - await reloadTeam() + await handleReload() }} className="h-[34px] px-3 flex items-center justify-center" > @@ -491,17 +521,17 @@ export default function TeamMemberView({
- + p.steamId)} strategy={verticalListSortingStrategy}> {renderMemberList(activePlayers)} - + p.steamId)} strategy={verticalListSortingStrategy}> {renderMemberList(inactivePlayers)} {canManage && ( - + { @@ -519,6 +549,46 @@ export default function TeamMemberView({ )} + + {invitedPlayers.length > 0 && ( +
+
+

Eingeladene Spieler

+
+
+
+ + {invitedPlayers.map((player: Player) => ( + + {}} + draggable={false} + currentUserSteamId={currentUserSteamId} + teamLeaderSteamId={teamState.leader} + isSelectable={false} + isInvite={true} + rank={player.premierRank} + onKick={revokeInvitation} + invitationId={player.invitationId} + /> + + ))} + +
+
+
+ )}
diff --git a/src/app/hooks/useTeamManager.tsx b/src/app/hooks/useTeamManager.tsx index 66f6f7e..a6545f8 100644 --- a/src/app/hooks/useTeamManager.tsx +++ b/src/app/hooks/useTeamManager.tsx @@ -99,6 +99,7 @@ export function useTeamManager( logo: teamData.logo, activePlayers : newActive, inactivePlayers: newInactive, + invitedPlayers : teamData.invitedPlayers ?? [], }) setactivePlayers(newActive) setInactivePlayers(newInactive) @@ -195,6 +196,22 @@ export function useTeamManager( } } + const revokeInvitation = async (invitationId: string) => { + const id = invitationId || pendingInvitation?.id + if (!id) return + try { + await fetch('/api/user/invitations/revoke', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ invitationId: id }), + }) + if (pendingInvitation?.id === id) setPendingInvitation(null) + await fetchTeam() + } catch (error) { + console.error('Fehler beim Annehmen der Einladung:', error) + } + } + const markAllAsRead = async () => { try { await fetch('/api/notifications/mark-all-read', { method: 'POST' }) @@ -301,6 +318,7 @@ export function useTeamManager( setInactivePlayers, acceptInvitation, rejectInvitation, + revokeInvitation, markAllAsRead, markOneAsRead, handleInviteAction, diff --git a/src/app/lib/team-actions.ts b/src/app/lib/team-actions.ts new file mode 100644 index 0000000..3526da9 --- /dev/null +++ b/src/app/lib/team-actions.ts @@ -0,0 +1,202 @@ +// lib/team-actions.ts + +import { Player, Team } from '../types/team' + +// 🔄 Team laden +export async function reloadTeam(teamId: string): Promise { + try { + const res = await fetch(`/api/team/${encodeURIComponent(teamId)}`) + if (!res.ok) throw new Error('Fehler beim Abrufen des Teams') + + const data = await res.json() + const team = data.team ?? data + if (!team) return null + + const sortByName = (players: Player[]) => + players.sort((a, b) => a.name.localeCompare(b.name)) + + return { + id: team.id, + name: team.name, + logo: team.logo, + leader: team.leader, + activePlayers: sortByName(team.activePlayers ?? []), + inactivePlayers: sortByName(team.inactivePlayers ?? []), + invitedPlayers: sortByName(team.invitedPlayers ?? []), + } + } catch (error) { + console.error('reloadTeam:', error) + return null + } +} + +// ✅ Einladung annehmen +export async function acceptInvitation(invitationId: string): Promise { + try { + const res = await fetch('/api/user/invitations/accept', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ invitationId }), + }) + return res.ok + } catch (error) { + console.error('acceptInvitation:', error) + return false + } +} + +// ❌ Einladung ablehnen +export async function rejectInvitation(invitationId: string): Promise { + try { + const res = await fetch('/api/user/invitations/reject', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ invitationId }), + }) + return res.ok + } catch (error) { + console.error('rejectInvitation:', error) + return false + } +} + +// 📩 Einladung zurückziehen +export async function revokeInvitation(invitationId: string): Promise { + try { + const res = await fetch('/api/user/invitations/revoke', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ invitationId }), + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.message || 'Fehler beim Zurückziehen der Einladung') + } + + return true + } catch (err) { + console.error('revokeInvitation:', err) + return false + } +} + +// ✏️ Team umbenennen +export async function renameTeam(teamId: string, newName: string): Promise { + try { + const res = await fetch('/api/team/rename', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ teamId, newName }), + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.message || 'Fehler beim Umbenennen') + } + + return true + } catch (err) { + console.error('renameTeam:', err) + return false + } +} + +// 🚪 Team verlassen +export async function leaveTeam(steamId: string, newLeaderId?: string): Promise { + try { + const body = newLeaderId ? { steamId, newLeaderId } : { steamId } + + const res = await fetch('/api/team/leave', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.message || 'Fehler beim Verlassen') + } + + return true + } catch (err) { + console.error('leaveTeam:', err) + return false + } +} + +// 👢 Spieler kicken +export async function kickPlayer(teamId: string, steamId: string): Promise { + try { + const res = await fetch('/api/team/kick', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ teamId, steamId }), + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.message || 'Fehler beim Kicken') + } + + return true + } catch (err) { + console.error('kickPlayer:', err) + return false + } +} + +// 👑 Leader übertragen +export async function transferLeader(teamId: string, newLeaderSteamId: string): Promise { + try { + const res = await fetch('/api/team/transfer-leader', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ teamId, newLeaderSteamId }), + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.message || 'Fehler beim Übertragen der Leader-Rolle') + } + + return true + } catch (err) { + console.error('transferLeader:', err) + return false + } +} + +// 🗑️ Team löschen +export async function deleteTeam(teamId: string): Promise { + try { + const res = await fetch('/api/team/delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ teamId }), + }) + + if (!res.ok) { + const data = await res.json() + throw new Error(data.message || 'Fehler beim Löschen') + } + + return true + } catch (err) { + console.error('deleteTeam:', err) + return false + } +} + +// ✅ Einzelne Notification als gelesen markieren +export async function markOneAsRead(id: string): Promise { + try { + const res = await fetch(`/api/notifications/mark-read/${id}`, { + method: 'POST', + }) + return res.ok + } catch (err) { + console.error(`Fehler beim Markieren von Benachrichtigung ${id}:`, err) + return false + } +} diff --git a/src/app/types/team.ts b/src/app/types/team.ts index 66c2d8f..fb2299c 100644 --- a/src/app/types/team.ts +++ b/src/app/types/team.ts @@ -1,19 +1,24 @@ // /types/team.ts export type Player = { - steamId : string - name : string - avatar : string - location? : string + steamId: string + name: string + avatar: string + location?: string premierRank?: number - isAdmin? : boolean + isAdmin?: boolean } +export type InvitedPlayer = Player & { + invitationId: string +} + + export type Team = { - id : string + id: string name?: string | null logo?: string | null leader?: string | null - activePlayers? : Player[] - inactivePlayers?: Player[] - invitedPlayers?: Player[] + activePlayers: Player[] + inactivePlayers: Player[] + invitedPlayers: InvitedPlayer[] }