This commit is contained in:
Linrador 2025-08-18 15:29:09 +02:00
parent dfc3e6bf99
commit 1b5cd58f7f
14 changed files with 509 additions and 532 deletions

View File

@ -19,6 +19,8 @@ const ACTION_MAP: Record<MapVoteAction, 'ban'|'pick'|'decider'> = {
/* -------------------- Helper -------------------- */
const sleep = (ms: number) => new Promise<void>(res => setTimeout(res, ms));
// Admin-Edit-Flag setzen/zurücksetzen
async function setAdminEdit(voteId: string, by: string | null) {
return prisma.mapVote.update({
@ -497,6 +499,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
// match-ready senden (erste Map + Teilnehmer)
if (updated?.locked) {
await sleep(3000);
const chosen = deriveChosenSteps(updated)
const first = chosen[0]
const key = first?.map ?? null
@ -550,6 +553,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
// match-ready senden
if (updated?.locked) {
await sleep(3000);
const chosen = deriveChosenSteps(updated)
const first = chosen[0]
const key = first?.map ?? null
@ -642,6 +646,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
// Falls durch diesen Schritt locked wurde → Export & match-ready
if (updated?.locked) {
await sleep(3000);
const chosen = deriveChosenSteps(updated)
const first = chosen[0]
const key = first?.map ?? null

View File

@ -168,12 +168,12 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
>
{loading && (
<span
className="animate-spin inline-block size-4 border-[3px] border-current border-t-transparent rounded-full"
className="animate-spin inline-block size-4 border-[3px] border-current border-t-transparent rounded-full mr-2"
role="status"
aria-label="loading"
></span>
)}
{loading ? 'Lädt' : (children ?? title)}
{children ?? title}
</button>
)
})

View File

@ -267,32 +267,18 @@ export default function CommunityMatchList({ matchType }: Props) {
${dimmed ? 'opacity-40' : 'opacity-100'}
`}
>
{isLive && (
<span className="absolute top-2 px-2 py-0.5 text-xs font-semibold rounded-full bg-red-300 dark:bg-red-500 text-white">
{/* Live / Map-Vote Badge */}
{isLive ? (
<span className="absolute top-2 px-2 py-0.5 text-xs font-semibold rounded-full bg-red-500 text-white shadow">
LIVE
</span>
)}
{/* Map-Vote Badge */}
{m.mapVote && (
<span
className={`
px-2 py-0.5 rounded-full text-[11px] font-semibold
${m.mapVote.isOpen ? 'bg-green-300 dark:bg-green-600 text-white' : 'bg-neutral-200 dark:bg-neutral-700'}
`}
title={
m.mapVote.opensAt
? `Öffnet ${format(new Date(m.mapVote.opensAt), 'dd.MM.yyyy HH:mm', { locale: de })} Uhr`
: undefined
}
>
{m.mapVote.isOpen
? (m.mapVote.status === 'completed' ? 'Map-Vote abgeschlossen' : 'Map-Vote offen')
: m.mapVote.opensAt
? `Map-Vote ab ${format(new Date(m.mapVote.opensAt), 'HH:mm', { locale: de })} Uhr`
: 'Map-Vote bald'}
) : (m.mapVote?.isOpen ? (
<span className="absolute top-2 px-2 py-0.5 text-[11px] font-semibold rounded-full bg-green-600 text-white shadow">
Map-Vote offen
</span>
)}
) : null
)
}
<div className="flex w-full justify-around items-center">
<div className="flex flex-col items-center w-1/3">
@ -306,12 +292,17 @@ export default function CommunityMatchList({ matchType }: Props) {
</div>
</div>
<div className="flex flex-col items-center space-y-1 mt-2">
<span className={`px-3 py-0.5 rounded-full text-sm font-semibold ${isLive ? 'bg-red-300 dark:bg-red-500' : 'bg-yellow-300 dark:bg-yellow-500'}`}>
{/* Datum + Uhrzeit: höher & highlight */}
<div className="flex flex-col items-center -mt-1 space-y-1">
<span
className={`px-3 py-1 rounded-full text-[13px] font-bold shadow ring-1 ring-black/10
${isLive ? 'bg-red-500 text-white' : 'bg-yellow-400 text-gray-900 dark:bg-yellow-500 dark:text-black'}
`}
>
{format(new Date(m.demoDate), 'dd.MM.yyyy', { locale: de })}
</span>
<span className="flex items-center gap-1 text-xs opacity-80">
<span className="flex items-center gap-1 text-xs font-semibold opacity-90">
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 512 512">
<path d="M256 48a208 208 0 1 0 208 208A208.24 208.24 0 0 0 256 48Zm0 384a176 176 0 1 1 176-176 176.2 176.2 0 0 1-176 176Zm80-176h-64V144a16 16 0 0 0-32 0v120a16 16 0 0 0 16 16h80a16 16 0 0 0 0-32Z" />
</svg>

View File

@ -1,11 +1,9 @@
// CompRankBadge.tsx
'use client';
import Image from 'next/image';
import Tooltip from './Tooltip';
type Props = {
rank: number | null;
};
type Props = { rank: number | null };
const rankNames: Record<number, string> = {
1: 'Silver I',
@ -32,7 +30,6 @@ const rankNames: Record<number, string> = {
export default function CompRankBadge({ rank }: Props) {
let imageName = 'skillgroup_none.webp';
let altText = 'No Rank';
if (typeof rank === 'number') {
if (rank >= 1 && rank <= 18) {
imageName = `skillgroup${rank}.webp`;
@ -50,8 +47,10 @@ export default function CompRankBadge({ rank }: Props) {
alt={altText}
width={60}
height={60}
sizes="(max-width: 768px) 100px, 70px"
style={{ objectFit: 'contain' }} // ← korrekt!
// Wichtig: feste Höhe für die Zeile, Breite auto
className="inline-block align-middle h-7 w-auto" // h-7 = 28px
sizes="70px"
style={{ objectFit: 'contain' }}
/>
</Tooltip>
);

View File

@ -27,6 +27,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
const [selectedIds, setSelectedIds] = useState<string[]>([])
const [invitedIds, setInvitedIds] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(false)
const [isInviting, setIsInviting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [sentCount, setSentCount] = useState(0)
const [searchTerm, setSearchTerm] = useState('')
@ -87,10 +88,12 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
}
const handleInvite = async () => {
if (isInviting) return
if (selectedIds.length === 0 || !steamId) return
const ids = [...selectedIds]
try {
setIsInviting(true)
const url = directAdd ? '/api/team/add-players' : '/api/team/invite'
const body = directAdd
? { teamId: team.id, steamIds: ids }
@ -135,6 +138,9 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
setSentCount(0)
setIsSuccess(true)
}
finally {
setIsInviting(false)
}
}
useEffect(() => {
@ -272,13 +278,18 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
title={directAdd ? 'Spieler hinzufügen' : 'Spieler einladen'}
show={show}
onClose={onClose}
onSave={handleInvite}
onSave={() => { if (!isInviting) handleInvite() }}
closeButtonColor={isSuccess ? 'teal' : 'blue'}
closeButtonTitle={
isSuccess
? directAdd ? 'Spieler hinzugefügt' : 'Einladungen versendet'
: directAdd ? 'Hinzufügen' : 'Einladungen senden'
? (directAdd ? 'Spieler hinzugefügt' : 'Einladungen versendet')
: (
isInviting
? (directAdd ? 'Wird hinzugefügt...' : 'Wird eingeladen...')
: (directAdd ? 'Hinzufügen' : 'Einladungen senden')
)
}
closeButtonLoading={isInviting}
scrollBody={true}
>
<p ref={descRef} className="text-sm text-gray-700 dark:text-neutral-300 mb-2">

View File

@ -88,11 +88,11 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
const gotoFullPage = () => router.push(`/match-details/${match.id}/vote`)
const cardClasses =
'relative overflow-hidden rounded-xl border bg-white/90 dark:bg-neutral-800/90 ' +
'dark:border-neutral-700 shadow-sm transition cursor-pointer focus:outline-none ' +
'group relative overflow-hidden rounded-xl border bg-white/90 dark:bg-neutral-800/90 ' +
'dark:border-neutral-700 shadow-sm cursor-pointer focus:outline-none transition ' +
(isOpen
? 'ring-1 ring-green-500/15 hover:ring-green-500/25 hover:shadow-md'
: 'ring-1 ring-neutral-500/10 hover:ring-neutral-500/20 hover:shadow-md')
? 'ring-1 ring-green-500/15 hover:ring-green-500/30 hover:shadow-lg'
: 'ring-1 ring-neutral-500/10 hover:ring-neutral-500/20 hover:shadow-md');
return (
<div
@ -103,11 +103,16 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
className={cardClasses}
aria-label="Map-Vote öffnen"
>
{isOpen && <div aria-hidden className="absolute inset-0 z-0 pointer-events-none mapVoteGradient" />}
{isOpen && (
<>
<div aria-hidden className="absolute inset-0 z-0 pointer-events-none mapVoteGradient" />
<span aria-hidden className="shine pointer-events-none" />
</>
)}
<div className="relative z-[1] px-4 py-3 flex items-center justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<div className="shrink-0 w-9 h-9 rounded-full grid place-items-center bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-200">
<div className="shrink-0 w-9 h-9 rounded-full grid place-items-center bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-200 transition-transform group-hover:scale-[1.03] group-hover:translate-x-[1px]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
<path d="M15 4.5 9 7.5l-6-3v15l6 3 6-3 6 3v-15l-6-3Zm-6 16.5-4-2V6l4 2v13Zm2-13 4-2v13l-4 2V8Z"/>
</svg>
@ -151,6 +156,7 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
</div>
<style jsx>{`
/* Hintergrund-Schimmer (läuft permanent) */
@keyframes slide-x {
from { background-position-x: 0%; }
to { background-position-x: 200%; }
@ -158,24 +164,57 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
.mapVoteGradient {
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%
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 3s linear infinite;
animation: slide-x 6s linear infinite; /* etwas ruhiger */
}
:global(.dark) .mapVoteGradient {
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%
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) {
.mapVoteGradient { animation: none; }
.shine::before { animation: none !important; transform: none !important; opacity: 0 !important; }
}
`}</style>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@ import { Team } from '../types/team'
import Alert from './Alert'
import Image from 'next/image'
import { MATCH_EVENTS } from '../lib/sseEvents'
import Link from 'next/link'
type TeamWithPlayers = Team & { players?: MatchPlayer[] }
@ -246,20 +247,37 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
/* ─── Render ─────────────────────────────────────────────── */
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">
Match auf {mapLabel} ({match.matchType})
</h1>
{/* Kopfzeile: Zurück + Admin-Buttons */}
<div className="flex items-center justify-between">
{/* Links: Zurück */}
<Link href="/schedule">
<Button color="gray" variant="outline">
Zurück
</Button>
</Link>
{/* Rechts: Admin-Buttons */}
{isAdmin && (
<div className="flex gap-2">
<Button onClick={() => setEditMetaOpen(true)} className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-md">
<Button
onClick={() => setEditMetaOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-md"
>
Match bearbeiten
</Button>
<Button onClick={handleDelete} className="bg-red-600 hover:bg-red-700 text-white px-3 py-1.5 rounded-md">
<Button
onClick={handleDelete}
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1.5 rounded-md"
>
Match löschen
</Button>
</div>
)}
</div>
<h1 className="text-2xl font-bold">
Match auf {mapLabel} ({match.matchType})
</h1>
<p className="text-sm text-gray-500">Datum: {readableDate}</p>
@ -280,20 +298,50 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
<div className="flex items-center justify-between mb-2">
<h2 className="text-xl font-semibold">{match.teamA?.name ?? 'Team A'}</h2>
{showEditA && (
<Alert type="soft" color="info" className="flex items-center justify-between gap-4">
<span>
Du kannst die Aufstellung noch bis <strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
<Alert type="soft" color="dark" className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
{showEditA ? (
<>
{/* Unlocked-Icon */}
<svg xmlns="http://www.w3.org/2000/svg" fill="green" height="20" width="20" viewBox="0 0 640 640">
<path d="M416 160C416 124.7 444.7 96 480 96C515.3 96 544 124.7 544 160L544 192C544 209.7 558.3 224 576 224C593.7 224 608 209.7 608 192L608 160C608 89.3 550.7 32 480 32C409.3 32 352 89.3 352 160L352 224L192 224C156.7 224 128 252.7 128 288L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 288C512 252.7 483.3 224 448 224L416 224L416 160z"/>
</svg>
</>
) : (
<>
{/* Locked-Icon */}
<svg xmlns="http://www.w3.org/2000/svg" fill="red" height="20" width="20" viewBox="0 0 640 640">
<path d="M256 160L256 224L384 224L384 160C384 124.7 355.3 96 320 96C284.7 96 256 124.7 256 160zM192 224L192 160C192 89.3 249.3 32 320 32C390.7 32 448 89.3 448 160L448 224C483.3 224 512 252.7 512 288L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 288C128 252.7 156.7 224 192 224z"/>
</svg>
</>
)
}
<span className='text-gray-300'>
{showEditA ? (
<>
Du kannst die Aufstellung noch bis{' '}
<strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
</>
) : (
<>Die Aufstellung kann nicht mehr bearbeitet werden.</>
)}
</span>
</div>
<Button
size="sm"
onClick={() => setEditSide('A')}
className="px-3 py-1.5 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 text-white"
onClick={() => showEditA && setEditSide('A')}
disabled={!showEditA}
className={`px-3 py-1.5 text-sm rounded-lg ${
showEditA
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-gray-400 text-gray-200 cursor-not-allowed'
}`}
>
Spieler bearbeiten
</Button>
</Alert>
)}
</div>
{renderTable(teamAPlayers)}
@ -322,20 +370,51 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
{match.teamB?.name ?? 'Team B'}
</h2>
{showEditB && (
<Alert type="soft" color="info" className="flex items-center justify-between gap-4">
<span>
Du kannst die Aufstellung noch bis <strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
<Alert type="soft" color="dark" className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
{showEditB ? (
<>
{/* Unlocked-Icon */}
<svg xmlns="http://www.w3.org/2000/svg" fill="green" height="20" width="20" viewBox="0 0 640 640">
<path d="M416 160C416 124.7 444.7 96 480 96C515.3 96 544 124.7 544 160L544 192C544 209.7 558.3 224 576 224C593.7 224 608 209.7 608 192L608 160C608 89.3 550.7 32 480 32C409.3 32 352 89.3 352 160L352 224L192 224C156.7 224 128 252.7 128 288L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 288C512 252.7 483.3 224 448 224L416 224L416 160z"/>
</svg>
</>
) : (
<>
{/* Locked-Icon */}
<svg xmlns="http://www.w3.org/2000/svg" fill="red" height="20" width="20" viewBox="0 0 640 640">
<path d="M256 160L256 224L384 224L384 160C384 124.7 355.3 96 320 96C284.7 96 256 124.7 256 160zM192 224L192 160C192 89.3 249.3 32 320 32C390.7 32 448 89.3 448 160L448 224C483.3 224 512 252.7 512 288L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 288C128 252.7 156.7 224 192 224z"/>
</svg>
</>
)
}
<span className='text-gray-300'>
{showEditB ? (
<>
Du kannst die Aufstellung noch bis{' '}
<strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
</>
) : (
<>Die Aufstellung kann nicht mehr bearbeitet werden.</>
)}
</span>
</div>
<Button
size="sm"
onClick={() => setEditSide('B')}
className="px-3 py-1.5 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 text-white"
onClick={() => showEditB && setEditSide('B')}
disabled={!showEditB}
className={`px-3 py-1.5 text-sm rounded-lg ${
showEditB
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-gray-400 text-gray-200 cursor-not-allowed'
}`}
>
Spieler bearbeiten
</Button>
</Alert>
)}
</div>
{renderTable(teamBPlayers)}

View File

@ -1,6 +1,7 @@
'use client'
import { useEffect } from 'react'
import Button from './Button'
type Width =
| 'sm:max-w-sm'
@ -20,6 +21,7 @@ type ModalProps = {
hideCloseButton?: boolean
closeButtonColor?: 'blue' | 'red' | 'green' | 'teal'
closeButtonTitle?: string
closeButtonLoading?: boolean
disableSave?: boolean
maxWidth?: Width
/** Wenn false, wird der Body nicht gescrollt (wir paginieren stattdessen) */
@ -119,8 +121,8 @@ export default function Modal({
</h3>
{!hideCloseButton && (
<button
type="button"
<Button
size='sm'
aria-label="Close"
data-hs-overlay={`#${id}`}
onClick={onClose}
@ -139,7 +141,7 @@ export default function Modal({
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</Button>
)}
</div>
@ -156,25 +158,25 @@ export default function Modal({
{/* Footer (fixe Höhe) */}
<div className="flex justify-end items-center gap-x-2 py-3 px-4 border-t border-gray-200 dark:border-neutral-700">
{!hideCloseButton && (
<button
type="button"
<Button
size='sm'
data-hs-overlay={`#${id}`}
onClick={onClose}
className="py-2 px-3 text-sm font-medium rounded-lg border border-gray-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-gray-800 dark:text-white shadow-2xs hover:bg-gray-50 dark:hover:bg-neutral-700"
>
Schließen
</button>
</Button>
)}
{onSave && (
<button
type="button"
<Button
size='sm'
onClick={onSave}
disabled={disableSave}
className={`py-2 px-3 text-sm font-medium rounded-lg border border-transparent bg-${closeButtonColor}-600 hover:bg-${closeButtonColor}-700 focus:bg-${closeButtonColor}-700 text-white`}
>
{closeButtonTitle}
</button>
</Button>
)}
</div>
</div>

View File

@ -70,7 +70,7 @@ function Cell({
const baseClass =
Component === 'th'
? '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'
: 'px-6 py-3 text-sm text-gray-800 dark:text-neutral-200'
return (
<Component {...rest} className={`${baseClass} ${hoverClass} ${className}`}>
{children}

View File

@ -144,36 +144,65 @@ export default function TeamInvitationBanner({
</div>
<style jsx>{`
/* kontinuierlich nach rechts schieben */
/* Hintergrund-Schimmer (läuft permanent) */
@keyframes slide-x {
from { background-position-x: 0%; }
to { background-position-x: 200%; }
}
.invitationGradient {
/* weicher, dezenter Verlauf */
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%
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 3s linear infinite;
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%
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>

View File

@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import TeamCardComponent from "../components/TeamCardComponent";
import Card from "../components/Card";
import LoadingSpinner from "../components/LoadingSpinner";
type TeamsResponse = { teams?: any[] } | undefined;
type InvitesResponse = { invitations?: any[] } | undefined;
@ -57,7 +58,11 @@ export default function TeamPageClient() {
}, []);
if (loading) {
return <p>Lade Teams </p>;
return (
<p>
<LoadingSpinner />
</p>
);
}
return (

View File

@ -346,7 +346,7 @@ const config = {
"value": "prisma-client-js"
},
"output": {
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
"fromEnvVar": null
},
"config": {
@ -360,7 +360,7 @@ const config = {
}
],
"previewFeatures": [],
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"isCustomOutput": true
},
"relativeEnvPaths": {
@ -374,6 +374,7 @@ const config = {
"db"
],
"activeProvider": "postgresql",
"postinstall": false,
"inlineDatasources": {
"db": {
"url": {

View File

@ -347,7 +347,7 @@ const config = {
"value": "prisma-client-js"
},
"output": {
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
"fromEnvVar": null
},
"config": {
@ -361,7 +361,7 @@ const config = {
}
],
"previewFeatures": [],
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
"isCustomOutput": true
},
"relativeEnvPaths": {
@ -375,6 +375,7 @@ const config = {
"db"
],
"activeProvider": "postgresql",
"postinstall": false,
"inlineDatasources": {
"db": {
"url": {