updated
This commit is contained in:
parent
dfc3e6bf99
commit
1b5cd58f7f
@ -19,6 +19,8 @@ const ACTION_MAP: Record<MapVoteAction, 'ban'|'pick'|'decider'> = {
|
|||||||
|
|
||||||
/* -------------------- Helper -------------------- */
|
/* -------------------- Helper -------------------- */
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise<void>(res => setTimeout(res, ms));
|
||||||
|
|
||||||
// Admin-Edit-Flag setzen/zurücksetzen
|
// Admin-Edit-Flag setzen/zurücksetzen
|
||||||
async function setAdminEdit(voteId: string, by: string | null) {
|
async function setAdminEdit(voteId: string, by: string | null) {
|
||||||
return prisma.mapVote.update({
|
return prisma.mapVote.update({
|
||||||
@ -497,6 +499,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
|
|
||||||
// ➕ match-ready senden (erste Map + Teilnehmer)
|
// ➕ match-ready senden (erste Map + Teilnehmer)
|
||||||
if (updated?.locked) {
|
if (updated?.locked) {
|
||||||
|
await sleep(3000);
|
||||||
const chosen = deriveChosenSteps(updated)
|
const chosen = deriveChosenSteps(updated)
|
||||||
const first = chosen[0]
|
const first = chosen[0]
|
||||||
const key = first?.map ?? null
|
const key = first?.map ?? null
|
||||||
@ -550,6 +553,7 @@ export async function POST(req: NextRequest, { params }: { params: { matchId: st
|
|||||||
|
|
||||||
// ➕ match-ready senden
|
// ➕ match-ready senden
|
||||||
if (updated?.locked) {
|
if (updated?.locked) {
|
||||||
|
await sleep(3000);
|
||||||
const chosen = deriveChosenSteps(updated)
|
const chosen = deriveChosenSteps(updated)
|
||||||
const first = chosen[0]
|
const first = chosen[0]
|
||||||
const key = first?.map ?? null
|
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
|
// Falls durch diesen Schritt locked wurde → Export & match-ready
|
||||||
if (updated?.locked) {
|
if (updated?.locked) {
|
||||||
|
await sleep(3000);
|
||||||
const chosen = deriveChosenSteps(updated)
|
const chosen = deriveChosenSteps(updated)
|
||||||
const first = chosen[0]
|
const first = chosen[0]
|
||||||
const key = first?.map ?? null
|
const key = first?.map ?? null
|
||||||
|
|||||||
@ -168,12 +168,12 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
>
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
<span
|
<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"
|
role="status"
|
||||||
aria-label="loading"
|
aria-label="loading"
|
||||||
></span>
|
></span>
|
||||||
)}
|
)}
|
||||||
{loading ? 'Lädt' : (children ?? title)}
|
{children ?? title}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -267,32 +267,18 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
${dimmed ? 'opacity-40' : 'opacity-100'}
|
${dimmed ? 'opacity-40' : 'opacity-100'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{isLive && (
|
{/* Live / Map-Vote Badge */}
|
||||||
<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">
|
{isLive ? (
|
||||||
|
<span className="absolute top-2 px-2 py-0.5 text-xs font-semibold rounded-full bg-red-500 text-white shadow">
|
||||||
LIVE
|
LIVE
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : (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 Badge */}
|
Map-Vote offen
|
||||||
{m.mapVote && (
|
</span>
|
||||||
<span
|
) : null
|
||||||
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'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex w-full justify-around items-center">
|
<div className="flex w-full justify-around items-center">
|
||||||
<div className="flex flex-col items-center w-1/3">
|
<div className="flex flex-col items-center w-1/3">
|
||||||
@ -306,12 +292,17 @@ export default function CommunityMatchList({ matchType }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center space-y-1 mt-2">
|
{/* Datum + Uhrzeit: höher & highlight */}
|
||||||
<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'}`}>
|
<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 })}
|
{format(new Date(m.demoDate), 'dd.MM.yyyy', { locale: de })}
|
||||||
</span>
|
</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">
|
<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" />
|
<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>
|
</svg>
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
|
// CompRankBadge.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
|
|
||||||
type Props = {
|
type Props = { rank: number | null };
|
||||||
rank: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const rankNames: Record<number, string> = {
|
const rankNames: Record<number, string> = {
|
||||||
1: 'Silver I',
|
1: 'Silver I',
|
||||||
@ -32,7 +30,6 @@ const rankNames: Record<number, string> = {
|
|||||||
export default function CompRankBadge({ rank }: Props) {
|
export default function CompRankBadge({ rank }: Props) {
|
||||||
let imageName = 'skillgroup_none.webp';
|
let imageName = 'skillgroup_none.webp';
|
||||||
let altText = 'No Rank';
|
let altText = 'No Rank';
|
||||||
|
|
||||||
if (typeof rank === 'number') {
|
if (typeof rank === 'number') {
|
||||||
if (rank >= 1 && rank <= 18) {
|
if (rank >= 1 && rank <= 18) {
|
||||||
imageName = `skillgroup${rank}.webp`;
|
imageName = `skillgroup${rank}.webp`;
|
||||||
@ -50,8 +47,10 @@ export default function CompRankBadge({ rank }: Props) {
|
|||||||
alt={altText}
|
alt={altText}
|
||||||
width={60}
|
width={60}
|
||||||
height={60}
|
height={60}
|
||||||
sizes="(max-width: 768px) 100px, 70px"
|
// Wichtig: feste Höhe für die Zeile, Breite auto
|
||||||
style={{ objectFit: 'contain' }} // ← korrekt!
|
className="inline-block align-middle h-7 w-auto" // h-7 = 28px
|
||||||
|
sizes="70px"
|
||||||
|
style={{ objectFit: 'contain' }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||||
const [invitedIds, setInvitedIds] = useState<string[]>([])
|
const [invitedIds, setInvitedIds] = useState<string[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isInviting, setIsInviting] = useState(false)
|
||||||
const [isSuccess, setIsSuccess] = useState(false)
|
const [isSuccess, setIsSuccess] = useState(false)
|
||||||
const [sentCount, setSentCount] = useState(0)
|
const [sentCount, setSentCount] = useState(0)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
@ -87,10 +88,12 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleInvite = async () => {
|
const handleInvite = async () => {
|
||||||
|
if (isInviting) return
|
||||||
if (selectedIds.length === 0 || !steamId) return
|
if (selectedIds.length === 0 || !steamId) return
|
||||||
const ids = [...selectedIds]
|
const ids = [...selectedIds]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setIsInviting(true)
|
||||||
const url = directAdd ? '/api/team/add-players' : '/api/team/invite'
|
const url = directAdd ? '/api/team/add-players' : '/api/team/invite'
|
||||||
const body = directAdd
|
const body = directAdd
|
||||||
? { teamId: team.id, steamIds: ids }
|
? { teamId: team.id, steamIds: ids }
|
||||||
@ -135,6 +138,9 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
setSentCount(0)
|
setSentCount(0)
|
||||||
setIsSuccess(true)
|
setIsSuccess(true)
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
setIsInviting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -272,13 +278,18 @@ export default function InvitePlayersModal({ show, onClose, onSuccess, team, dir
|
|||||||
title={directAdd ? 'Spieler hinzufügen' : 'Spieler einladen'}
|
title={directAdd ? 'Spieler hinzufügen' : 'Spieler einladen'}
|
||||||
show={show}
|
show={show}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSave={handleInvite}
|
onSave={() => { if (!isInviting) handleInvite() }}
|
||||||
closeButtonColor={isSuccess ? 'teal' : 'blue'}
|
closeButtonColor={isSuccess ? 'teal' : 'blue'}
|
||||||
closeButtonTitle={
|
closeButtonTitle={
|
||||||
isSuccess
|
isSuccess
|
||||||
? directAdd ? 'Spieler hinzugefügt' : 'Einladungen versendet'
|
? (directAdd ? 'Spieler hinzugefügt' : 'Einladungen versendet')
|
||||||
: directAdd ? 'Hinzufügen' : 'Einladungen senden'
|
: (
|
||||||
|
isInviting
|
||||||
|
? (directAdd ? 'Wird hinzugefügt...' : 'Wird eingeladen...')
|
||||||
|
: (directAdd ? 'Hinzufügen' : 'Einladungen senden')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
closeButtonLoading={isInviting}
|
||||||
scrollBody={true}
|
scrollBody={true}
|
||||||
>
|
>
|
||||||
<p ref={descRef} className="text-sm text-gray-700 dark:text-neutral-300 mb-2">
|
<p ref={descRef} className="text-sm text-gray-700 dark:text-neutral-300 mb-2">
|
||||||
|
|||||||
@ -88,11 +88,11 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
|
|||||||
const gotoFullPage = () => router.push(`/match-details/${match.id}/vote`)
|
const gotoFullPage = () => router.push(`/match-details/${match.id}/vote`)
|
||||||
|
|
||||||
const cardClasses =
|
const cardClasses =
|
||||||
'relative overflow-hidden rounded-xl border bg-white/90 dark:bg-neutral-800/90 ' +
|
'group 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 ' +
|
'dark:border-neutral-700 shadow-sm cursor-pointer focus:outline-none transition ' +
|
||||||
(isOpen
|
(isOpen
|
||||||
? 'ring-1 ring-green-500/15 hover:ring-green-500/25 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')
|
: 'ring-1 ring-neutral-500/10 hover:ring-neutral-500/20 hover:shadow-md');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -103,11 +103,16 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
|
|||||||
className={cardClasses}
|
className={cardClasses}
|
||||||
aria-label="Map-Vote öffnen"
|
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="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="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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
@ -151,6 +156,7 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
|
/* Hintergrund-Schimmer (läuft permanent) */
|
||||||
@keyframes slide-x {
|
@keyframes slide-x {
|
||||||
from { background-position-x: 0%; }
|
from { background-position-x: 0%; }
|
||||||
to { background-position-x: 200%; }
|
to { background-position-x: 200%; }
|
||||||
@ -158,24 +164,57 @@ export default function MapVoteBanner({ match, initialNow }: Props) {
|
|||||||
.mapVoteGradient {
|
.mapVoteGradient {
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(16, 168, 54, 0.20) 0%,
|
rgba(16,168,54,0.20) 0%,
|
||||||
rgba(16, 168, 54, 0.04) 50%,
|
rgba(16,168,54,0.04) 50%,
|
||||||
rgba(16, 168, 54, 0.20) 100%
|
rgba(16,168,54,0.20) 100%
|
||||||
);
|
);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
animation: slide-x 3s linear infinite;
|
animation: slide-x 6s linear infinite; /* etwas ruhiger */
|
||||||
}
|
}
|
||||||
:global(.dark) .mapVoteGradient {
|
:global(.dark) .mapVoteGradient {
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(16, 168, 54, 0.28) 0%,
|
rgba(16,168,54,0.28) 0%,
|
||||||
rgba(16, 168, 54, 0.08) 50%,
|
rgba(16,168,54,0.08) 50%,
|
||||||
rgba(16, 168, 54, 0.28) 100%
|
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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.mapVoteGradient { animation: none; }
|
.mapVoteGradient { animation: none; }
|
||||||
|
.shine::before { animation: none !important; transform: none !important; opacity: 0 !important; }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,7 @@ import { Team } from '../types/team'
|
|||||||
import Alert from './Alert'
|
import Alert from './Alert'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { MATCH_EVENTS } from '../lib/sseEvents'
|
import { MATCH_EVENTS } from '../lib/sseEvents'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
type TeamWithPlayers = Team & { players?: MatchPlayer[] }
|
type TeamWithPlayers = Team & { players?: MatchPlayer[] }
|
||||||
|
|
||||||
@ -246,21 +247,38 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
/* ─── Render ─────────────────────────────────────────────── */
|
/* ─── Render ─────────────────────────────────────────────── */
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* 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"
|
||||||
|
>
|
||||||
|
Match bearbeiten
|
||||||
|
</Button>
|
||||||
|
<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">
|
<h1 className="text-2xl font-bold">
|
||||||
Match auf {mapLabel} ({match.matchType})
|
Match auf {mapLabel} ({match.matchType})
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{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">
|
|
||||||
Match bearbeiten
|
|
||||||
</Button>
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className="text-sm text-gray-500">Datum: {readableDate}</p>
|
<p className="text-sm text-gray-500">Datum: {readableDate}</p>
|
||||||
|
|
||||||
<div className="text-md">
|
<div className="text-md">
|
||||||
@ -280,20 +298,50 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h2 className="text-xl font-semibold">{match.teamA?.name ?? 'Team A'}</h2>
|
<h2 className="text-xl font-semibold">{match.teamA?.name ?? 'Team A'}</h2>
|
||||||
|
|
||||||
{showEditA && (
|
<Alert type="soft" color="dark" className="flex items-center justify-between gap-4">
|
||||||
<Alert type="soft" color="info" className="flex items-center justify-between gap-4">
|
<div className="flex items-center gap-2">
|
||||||
<span>
|
{showEditA ? (
|
||||||
Du kannst die Aufstellung noch bis <strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
|
<>
|
||||||
|
{/* 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>
|
</span>
|
||||||
<Button
|
</div>
|
||||||
size="sm"
|
|
||||||
onClick={() => setEditSide('A')}
|
<Button
|
||||||
className="px-3 py-1.5 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 text-white"
|
size="sm"
|
||||||
>
|
onClick={() => showEditA && setEditSide('A')}
|
||||||
Spieler bearbeiten
|
disabled={!showEditA}
|
||||||
</Button>
|
className={`px-3 py-1.5 text-sm rounded-lg ${
|
||||||
</Alert>
|
showEditA
|
||||||
)}
|
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||||
|
: 'bg-gray-400 text-gray-200 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Spieler bearbeiten
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderTable(teamAPlayers)}
|
{renderTable(teamAPlayers)}
|
||||||
@ -322,20 +370,51 @@ export function MatchDetails({ match, initialNow }: { match: Match; initialNow:
|
|||||||
{match.teamB?.name ?? 'Team B'}
|
{match.teamB?.name ?? 'Team B'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{showEditB && (
|
|
||||||
<Alert type="soft" color="info" className="flex items-center justify-between gap-4">
|
<Alert type="soft" color="dark" className="flex items-center justify-between gap-4">
|
||||||
<span>
|
<div className="flex items-center gap-2">
|
||||||
Du kannst die Aufstellung noch bis <strong>{format(endDate, 'dd.MM.yyyy HH:mm')}</strong> bearbeiten.
|
{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>
|
</span>
|
||||||
<Button
|
</div>
|
||||||
size="sm"
|
|
||||||
onClick={() => setEditSide('B')}
|
<Button
|
||||||
className="px-3 py-1.5 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 text-white"
|
size="sm"
|
||||||
>
|
onClick={() => showEditB && setEditSide('B')}
|
||||||
Spieler bearbeiten
|
disabled={!showEditB}
|
||||||
</Button>
|
className={`px-3 py-1.5 text-sm rounded-lg ${
|
||||||
</Alert>
|
showEditB
|
||||||
)}
|
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||||
|
: 'bg-gray-400 text-gray-200 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Spieler bearbeiten
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderTable(teamBPlayers)}
|
{renderTable(teamBPlayers)}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import Button from './Button'
|
||||||
|
|
||||||
type Width =
|
type Width =
|
||||||
| 'sm:max-w-sm'
|
| 'sm:max-w-sm'
|
||||||
@ -20,6 +21,7 @@ type ModalProps = {
|
|||||||
hideCloseButton?: boolean
|
hideCloseButton?: boolean
|
||||||
closeButtonColor?: 'blue' | 'red' | 'green' | 'teal'
|
closeButtonColor?: 'blue' | 'red' | 'green' | 'teal'
|
||||||
closeButtonTitle?: string
|
closeButtonTitle?: string
|
||||||
|
closeButtonLoading?: boolean
|
||||||
disableSave?: boolean
|
disableSave?: boolean
|
||||||
maxWidth?: Width
|
maxWidth?: Width
|
||||||
/** Wenn false, wird der Body nicht gescrollt (wir paginieren stattdessen) */
|
/** Wenn false, wird der Body nicht gescrollt (wir paginieren stattdessen) */
|
||||||
@ -119,8 +121,8 @@ export default function Modal({
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{!hideCloseButton && (
|
{!hideCloseButton && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
size='sm'
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
data-hs-overlay={`#${id}`}
|
data-hs-overlay={`#${id}`}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -139,7 +141,7 @@ export default function Modal({
|
|||||||
<path d="M18 6 6 18" />
|
<path d="M18 6 6 18" />
|
||||||
<path d="m6 6 12 12" />
|
<path d="m6 6 12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -156,25 +158,25 @@ export default function Modal({
|
|||||||
{/* Footer (fixe Höhe) */}
|
{/* 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">
|
<div className="flex justify-end items-center gap-x-2 py-3 px-4 border-t border-gray-200 dark:border-neutral-700">
|
||||||
{!hideCloseButton && (
|
{!hideCloseButton && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
size='sm'
|
||||||
data-hs-overlay={`#${id}`}
|
data-hs-overlay={`#${id}`}
|
||||||
onClick={onClose}
|
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"
|
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
|
Schließen
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{onSave && (
|
{onSave && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
size='sm'
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
disabled={disableSave}
|
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`}
|
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}
|
{closeButtonTitle}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -70,7 +70,7 @@ function Cell({
|
|||||||
const baseClass =
|
const baseClass =
|
||||||
Component === 'th'
|
Component === 'th'
|
||||||
? 'px-6 py-3 text-start font-medium text-xs text-gray-500 uppercase dark:text-neutral-400'
|
? '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 (
|
return (
|
||||||
<Component {...rest} className={`${baseClass} ${hoverClass} ${className}`}>
|
<Component {...rest} className={`${baseClass} ${hoverClass} ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -144,36 +144,65 @@ export default function TeamInvitationBanner({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
/* kontinuierlich nach rechts schieben */
|
/* Hintergrund-Schimmer (läuft permanent) */
|
||||||
@keyframes slide-x {
|
@keyframes slide-x {
|
||||||
from { background-position-x: 0%; }
|
from { background-position-x: 0%; }
|
||||||
to { background-position-x: 200%; }
|
to { background-position-x: 200%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.invitationGradient {
|
.invitationGradient {
|
||||||
/* weicher, dezenter Verlauf */
|
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(16, 168, 54, 0.20) 0%,
|
rgba(16,168,54,0.20) 0%,
|
||||||
rgba(16, 168, 54, 0.04) 50%,
|
rgba(16,168,54,0.04) 50%,
|
||||||
rgba(16, 168, 54, 0.20) 100%
|
rgba(16,168,54,0.20) 100%
|
||||||
);
|
);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
animation: slide-x 3s linear infinite;
|
animation: slide-x 6s linear infinite; /* etwas ruhiger */
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .invitationGradient {
|
:global(.dark) .invitationGradient {
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(16, 168, 54, 0.28) 0%,
|
rgba(16,168,54,0.28) 0%,
|
||||||
rgba(16, 168, 54, 0.08) 50%,
|
rgba(16,168,54,0.08) 50%,
|
||||||
rgba(16, 168, 54, 0.28) 100%
|
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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.invitationGradient { animation: none; }
|
.invitationGradient { animation: none; }
|
||||||
|
.shine::before { animation: none !important; transform: none !important; opacity: 0 !important; }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import TeamCardComponent from "../components/TeamCardComponent";
|
import TeamCardComponent from "../components/TeamCardComponent";
|
||||||
import Card from "../components/Card";
|
import Card from "../components/Card";
|
||||||
|
import LoadingSpinner from "../components/LoadingSpinner";
|
||||||
|
|
||||||
type TeamsResponse = { teams?: any[] } | undefined;
|
type TeamsResponse = { teams?: any[] } | undefined;
|
||||||
type InvitesResponse = { invitations?: any[] } | undefined;
|
type InvitesResponse = { invitations?: any[] } | undefined;
|
||||||
@ -57,7 +58,11 @@ export default function TeamPageClient() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <p>Lade Teams …</p>;
|
return (
|
||||||
|
<p>
|
||||||
|
<LoadingSpinner />
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -346,7 +346,7 @@ const config = {
|
|||||||
"value": "prisma-client-js"
|
"value": "prisma-client-js"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||||
"fromEnvVar": null
|
"fromEnvVar": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -360,7 +360,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previewFeatures": [],
|
"previewFeatures": [],
|
||||||
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
@ -374,6 +374,7 @@ const config = {
|
|||||||
"db"
|
"db"
|
||||||
],
|
],
|
||||||
"activeProvider": "postgresql",
|
"activeProvider": "postgresql",
|
||||||
|
"postinstall": false,
|
||||||
"inlineDatasources": {
|
"inlineDatasources": {
|
||||||
"db": {
|
"db": {
|
||||||
"url": {
|
"url": {
|
||||||
|
|||||||
@ -347,7 +347,7 @@ const config = {
|
|||||||
"value": "prisma-client-js"
|
"value": "prisma-client-js"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"value": "C:\\Users\\Chris\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
"value": "C:\\Users\\Rother\\fork\\ironie-nextjs\\src\\generated\\prisma",
|
||||||
"fromEnvVar": null
|
"fromEnvVar": null
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@ -361,7 +361,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"previewFeatures": [],
|
"previewFeatures": [],
|
||||||
"sourceFilePath": "C:\\Users\\Chris\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
"sourceFilePath": "C:\\Users\\Rother\\fork\\ironie-nextjs\\prisma\\schema.prisma",
|
||||||
"isCustomOutput": true
|
"isCustomOutput": true
|
||||||
},
|
},
|
||||||
"relativeEnvPaths": {
|
"relativeEnvPaths": {
|
||||||
@ -375,6 +375,7 @@ const config = {
|
|||||||
"db"
|
"db"
|
||||||
],
|
],
|
||||||
"activeProvider": "postgresql",
|
"activeProvider": "postgresql",
|
||||||
|
"postinstall": false,
|
||||||
"inlineDatasources": {
|
"inlineDatasources": {
|
||||||
"db": {
|
"db": {
|
||||||
"url": {
|
"url": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user