211 lines
7.0 KiB
TypeScript
211 lines
7.0 KiB
TypeScript
// TeamInvitationBanner.tsx
|
|
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import Button from './Button'
|
|
import TeamPremierRankBadge from './TeamPremierRankBadge'
|
|
import type { Invitation } from '../types/invitation'
|
|
import type { Team } from '../types/team'
|
|
|
|
type Props = {
|
|
invitation: Invitation
|
|
notificationId: string
|
|
onAction: (action: 'accept' | 'reject', invitationId: string) => Promise<void>
|
|
onMarkAsRead: (id: string) => Promise<void>
|
|
}
|
|
|
|
export default function TeamInvitationBanner({
|
|
invitation,
|
|
notificationId,
|
|
onAction,
|
|
onMarkAsRead,
|
|
}: Props) {
|
|
const router = useRouter()
|
|
const [isSubmitting, setIsSubmitting] = useState<'accept' | 'reject' | null>(null)
|
|
|
|
if (!invitation?.team) return null
|
|
const team: Team = invitation.team
|
|
const targetHref = `/team/${team.id}`
|
|
|
|
const active = team.activePlayers ?? []
|
|
const inactive = team.inactivePlayers ?? []
|
|
const badgePlayers = active.length ? active : inactive
|
|
|
|
const handleRespond = async (action: 'accept' | 'reject') => {
|
|
if (isSubmitting) return
|
|
try {
|
|
setIsSubmitting(action)
|
|
await onAction(action, invitation.id)
|
|
await onMarkAsRead(notificationId)
|
|
} catch (err) {
|
|
console.error(`[TeamInvitationView] ${action} failed:`, err)
|
|
} finally {
|
|
setIsSubmitting(null)
|
|
}
|
|
}
|
|
|
|
// Klassen als Ausdruck (keine mehrzeiligen String-Literals im JSX)
|
|
const cardClasses =
|
|
'relative overflow-hidden rounded-xl border bg-white/90 dark:bg-neutral-800/90 ' +
|
|
'dark:border-neutral-700 shadow-sm hover:shadow-md transition cursor-pointer ' +
|
|
'focus:outline-none ring-1 ring-green-500/15 hover:ring-green-500/25 mb-5';
|
|
|
|
return (
|
|
<div
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={() => router.push(targetHref)}
|
|
onKeyDown={(e) => e.key === 'Enter' && router.push(targetHref)}
|
|
className={cardClasses}
|
|
>
|
|
{/* animierter, dezenter grüner Gradient */}
|
|
<div aria-hidden className="absolute inset-0 z-0 pointer-events-none invitationGradient" />
|
|
|
|
{/* Inhalt */}
|
|
<div className="relative z-[1] p-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<img
|
|
src={team.logo ? `/assets/img/logos/${team.logo}` : `/assets/img/logos/cs2.webp`}
|
|
alt={team.name ?? 'Teamlogo'}
|
|
className="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-neutral-600"
|
|
/>
|
|
<div className="flex flex-col">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium truncate text-gray-800 dark:text-neutral-200">
|
|
{team.name ?? 'Team'}
|
|
</span>
|
|
{badgePlayers.length > 0 && <TeamPremierRankBadge players={badgePlayers} />}
|
|
</div>
|
|
<span className="text-xs text-gray-600 dark:text-neutral-400">
|
|
Du wurdest in dieses Team eingeladen.
|
|
</span>
|
|
</div>
|
|
|
|
{/* Teammitglieder */}
|
|
<div className="flex -space-x-3">
|
|
{[...(team.activePlayers ?? []), ...(team.inactivePlayers ?? [])].map((p) => (
|
|
<img
|
|
key={p.steamId}
|
|
src={p.avatar}
|
|
alt={p.name}
|
|
title={p.name}
|
|
className="w-12 h-12 rounded-full border-2 border-white dark:border-neutral-800 object-cover"
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
title="Ablehnen"
|
|
size="sm"
|
|
color="red"
|
|
variant="ghost"
|
|
disabled={isSubmitting !== null}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
handleRespond('reject')
|
|
}}
|
|
>
|
|
{isSubmitting === 'reject' ? (
|
|
<>
|
|
<span className="animate-spin inline-block size-4 border-[3px] border-current border-t-transparent rounded-full mr-1" />
|
|
Ablehnen…
|
|
</>
|
|
) : (
|
|
'Ablehnen'
|
|
)}
|
|
</Button>
|
|
|
|
<Button
|
|
title="Annehmen"
|
|
size="sm"
|
|
color="green"
|
|
variant="solid"
|
|
disabled={isSubmitting !== null}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
handleRespond('accept')
|
|
}}
|
|
>
|
|
{isSubmitting === 'accept' ? (
|
|
<>
|
|
<span className="animate-spin inline-block size-4 border-[3px] border-current border-t-transparent rounded-full mr-1" />
|
|
Annehmen…
|
|
</>
|
|
) : (
|
|
'Annehmen'
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style jsx>{`
|
|
/* Hintergrund-Schimmer (läuft permanent) */
|
|
@keyframes slide-x {
|
|
from { background-position-x: 0%; }
|
|
to { background-position-x: 200%; }
|
|
}
|
|
.invitationGradient {
|
|
background-image: repeating-linear-gradient(
|
|
90deg,
|
|
rgba(16,168,54,0.20) 0%,
|
|
rgba(16,168,54,0.04) 50%,
|
|
rgba(16,168,54,0.20) 100%
|
|
);
|
|
background-size: 200% 100%;
|
|
background-repeat: repeat-x;
|
|
animation: slide-x 6s linear infinite; /* etwas ruhiger */
|
|
}
|
|
:global(.dark) .invitationGradient {
|
|
background-image: repeating-linear-gradient(
|
|
90deg,
|
|
rgba(16,168,54,0.28) 0%,
|
|
rgba(16,168,54,0.08) 50%,
|
|
rgba(16,168,54,0.28) 100%
|
|
);
|
|
}
|
|
|
|
/* Shine-Sweep nur auf Hover */
|
|
@keyframes shine {
|
|
0% { transform: translateX(-120%) skewX(-20deg); opacity: 0; }
|
|
10% { opacity: .7; }
|
|
27% { transform: translateX(120%) skewX(-20deg); opacity: 0; }
|
|
100% { transform: translateX(120%) skewX(-20deg); opacity: 0; }
|
|
}
|
|
.shine {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
.shine::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: -25%;
|
|
bottom: -25%;
|
|
left: -20%;
|
|
width: 35%;
|
|
pointer-events: none;
|
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
|
|
filter: blur(2px);
|
|
opacity: 0;
|
|
transform: translateX(-120%) skewX(-20deg);
|
|
transition: opacity .2s;
|
|
}
|
|
/* nur wenn die Karte offen ist und gehovert wird */
|
|
:global(.group:hover) .shine::before {
|
|
animation: shine 3.8s ease-out infinite;
|
|
}
|
|
|
|
/* Respektiere Bewegungs-Präferenzen */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.invitationGradient { animation: none; }
|
|
.shine::before { animation: none !important; transform: none !important; opacity: 0 !important; }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
)
|
|
}
|