This commit is contained in:
Linrador 2025-08-05 15:36:44 +02:00
parent ad4fe7c29a
commit 442fad9c98
27 changed files with 779 additions and 769 deletions

54
package-lock.json generated
View File

@ -12,7 +12,7 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@floating-ui/react": "^0.27.12", "@floating-ui/react": "^0.27.12",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/fontawesome-free": "^7.0.0",
"@preline/dropdown": "^3.0.1", "@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0", "@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.13.0", "@prisma/client": "^6.13.0",
@ -24,6 +24,7 @@
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"dropzone": "^6.0.0-beta.2", "dropzone": "^6.0.0-beta.2",
"flag-icons": "^7.3.2", "flag-icons": "^7.3.2",
"font-awesome": "^4.7.0",
"framer-motion": "^12.18.1", "framer-motion": "^12.18.1",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"ky": "^1.8.2", "ky": "^1.8.2",
@ -902,42 +903,15 @@
"tslib": "2" "tslib": "2"
} }
}, },
"node_modules/@fortawesome/fontawesome-common-types": { "node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.2", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", "integrity": "sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ==",
"license": "MIT", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"peer": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"react": ">=16.3"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -4469,6 +4443,15 @@
} }
} }
}, },
"node_modules/font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
"integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==",
"license": "(OFL-1.1 AND MIT)",
"engines": {
"node": ">=0.10.3"
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -5400,6 +5383,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@ -5799,6 +5783,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
@ -6289,6 +6274,7 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -6779,6 +6765,7 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
@ -6901,6 +6888,7 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/read-package-up": { "node_modules/read-package-up": {

View File

@ -16,7 +16,7 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@floating-ui/react": "^0.27.12", "@floating-ui/react": "^0.27.12",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/fontawesome-free": "^7.0.0",
"@preline/dropdown": "^3.0.1", "@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0", "@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.13.0", "@prisma/client": "^6.13.0",
@ -28,6 +28,7 @@
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"dropzone": "^6.0.0-beta.2", "dropzone": "^6.0.0-beta.2",
"flag-icons": "^7.3.2", "flag-icons": "^7.3.2",
"font-awesome": "^4.7.0",
"framer-motion": "^12.18.1", "framer-motion": "^12.18.1",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"ky": "^1.8.2", "ky": "^1.8.2",

View File

@ -1,84 +0,0 @@
import { prisma } from '@/app/lib/prisma'
async function seedDemoUsers() {
const teamAId = 'cmab28h050001ph4s8w2a0wsc'
const teamBId = 'cmab2acln0003ph4stxe3ad38'
const usersA = Array.from({ length: 4 }, (_, i) => ({
steamId: `76561198000000A${i}`,
name: `DemoA${i}`,
avatar: '/default-avatar.png',
location: 'DE',
team: teamAId,
}))
const usersB = Array.from({ length: 4 }, (_, i) => ({
steamId: `76561198000000B${i}`,
name: `DemoB${i}`,
avatar: '/default-avatar.png',
location: 'DE',
team: teamBId,
}))
// Benutzer einfügen oder updaten
const createdUsers = await Promise.all(
[...usersA, ...usersB].map((user) =>
prisma.user.upsert({
where: { steamId: user.steamId },
update: {},
create: user,
})
)
)
// Team-Aktive Spieler setzen
await prisma.team.update({
where: { id: teamAId },
data: { activePlayers: usersA.map((u) => u.steamId) },
})
await prisma.team.update({
where: { id: teamBId },
data: { activePlayers: usersB.map((u) => u.steamId) },
})
// Zuletzt erstelltes Match mit diesen Teams finden
const match = await prisma.match.findFirst({
where: {
teamAId,
teamBId,
},
orderBy: { createdAt: 'desc' },
})
if (!match) {
throw new Error('Kein Match zwischen den beiden Teams gefunden.')
}
// Spieler zu MatchPlayer hinzufügen
await prisma.matchPlayer.createMany({
data: [
...usersA.map((u) => ({
matchId: match.id,
userId: u.steamId,
teamId: teamAId,
})),
...usersB.map((u) => ({
matchId: match.id,
userId: u.steamId,
teamId: teamBId,
})),
],
skipDuplicates: true,
})
console.log('✅ Demouser erstellt und MatchPlayer zugewiesen.')
}
seedDemoUsers()
.catch((err) => {
console.error('❌ Fehler beim Seed:', err)
})
.finally(() => {
prisma.$disconnect()
})

View File

@ -0,0 +1,62 @@
// /api/cs2/authcode/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth'
import { prisma } from '@/app/lib/prisma'
import { decrypt, encrypt } from '@/app/lib/crypto'
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions(req))
const steamId = session?.user?.steamId
if (!steamId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const user = await prisma.user.findUnique({
where: { steamId },
select: { authCode: true },
})
const authCode = user?.authCode ? decrypt(user.authCode) : null
return NextResponse.json({ authCode })
} catch (error) {
console.error('[GET /api/cs2/authcode]', error)
return NextResponse.json({ error: 'Fehler beim Abrufen' }, { status: 500 })
}
}
export async function PUT(req: NextRequest) {
const session = await getServerSession(authOptions(req))
const steamId = session?.user?.steamId
if (!steamId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { authCode } = await req.json()
const isValidAuthCode =
!authCode || /^[A-Z0-9]{4}-[A-Z0-9]{5}-[A-Z0-9]{4}$/.test(authCode)
if (!isValidAuthCode) {
return NextResponse.json({ error: 'invalid-authcode' }, { status: 400 })
}
try {
await prisma.user.update({
where: { steamId },
data: {
authCode: typeof authCode === 'string' ? encrypt(authCode) : null,
},
})
return new NextResponse(null, { status: 204 })
} catch (error) {
console.error('[PUT /api/cs2/authcode]', error)
return NextResponse.json({ error: 'Fehler beim Speichern' }, { status: 500 })
}
}

View File

@ -4,9 +4,7 @@ import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/lib/auth' import { authOptions } from '@/app/lib/auth'
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'
import { decrypt, encrypt } from '@/app/lib/crypto'
// Maximal 30 Tage gültig
const EXPIRY_DAYS = 30 const EXPIRY_DAYS = 30
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
@ -21,13 +19,11 @@ export async function GET(req: NextRequest) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { steamId }, where: { steamId },
select: { select: {
authCode: true,
lastKnownShareCode: true, lastKnownShareCode: true,
lastKnownShareCodeDate: true, lastKnownShareCodeDate: true,
}, },
}) })
const authCode = user?.authCode ? decrypt(user.authCode) : null
const lastKnownShareCode = user?.lastKnownShareCode ?? null const lastKnownShareCode = user?.lastKnownShareCode ?? null
const lastKnownShareCodeDate = user?.lastKnownShareCodeDate ?? null const lastKnownShareCodeDate = user?.lastKnownShareCodeDate ?? null
@ -41,7 +37,6 @@ export async function GET(req: NextRequest) {
} }
return NextResponse.json({ return NextResponse.json({
authCode,
lastKnownShareCode, lastKnownShareCode,
lastKnownShareCodeDate, lastKnownShareCodeDate,
reason, reason,
@ -60,11 +55,10 @@ export async function PUT(req: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
} }
const { authCode, lastKnownShareCode } = await req.json() const { lastKnownShareCode } = await req.json()
// Optional: zusätzliche Validierung für authCode const isValidShareCode =
const isValidAuthCode = !authCode || /^[A-Z0-9]{4}-[A-Z0-9]{5}-[A-Z0-9]{4}$/.test(authCode) !lastKnownShareCode || /^CSGO(-[a-zA-Z0-9]{5}){5}$/.test(lastKnownShareCode)
const isValidShareCode = !lastKnownShareCode || /^CSGO(-[a-zA-Z0-9]{5}){5}$/.test(lastKnownShareCode)
if (!isValidShareCode) { if (!isValidShareCode) {
return NextResponse.json({ error: 'expired-sharecode' }, { status: 400 }) return NextResponse.json({ error: 'expired-sharecode' }, { status: 400 })
@ -74,9 +68,8 @@ export async function PUT(req: NextRequest) {
await prisma.user.update({ await prisma.user.update({
where: { steamId }, where: { steamId },
data: { data: {
authCode: authCode === null ? null : (isValidAuthCode ? encrypt(authCode) : undefined), lastKnownShareCode: lastKnownShareCode ?? null,
lastKnownShareCode: lastKnownShareCode || undefined, lastKnownShareCodeDate: lastKnownShareCode ? new Date() : null,
lastKnownShareCodeDate: lastKnownShareCode ? new Date() : undefined,
}, },
}) })

View File

@ -1,208 +1,175 @@
'use client' 'use client'
import { useState } from "react" import { useState } from "react"
import Link from "next/link" import { usePathname, useRouter } from 'next/navigation'
import SidebarFooter from "./SidebarFooter" import SidebarFooter from "./SidebarFooter"
import { usePathname } from 'next/navigation'
import Button from "./Button" import Button from "./Button"
export default function Sidebar({ children }: { children?: React.ReactNode }) { export default function Sidebar({ children }: { children?: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false) const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const toggleSidebar = () => {
setIsOpen(prev => !prev) const [isOpen, setIsOpen] = useState(false)
const [openSubmenu, setOpenSubmenu] = useState<'teams' | 'players' | null>(null)
const toggleSidebar = () => setIsOpen(prev => !prev)
const handleSubmenuToggle = (menu: 'teams' | 'players') => {
setOpenSubmenu(prev => (prev === menu ? null : menu))
} }
return ( return (
<> <>
<Button <Button
onClick={toggleSidebar} onClick={toggleSidebar}
color="gray"
variant="solid"
className="absolute items-center p-2 mt-2 ms-3 text-sm text-gray-500 rounded-lg sm:hidden focus:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:hover:text-neutral-200 dark:focus:text-neutral-200" className="absolute items-center p-2 mt-2 ms-3 text-sm text-gray-500 rounded-lg sm:hidden focus:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:hover:text-neutral-200 dark:focus:text-neutral-200"
> >
<span className="sr-only">Open sidebar</span> <span className="sr-only">Open sidebar</span>
<svg <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
className="w-6 h-6" <path clipRule="evenodd" fillRule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z" />
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path clipRule="evenodd" fillRule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"></path>
</svg> </svg>
</Button> </Button>
<div className="flex"> <div className="flex">
<aside <aside className={`fixed top-0 left-0 z-40 h-screen w-64 bg-white dark:bg-neutral-800 border-e border-gray-200 dark:border-neutral-700 transition-transform sm:translate-x-0 ${isOpen ? 'translate-x-0' : '-translate-x-full'}`} aria-label="Sidebar">
className={` <div className="flex flex-col h-full">
fixed top-0 left-0 z-40 h-screen w-64 bg-white dark:bg-neutral-800 <header className="p-4 flex justify-between items-center">
border-e border-gray-200 dark:border-neutral-700 <a href="#" className="font-semibold text-xl text-black dark:text-white">Iron:e</a>
transition-transform sm:translate-x-0 <button onClick={toggleSidebar} className="lg:hidden">
${isOpen ? 'translate-x-0' : '-translate-x-full'} <svg className="size-4" viewBox="0 0 24 24" stroke="currentColor" fill="none">
`} <path d="M18 6L6 18M6 6l12 12" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
aria-label="Sidebar" </svg>
>
<div className="relative flex flex-col h-full max-h-full ">
<header className="p-4 flex justify-between items-center gap-x-2">
<a className="flex-none font-semibold text-xl text-black focus:outline-hidden focus:opacity-80 dark:text-white" href="#" aria-label="Iron:e">Iron:e</a>
<div className="lg:hidden -me-2">
<button type="button" onClick={toggleSidebar} className="flex justify-center items-center gap-x-3 size-6 bg-white border border-gray-200 text-sm text-gray-600 hover:bg-gray-100 rounded-full disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:hover:text-neutral-200 dark:focus:text-neutral-200" data-hs-overlay="#hs-sidebar-footer">
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
<span className="sr-only">Close</span>
</button> </button>
</div>
</header> </header>
<nav className="h-full overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500"> <nav className="flex-1 overflow-y-auto px-2">
<div className="hs-accordion-group pb-0 px-2 w-full flex flex-col flex-wrap" data-hs-accordion-always-open>
<ul className="space-y-1"> <ul className="space-y-1">
{/* Dashboard */}
<li> <li>
<Link <Button
href="/dashboard" onClick={() => router.push('/dashboard')}
className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors size="sm"
${pathname === '/dashboard' variant="link"
className={`w-full flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors ${
pathname === '/dashboard'
? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white' ? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white'
: 'text-gray-800 hover:bg-gray-100 dark:text-neutral-300 dark:hover:bg-neutral-700' : 'text-gray-800 hover:bg-gray-100 dark:text-neutral-300 dark:hover:bg-neutral-700'
}`} }`}
> >
<svg className="size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" > <svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><path d="M9 22V12h6v10"/></svg>
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
Dashboard Dashboard
</Link> </Button>
</li> </li>
<li className="hs-accordion" id="users-accordion"> {/* Teams */}
<button type="button" className="hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" aria-expanded="true" aria-controls="users-accordion-collapse-1"> <li>
<svg className="size-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <Button
<path fillRule="evenodd" d="M12 6a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Zm-1.5 8a4 4 0 0 0-4 4 2 2 0 0 0 2 2h7a2 2 0 0 0 2-2 4 4 0 0 0-4-4h-3Zm6.82-3.096a5.51 5.51 0 0 0-2.797-6.293 3.5 3.5 0 1 1 2.796 6.292ZM19.5 18h.5a2 2 0 0 0 2-2 4 4 0 0 0-4-4h-1.1a5.503 5.503 0 0 1-.471.762A5.998 5.998 0 0 1 19.5 18ZM4 7.5a3.5 3.5 0 0 1 5.477-2.889 5.5 5.5 0 0 0-2.796 6.293A3.501 3.501 0 0 1 4 7.5ZM7.1 12H6a4 4 0 0 0-4 4 2 2 0 0 0 2 2h.5a5.998 5.998 0 0 1 3.071-5.238A5.505 5.505 0 0 1 7.1 12Z" clipRule="evenodd"/> onClick={() => handleSubmenuToggle('teams')}
size="sm"
variant="link"
className="w-full flex items-center justify-between gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 dark:text-neutral-200 dark:hover:bg-neutral-700"
>
<span className="flex items-center gap-x-3.5">
<svg className="size-5" viewBox="0 0 640 640" fill="currentColor" stroke="currentColor" strokeWidth="2">
<path d="M320 64C355.3 64 384 92.7 384 128C384 163.3 355.3 192 320 192C284.7 192 256 163.3 256 128C256 92.7 284.7 64 320 64zM416 376C416 401 403.3 423 384 435.9L384 528C384 554.5 362.5 576 336 576L304 576C277.5 576 256 554.5 256 528L256 435.9C236.7 423 224 401 224 376L224 336C224 283 267 240 320 240C373 240 416 283 416 336L416 376zM160 96C190.9 96 216 121.1 216 152C216 182.9 190.9 208 160 208C129.1 208 104 182.9 104 152C104 121.1 129.1 96 160 96zM176 336L176 368C176 400.5 188.1 430.1 208 452.7L208 528C208 529.2 208 530.5 208.1 531.7C199.6 539.3 188.4 544 176 544L144 544C117.5 544 96 522.5 96 496L96 439.4C76.9 428.4 64 407.7 64 384L64 352C64 299 107 256 160 256C172.7 256 184.8 258.5 195.9 262.9C183.3 284.3 176 309.3 176 336zM432 528L432 452.7C451.9 430.2 464 400.5 464 368L464 336C464 309.3 456.7 284.4 444.1 262.9C455.2 258.4 467.3 256 480 256C533 256 576 299 576 352L576 384C576 407.7 563.1 428.4 544 439.4L544 496C544 522.5 522.5 544 496 544L464 544C451.7 544 440.4 539.4 431.9 531.7C431.9 530.5 432 529.2 432 528zM480 96C510.9 96 536 121.1 536 152C536 182.9 510.9 208 480 208C449.1 208 424 182.9 424 152C424 121.1 449.1 96 480 96z" />
</svg> </svg>
Teams Teams
</span>
<svg
className={`size-4 transition-transform ${openSubmenu === 'teams' ? 'rotate-180' : ''}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M6 9l6 6 6-6" />
</svg>
</Button>
<svg className="hs-accordion-active:block ms-auto hidden size-4 text-gray-600 group-hover:text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m18 15-6-6-6 6"/></svg> {openSubmenu === 'teams' && (
<ul className="pl-6 space-y-1 mt-1">
<svg className="hs-accordion-active:hidden ms-auto block size-4 text-gray-600 group-hover:text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div id="users-accordion-collapse-1" className="hs-accordion-content w-full overflow-hidden transition-[height] duration-300 hidden" role="region" aria-labelledby="users-accordion">
<ul className="hs-accordion-group pt-1 ps-7 space-y-1" data-hs-accordion-always-open>
<li className="hs-accordion" id="users-accordion-sub-1">
<button type="button" className="hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" aria-expanded="true" aria-controls="users-accordion-sub-1-collapse-1">
Sub Menu 1
<svg className="hs-accordion-active:block ms-auto hidden size-4 text-gray-600 group-hover:text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m18 15-6-6-6 6"/></svg>
<svg className="hs-accordion-active:hidden ms-auto block size-4 text-gray-600 group-hover:text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div id="users-accordion-sub-1-collapse-1" className="hs-accordion-content w-full overflow-hidden transition-[height] duration-300 hidden" role="region" aria-labelledby="users-accordion-sub-1">
<ul className="pt-1 ps-2 space-y-1">
<li> <li>
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#"> <Button onClick={() => router.push('/teams')} size="sm" variant="link" className="w-full text-start">
Link 1 Übersicht
</a> </Button>
</li> </li>
<li> <li>
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#"> <Button onClick={() => router.push('/teams/manage')} size="sm" variant="link" className="w-full text-start">
Link 2 Teamverwaltung
</a> </Button>
</li>
<li>
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#">
Link 3
</a>
</li> </li>
</ul> </ul>
</div> )}
</li> </li>
<li className="hs-accordion" id="users-accordion-sub-2"> {/* Spieler */}
<button type="button" className="hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" aria-expanded="true" aria-controls="users-accordion-sub-2-collapse-1">
Sub Menu 2
<svg className="hs-accordion-active:block ms-auto hidden size-4 text-gray-600 group-hover:text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m18 15-6-6-6 6"/></svg>
<svg className="hs-accordion-active:hidden ms-auto block size-4 text-gray-600 group-hover:text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div id="users-accordion-sub-2-collapse-1" className="hs-accordion-content w-full overflow-hidden transition-[height] duration-300 hidden" role="region" aria-labelledby="users-accordion-sub-2">
<ul className="pt-1 ps-2 space-y-1">
<li> <li>
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#"> <Button
Link 1 onClick={() => handleSubmenuToggle('players')}
</a> size="sm"
</li> variant="link"
<li> className="w-full flex items-center justify-between gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 dark:text-neutral-200 dark:hover:bg-neutral-700"
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#"> >
Link 2 <span className="flex items-center gap-x-3.5">
</a> <svg className="size-4" fill="currentColor" viewBox="0 0 24 24">
</li> <path fillRule="evenodd" d="M12 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm-2 9a4 4 0 0 0-4 4v1a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-1a4 4 0 0 0-4-4h-4Z" clipRule="evenodd"/>
<li> </svg>
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#">
Link 3
</a>
</li>
</ul>
</div>
</li>
</ul>
</div>
</li>
<li className="hs-accordion" id="account-accordion">
<button type="button" className="hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" aria-expanded="true" aria-controls="account-accordion-sub-1-collapse-1">
<svg className="size-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path fillRule="evenodd" d="M12 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm-2 9a4 4 0 0 0-4 4v1a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-1a4 4 0 0 0-4-4h-4Z" clipRule="evenodd"/></svg>
Spieler Spieler
</span>
<svg className={`size-4 transition-transform ${openSubmenu === 'players' ? 'rotate-180' : ''}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6" /></svg>
</Button>
<svg className="hs-accordion-active:block ms-auto hidden size-4 text-gray-600 group-hover:text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m18 15-6-6-6 6"/></svg> {openSubmenu === 'players' && (
<ul className="pl-6 space-y-1 mt-1">
<svg className="hs-accordion-active:hidden ms-auto block size-4 text-gray-600 group-hover:text-gray-500 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div id="account-accordion-sub-1-collapse-1" className="hs-accordion-content w-full overflow-hidden transition-[height] duration-300 hidden" role="region" aria-labelledby="account-accordion">
<ul className="pt-1 ps-7 space-y-1">
<li> <li>
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#"> <Button onClick={() => router.push('/players')} size="sm" variant="link" className="w-full text-start">
Link 1 Übersicht
</a> </Button>
</li> </li>
<li> <li>
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#"> <Button onClick={() => router.push('/players/stats')} size="sm" variant="link" className="w-full text-start">
Link 2 Statistiken
</a> </Button>
</li>
<li>
<a className="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200" href="#">
Link 3
</a>
</li> </li>
</ul> </ul>
</div> )}
</li> </li>
{/* Spielplan */}
<li> <li>
<Link href="/schedule" className="w-full flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700 dark:text-neutral-200"> <Button
<svg className="size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg> onClick={() => router.push('/schedule')}
Spielplan {/* <span className="ms-auto py-0.5 px-1.5 inline-flex items-center gap-x-1.5 text-xs bg-gray-200 text-gray-800 rounded-full dark:bg-neutral-600 dark:text-neutral-200">New</span> */} size="sm"
</Link> variant="link"
className={`w-full flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors ${
pathname === '/schedule'
? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white'
: 'text-gray-800 hover:bg-gray-100 dark:text-neutral-300 dark:hover:bg-neutral-700'
}`}
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect width="18" height="18" x="3" y="4" rx="2" ry="2"/>
<line x1="16" x2="16" y1="2" y2="6"/>
<line x1="8" x2="8" y1="2" y2="6"/>
<line x1="3" x2="21" y1="10" y2="10"/>
<path d="M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01"/>
</svg>
Spielplan
</Button>
</li> </li>
</ul> </ul>
</div>
</nav> </nav>
<footer className="mt-auto p-0 border-t border-gray-200 dark:border-neutral-700">
<SidebarFooter></SidebarFooter> <footer className="mt-auto border-t border-gray-200 dark:border-neutral-700">
<SidebarFooter />
</footer> </footer>
</div> </div>
</aside> </aside>
<div className="sm:ml-64 flex-1 h-screen overflow-y-auto p-6 bg-white dark:bg-black"> <main className="sm:ml-64 flex-1 p-6 bg-white dark:bg-black overflow-y-auto">
{children} {children}
</div> </main>
</div> </div>
</> </>
) )

View File

@ -2,15 +2,17 @@
import { signIn, signOut } from 'next-auth/react' import { signIn, signOut } from 'next-auth/react'
import { useSteamProfile } from '@/app/hooks/useSteamProfile' import { useSteamProfile } from '@/app/hooks/useSteamProfile'
import Link from 'next/link' import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import Script from "next/script"; import Script from "next/script";
import LoadingSpinner from '@/app/components/LoadingSpinner' import LoadingSpinner from '@/app/components/LoadingSpinner'
import Image from 'next/image' import Image from 'next/image'
import Button from './Button'
export default function SidebarFooter() { export default function SidebarFooter() {
const router = useRouter()
const { session, steamProfile, status } = useSteamProfile() const { session, steamProfile, status } = useSteamProfile()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const pathname = usePathname() const pathname = usePathname()
@ -43,9 +45,10 @@ export default function SidebarFooter() {
<> <>
<button <button
onClick={() => signIn('steam')} onClick={() => signIn('steam')}
className="w-full py-4 px-6 bg-green-700 text-white text-sm font-medium hover:bg-green-800 transition" className="flex items-center justify-center gap-2 w-full py-4 px-6 bg-green-800 text-white text-md font-medium hover:bg-green-900 transition"
> >
Mit Steam anmelden <i className="fab fa-steam" />
<span>Mit Steam anmelden</span>
</button> </button>
</> </>
) )
@ -109,8 +112,10 @@ export default function SidebarFooter() {
className="overflow-hidden w-full bg-white shadow-lg dark:bg-neutral-800 dark:border-neutral-600 z-20" className="overflow-hidden w-full bg-white shadow-lg dark:bg-neutral-800 dark:border-neutral-600 z-20"
> >
<div className="p-2 flex flex-col gap-1"> <div className="p-2 flex flex-col gap-1">
<Link <Button
href="/matches" onClick={() => router.push('/matches')}
size='sm'
variant='link'
className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors
${pathname === '/matches' ${pathname === '/matches'
? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white' ? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white'
@ -121,9 +126,11 @@ export default function SidebarFooter() {
<path strokeLinecap="round" strokeLinejoin="round" d="M9 17.25V15.75C9 14.2312 10.2312 13 11.75 13H12.25C13.7688 13 15 14.2312 15 15.75V17.25M4.5 12.75C4.5 11.2312 5.73122 10 7.25 10H7.75C9.26878 10 10.5 11.2312 10.5 12.75V14.25M13.5 10C13.5 8.48122 14.7312 7.25 16.25 7.25H16.75C18.2688 7.25 19.5 8.48122 19.5 10V11.5M4.5 4.5H19.5C20.3284 4.5 21 5.17157 21 6V18C21 18.8284 20.3284 19.5 19.5 19.5H4.5C3.67157 19.5 3 18.8284 3 18V6C3 5.17157 3.67157 4.5 4.5 4.5Z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 17.25V15.75C9 14.2312 10.2312 13 11.75 13H12.25C13.7688 13 15 14.2312 15 15.75V17.25M4.5 12.75C4.5 11.2312 5.73122 10 7.25 10H7.75C9.26878 10 10.5 11.2312 10.5 12.75V14.25M13.5 10C13.5 8.48122 14.7312 7.25 16.25 7.25H16.75C18.2688 7.25 19.5 8.48122 19.5 10V11.5M4.5 4.5H19.5C20.3284 4.5 21 5.17157 21 6V18C21 18.8284 20.3284 19.5 19.5 19.5H4.5C3.67157 19.5 3 18.8284 3 18V6C3 5.17157 3.67157 4.5 4.5 4.5Z" />
</svg> </svg>
Matches Matches
</Link> </Button>
<Link <Button
href={`/profile/${user.steamId}`} onClick={() => router.push(`/profile/${user.steamId}`)}
size='sm'
variant='link'
className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors
${pathname === `/profile/${user.steamId}` ${pathname === `/profile/${user.steamId}`
? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white' ? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white'
@ -134,9 +141,11 @@ export default function SidebarFooter() {
<path d="M15 9h3m-3 3h3m-3 3h3m-6 1c-.306-.613-.933-1-1.618-1H7.618c-.685 0-1.312.387-1.618 1M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm7 5a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"/> <path d="M15 9h3m-3 3h3m-3 3h3m-6 1c-.306-.613-.933-1-1.618-1H7.618c-.685 0-1.312.387-1.618 1M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm7 5a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"/>
</svg> </svg>
Mein Profil Mein Profil
</Link> </Button>
<Link <Button
href="/settings" onClick={() => router.push('/settings')}
size='sm'
variant='link'
className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors
${pathname.startsWith('/settings') ${pathname.startsWith('/settings')
? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white' ? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white'
@ -148,10 +157,12 @@ export default function SidebarFooter() {
</svg> </svg>
Einstellungen Einstellungen
</Link> </Button>
{user?.isAdmin && ( {user?.isAdmin && (
<Link <Button
href="/admin" onClick={() => router.push('/admin')}
size='sm'
variant='link'
className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors
${pathname.startsWith('/settings/admin') ${pathname.startsWith('/settings/admin')
? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white' ? 'bg-gray-100 dark:bg-neutral-700 text-gray-900 dark:text-white'
@ -162,11 +173,13 @@ export default function SidebarFooter() {
<path transform="scale(0.046875)" d="M78.6 5C69.1-2.4 55.6-1.5 47 7L7 47c-8.5 8.5-9.4 22-2.1 31.6l80 104c4.5 5.9 11.6 9.4 19 9.4l54.1 0 109 109c-14.7 29-10 65.4 14.3 89.6l112 112c12.5 12.5 32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3l-112-112c-24.2-24.2-60.6-29-89.6-14.3l-109-109 0-54.1c0-7.5-3.5-14.5-9.4-19L78.6 5zM19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9L233.7 374.3c-7.8-20.9-9-43.6-3.6-65.1l-61.7-61.7L19.9 396.1zM512 144c0-10.5-1.1-20.7-3.2-30.5c-2.4-11.2-16.1-14.1-24.2-6l-63.9 63.9c-3 3-7.1 4.7-11.3 4.7L352 176c-8.8 0-16-7.2-16-16l0-57.4c0-4.2 1.7-8.3 4.7-11.3l63.9-63.9c8.1-8.1 5.2-21.8-6-24.2C388.7 1.1 378.5 0 368 0C288.5 0 224 64.5 224 144l0 .8 85.3 85.3c36-9.1 75.8 .5 104 28.7L429 274.5c49-23 83-72.8 83-130.5zM56 432a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"/> <path transform="scale(0.046875)" d="M78.6 5C69.1-2.4 55.6-1.5 47 7L7 47c-8.5 8.5-9.4 22-2.1 31.6l80 104c4.5 5.9 11.6 9.4 19 9.4l54.1 0 109 109c-14.7 29-10 65.4 14.3 89.6l112 112c12.5 12.5 32.8 12.5 45.3 0l64-64c12.5-12.5 12.5-32.8 0-45.3l-112-112c-24.2-24.2-60.6-29-89.6-14.3l-109-109 0-54.1c0-7.5-3.5-14.5-9.4-19L78.6 5zM19.9 396.1C7.2 408.8 0 426.1 0 444.1C0 481.6 30.4 512 67.9 512c18 0 35.3-7.2 48-19.9L233.7 374.3c-7.8-20.9-9-43.6-3.6-65.1l-61.7-61.7L19.9 396.1zM512 144c0-10.5-1.1-20.7-3.2-30.5c-2.4-11.2-16.1-14.1-24.2-6l-63.9 63.9c-3 3-7.1 4.7-11.3 4.7L352 176c-8.8 0-16-7.2-16-16l0-57.4c0-4.2 1.7-8.3 4.7-11.3l63.9-63.9c8.1-8.1 5.2-21.8-6-24.2C388.7 1.1 378.5 0 368 0C288.5 0 224 64.5 224 144l0 .8 85.3 85.3c36-9.1 75.8 .5 104 28.7L429 274.5c49-23 83-72.8 83-130.5zM56 432a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"/>
</svg> </svg>
Administration Administration
</Link> </Button>
)} )}
<Link <Button
href="#"
onClick={() => signOut({ callbackUrl: '/' })} onClick={() => signOut({ callbackUrl: '/' })}
size='sm'
variant='link'
color='red'
className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors text-gray-800 hover:bg-red-100 dark:text-neutral-300 dark:hover:bg-red-700`} > className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg transition-colors text-gray-800 hover:bg-red-100 dark:text-neutral-300 dark:hover:bg-red-700`} >
<svg className="size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" > <svg className="size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" >
<path d="M20 12H8m12 0-4 4m4-4-4-4M9 4H7a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h2"/> <path d="M20 12H8m12 0-4 4m4-4-4-4M9 4H7a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h2"/>
@ -174,7 +187,7 @@ export default function SidebarFooter() {
Abmelden Abmelden
</Link> </Button>
</div> </div>
</motion.div> </motion.div>
)} )}

View File

@ -3,7 +3,7 @@
import DeleteAccountSettings from "./account/DeleteAccountSettings" import DeleteAccountSettings from "./account/DeleteAccountSettings"
import AppearanceSettings from "./account/AppearanceSettings" import AppearanceSettings from "./account/AppearanceSettings"
import AuthCodeSettings from "./account/AuthCodeSettings" import AuthCodeSettings from "./account/AuthCodeSettings"
import LatestKnownCodeSettings from "./account/LatestKnownCodeSettings" import LatestKnownCodeSettings from "./account/ShareCodeSettings"
export default function AccountSettings() { export default function AccountSettings() {
return ( return (
@ -27,6 +27,10 @@ export default function AccountSettings() {
<AuthCodeSettings /> <AuthCodeSettings />
{/* End Auth Code Settings */} {/* End Auth Code Settings */}
{/* Auth Code Settings */}
<LatestKnownCodeSettings />
{/* End Auth Code Settings */}
{/* Appearance */} {/* Appearance */}
<AppearanceSettings /> <AppearanceSettings />
{/* End Appearance */} {/* End Appearance */}

View File

@ -1,31 +1,18 @@
'use client' 'use client'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSession } from 'next-auth/react'
import Link from 'next/link' import Link from 'next/link'
import Popover from '../../Popover' import Popover from '../../Popover'
import LatestKnownCodeSettings from './LatestKnownCodeSettings'
import Button from '../../Button' import Button from '../../Button'
export default function AuthCodeSettings() { export default function AuthCodeSettings() {
const { data: session } = useSession()
const [authCode, setAuthCode] = useState('') const [authCode, setAuthCode] = useState('')
const [authCodeValid, setAuthCodeValid] = useState(false) const [authCodeValid, setAuthCodeValid] = useState(false)
const [lastKnownShareCode, setLastKnownShareCode] = useState('')
const [lastKnownShareCodeDate, setLastKnownShareCodeDate] = useState<Date | null>(null)
const [shareCodeManuallySet, setShareCodeManuallySet] = useState(false)
const [shareCodeSaved, setShareCodeSaved] = useState(false)
const [touched, setTouched] = useState(false) const [touched, setTouched] = useState(false)
const [manuallySet, setManuallySet] = useState(false)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const shareCodeExpired = useMemo(() => { const showInput = !isLoading && (!authCode || manuallySet)
if (!lastKnownShareCodeDate) return false
const daysSince = (Date.now() - new Date(lastKnownShareCodeDate).getTime()) / (1000 * 60 * 60 * 24)
return daysSince > 30
}, [lastKnownShareCodeDate])
const formatAuthCode = (value: string) => { const formatAuthCode = (value: string) => {
const raw = value.replace(/[^A-Za-z0-9]/g, '').toUpperCase() const raw = value.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
@ -38,38 +25,21 @@ export default function AuthCodeSettings() {
const validateAuthCode = (value: string) => const validateAuthCode = (value: string) =>
/^[A-Z0-9]{4}-[A-Z0-9]{5}-[A-Z0-9]{4}$/.test(value) /^[A-Z0-9]{4}-[A-Z0-9]{5}-[A-Z0-9]{4}$/.test(value)
const validateShareCode = (value: string) => const saveCode = async (code: string) => {
/^CSGO(-[a-zA-Z0-9]{5}){5}$/.test(value) if (!validateAuthCode(code)) return
const saveCodes = async (updatedAuthCode = authCode, updatedKnownCode = lastKnownShareCode) => {
if (!validateAuthCode(updatedAuthCode) || !validateShareCode(updatedKnownCode)) return
try { try {
const res = await fetch('/api/cs2/sharecode', { const res = await fetch('/api/cs2/authcode', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({ authCode: code }),
authCode: updatedAuthCode,
lastKnownShareCode: updatedKnownCode,
}),
}) })
if (res.ok) { if (res.ok) {
setLastKnownShareCodeDate(new Date()) setAuthCodeValid(true)
setShareCodeSaved(true)
} }
} catch (err) { } catch (err) {
console.error('Fehler beim Speichern der Codes:', err) console.error('Fehler beim Speichern des Codes:', err)
}
}
const handleSetShareCode = (val: string) => {
setLastKnownShareCode(val)
setShareCodeManuallySet(true)
setShareCodeSaved(false)
if (validateShareCode(val) && authCodeValid) {
saveCodes(authCode, val)
} }
} }
@ -77,19 +47,38 @@ export default function AuthCodeSettings() {
const formatted = formatAuthCode(e.target.value) const formatted = formatAuthCode(e.target.value)
setAuthCode(formatted) setAuthCode(formatted)
setTouched(true) setTouched(true)
setManuallySet(true)
const valid = validateAuthCode(formatted) const valid = validateAuthCode(formatted)
setAuthCodeValid(valid) setAuthCodeValid(valid)
if (valid && validateShareCode(lastKnownShareCode)) { if (valid) {
await saveCodes(formatted, lastKnownShareCode) await saveCode(formatted)
}
}
const handleDisconnect = async () => {
try {
const res = await fetch('/api/cs2/authcode', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ authCode: null }),
})
if (res.ok) {
setAuthCode('')
setAuthCodeValid(false)
setTouched(false)
setManuallySet(false)
}
} catch (err) {
console.error('Fehler beim Entfernen des Codes:', err)
} }
} }
useEffect(() => { useEffect(() => {
const fetchCodes = async () => { const fetchCode = async () => {
try { try {
const res = await fetch('/api/cs2/sharecode') const res = await fetch('/api/cs2/authcode')
const data = await res.json() const data = await res.json()
if (data?.authCode) { if (data?.authCode) {
@ -97,27 +86,15 @@ export default function AuthCodeSettings() {
setAuthCodeValid(validateAuthCode(data.authCode)) setAuthCodeValid(validateAuthCode(data.authCode))
} }
if (data?.lastKnownShareCode !== undefined) {
setLastKnownShareCode(data.lastKnownShareCode)
}
if (data?.lastKnownShareCodeDate) {
setLastKnownShareCodeDate(new Date(data.lastKnownShareCodeDate))
}
setIsLoading(false) setIsLoading(false)
} catch (err) { } catch (err) {
console.error('Fehler beim Laden der Codes:', err) console.error('Fehler beim Laden des Codes:', err)
} }
} }
fetchCodes() fetchCode()
}, []) }, [])
const showShareCodeInput = !isLoading && (
!lastKnownShareCode || shareCodeExpired || shareCodeManuallySet
)
return ( return (
<div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 dark:border-neutral-700"> <div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 dark:border-neutral-700">
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5"> <div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
@ -145,32 +122,8 @@ export default function AuthCodeSettings() {
</div> </div>
<div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5"> <div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5">
<div className="flex items-center gap-3">
<div className="relative w-full"> <div className="relative w-full">
{authCodeValid ? ( {showInput ? (
<Button color="red" onClick={async () => {
try {
const res = await fetch('/api/cs2/sharecode', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
authCode: null,
lastKnownShareCode,
}),
});
if (res.ok) {
setAuthCode('');
setAuthCodeValid(false);
setTouched(false);
}
} catch (err) {
console.error('Fehler beim Entfernen des Codes:', err);
}
}}>
Verbindung trennen
</Button>
) : (
<> <>
<input <input
type="text" type="text"
@ -181,7 +134,12 @@ export default function AuthCodeSettings() {
onBlur={() => setTouched(true)} onBlur={() => setTouched(true)}
className={`border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm className={`border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700
${touched ? (authCodeValid ? 'border-teal-500 focus:border-teal-500 focus:ring-teal-500' : 'border-red-500 focus:border-red-500 focus:ring-red-500') : 'border-gray-200'}`} ${touched
? (authCodeValid
? 'border-teal-500 dark:border-teal-500 focus:border-teal-500 focus:ring-teal-500'
: 'border-red-500 dark:border-red-500 focus:border-red-500 focus:ring-red-500')
: 'border-gray-200'
}`}
placeholder="XXXX-XXXXX-XXXX" placeholder="XXXX-XXXXX-XXXX"
required required
/> />
@ -194,9 +152,12 @@ export default function AuthCodeSettings() {
</div> </div>
)} )}
</> </>
) : (
<Button color="red" variant="ghost" onClick={handleDisconnect}>
Verbindung trennen
</Button>
)} )}
</div> </div>
</div>
{touched && ( {touched && (
<p className={`text-sm mt-2 ${authCodeValid ? 'text-teal-600' : 'text-red-600'}`}> <p className={`text-sm mt-2 ${authCodeValid ? 'text-teal-600' : 'text-red-600'}`}>
@ -205,15 +166,6 @@ export default function AuthCodeSettings() {
)} )}
</div> </div>
</div> </div>
{showShareCodeInput && (
<LatestKnownCodeSettings
lastKnownShareCode={lastKnownShareCode}
setLastKnownShareCode={handleSetShareCode}
isInvalid={shareCodeExpired}
isSaved={shareCodeSaved}
/>
)}
</div> </div>
) )
} }

View File

@ -1,120 +0,0 @@
// LatestKnownCodeSettings.tsx
'use client'
import Link from 'next/link'
import Popover from '../../Popover'
interface Props {
lastKnownShareCode: string
setLastKnownShareCode: (value: string) => void
isInvalid?: boolean
isSaved?: boolean
}
export default function LatestKnownCodeSettings({
lastKnownShareCode,
setLastKnownShareCode,
isInvalid = false,
isSaved = false,
}: Props) {
const formatLastKnownShareCode = (value: string) => {
const raw = value.replace(/[^a-zA-Z0-9]/g, '')
const part0 = raw.slice(0, 4)
const part1 = raw.slice(4, 9)
const part2 = raw.slice(9, 14)
const part3 = raw.slice(14, 19)
const part4 = raw.slice(19, 24)
const part5 = raw.slice(24, 29)
return [part0, part1, part2, part3, part4, part5].filter(Boolean).join('-')
}
const validate = (value: string) =>
/^CSGO(-[a-zA-Z0-9]{5}){5}$/.test(value)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatLastKnownShareCode(e.target.value)
setLastKnownShareCode(formatted)
}
const isValid = validate(lastKnownShareCode)
const showError = isInvalid || !isValid
return (
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
<div className="sm:col-span-4 2xl:col-span-2">
<label htmlFor="known-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
Austauschcode für Ihr letztes Spiel
</label>
<div className="mt-1">
<Popover text="Was ist der Austauschcode?" size="xl">
<div className="space-y-3">
<i><q>Mit dem Austauschcode können Anwendungen dein letztes offizielles Match finden und analysieren.</q></i>
<p>
Du findest deinen Code&nbsp;
<Link
href="https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
target="_blank"
className="text-blue-600 underline hover:text-blue-800"
>
hier
</Link>.
</p>
</div>
</Popover>
</div>
</div>
<div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5">
<div className="relative">
<input
type="text"
id="known-code"
name="known-code"
value={lastKnownShareCode}
onChange={handleChange}
className={`border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700
${showError
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
: 'border-teal-500 focus:border-teal-500 focus:ring-teal-500'}`}
placeholder="CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
required
/>
{!showError && (
<div className="absolute top-1/2 end-3 -translate-y-1/2 pointer-events-none">
<svg
className="size-4 text-teal-500"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
)}
{showError && (
<p className="text-sm text-red-600 mt-2">
Abgelaufener Austauschcode! Deinen neuen Austauschcode findest du&nbsp;
<Link
href="https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
target="_blank"
className="text-red-600 underline hover:text-blue-800"
>
hier
</Link>.
</p>
)}
{isSaved && !showError && (
<p className="text-sm text-teal-600 mt-2">
Gespeichert!
</p>
)}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,200 @@
// ShareauthCodeSettings.tsx
'use client'
import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import Popover from '../../Popover'
import Button from '../../Button'
export default function LatestKnownCodeSettings() {
const [lastKnownShareCode, setLastKnownShareCode] = useState('')
const [lastKnownShareCodeDate, setLastKnownShareCodeDate] = useState<Date | null>(null)
const [manuallySet, setManuallySet] = useState(false)
const [isSaved, setIsSaved] = useState(false)
const [touched, setTouched] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const shareCodeExpired = useMemo(() => {
if (!lastKnownShareCodeDate) return false
const daysSince = (Date.now() - new Date(lastKnownShareCodeDate).getTime()) / (1000 * 60 * 60 * 24)
return daysSince > 30
}, [lastKnownShareCodeDate])
const validateShareCode = (value: string) =>
/^CSGO(-[a-zA-Z0-9]{5}){5}$/.test(value)
const formatShareCode = (value: string) => {
// Entferne "CSGO-" (optional, falls mehrfach eingegeben), danach nur gültige Zeichen
const raw = value.replace(/^CSGO-?/i, '').replace(/[^a-zA-Z0-9]/g, '')
const parts = []
for (let i = 0; i < 5; i++) {
parts.push(raw.slice(i * 5, i * 5 + 5))
}
return ['CSGO', ...parts.filter(Boolean)].join('-')
}
const saveCode = async (code: string) => {
if (!validateShareCode(code)) return
try {
const res = await fetch('/api/cs2/sharecode', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
lastKnownShareCode: code,
}),
})
if (res.ok) {
setLastKnownShareCodeDate(new Date())
setIsSaved(true)
}
} catch (err) {
console.error('Fehler beim Speichern des Codes:', err)
}
}
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatShareCode(e.target.value)
setLastKnownShareCode(formatted)
setTouched(true)
setManuallySet(true)
setIsSaved(false)
if (validateShareCode(formatted)) {
await saveCode(formatted)
}
}
const handleDisconnect = async () => {
try {
const res = await fetch('/api/cs2/sharecode', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
lastKnownShareCode: null,
}),
})
if (res.ok) {
setLastKnownShareCode('')
setManuallySet(false)
setTouched(false)
setIsSaved(false)
}
} catch (err) {
console.error('Fehler beim Entfernen des Codes:', err)
}
}
useEffect(() => {
const fetchCode = async () => {
try {
const res = await fetch('/api/cs2/sharecode')
const data = await res.json()
if (data?.lastKnownShareCode !== undefined) {
setLastKnownShareCode(data.lastKnownShareCode)
}
if (data?.lastKnownShareCodeDate) {
setLastKnownShareCodeDate(new Date(data.lastKnownShareCodeDate))
}
setIsLoading(false)
} catch (err) {
console.error('Fehler beim Laden des Codes:', err)
}
}
fetchCode()
}, [])
const showInput = !isLoading && (
!lastKnownShareCode || shareCodeExpired || manuallySet
)
const isValid = validateShareCode(lastKnownShareCode)
const showError = touched && !isValid
return (
<div className="py-6 sm:py-8 space-y-5 border-t border-gray-200 dark:border-neutral-700">
<div className="grid sm:grid-cols-12 gap-y-1.5 sm:gap-y-0 sm:gap-x-5">
<div className="sm:col-span-4 2xl:col-span-2">
<label htmlFor="known-code" className="sm:mt-2.5 inline-block text-sm text-gray-500 dark:text-neutral-500">
Austauschcode für dein letztes Spiel
</label>
<div className="mt-1">
<Popover text="Was ist der Austauschcode?" size="xl">
<div className="space-y-3">
<i><q>Mit dem Austauschcode können Anwendungen dein letztes offizielles Match finden und analysieren.</q></i>
<p>
Du findest deinen Code&nbsp;
<Link
href="https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
target="_blank"
className="text-blue-600 underline hover:text-blue-800"
>
hier
</Link>.
</p>
</div>
</Popover>
</div>
</div>
<div className="sm:col-span-8 xl:col-span-6 2xl:col-span-5">
<div className="relative">
{showInput ? (
<>
<input
type="text"
id="known-code"
name="known-code"
value={lastKnownShareCode}
onChange={handleChange}
className={`border py-2.5 sm:py-3 px-4 block w-full rounded-lg sm:text-sm
dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700
${touched
? (isSaved
? 'border-teal-500 dark:border-teal-500 focus:border-teal-500 focus:ring-teal-500'
: 'border-red-500 dark:border-red-500 focus:border-red-500 focus:ring-red-500')
: 'border-gray-200'
}`}
placeholder="CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
required
/>
{showError && (
<p className="text-sm text-red-600 mt-2">
Abgelaufener Austauschcode! Deinen neuen Austauschcode findest du&nbsp;
<Link
href="https://help.steampowered.com/de/wizard/HelpWithGameIssue/?appid=730&issueid=128"
target="_blank"
className="text-red-600 underline hover:text-blue-800"
>
hier
</Link>.
</p>
)}
{isSaved && !showError && (
<p className="text-sm text-teal-600 mt-2">
Gespeichert!
</p>
)}
</>
) : (
<Button color="red" variant="ghost" onClick={handleDisconnect}>
Zurücksetzen
</Button>
)}
</div>
</div>
</div>
</div>
)
}

View File

@ -6,6 +6,7 @@
@source "../node_modules/preline/dist/*.js"; @source "../node_modules/preline/dist/*.js";
@import 'flag-icons/css/flag-icons.min.css'; @import 'flag-icons/css/flag-icons.min.css';
@import '@fortawesome/fontawesome-free/css/all.min.css';
@keyframes shake { @keyframes shake {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
@ -59,7 +60,7 @@
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Helvetica, sans-serif; font-family: ui-sans-serif,-apple-system,system-ui,Segoe UI,Helvetica,Apple Color Emoji,Arial,sans-serif,Segoe UI Emoji,Segoe UI Symbol;
} }
.premier-rank-wrapper { .premier-rank-wrapper {

View File

@ -35,12 +35,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.10.1 * Prisma Client JS version: 6.13.0
* Query Engine version: 9b628578b3b7cae625e8c927178f15a170e74a9c * Query Engine version: 361e86d0ea4987e9f53a565309b3eed797a6bcbd
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.10.1", client: "6.13.0",
engine: "9b628578b3b7cae625e8c927178f15a170e74a9c" engine: "361e86d0ea4987e9f53a565309b3eed797a6bcbd"
} }
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError; Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@ -302,7 +302,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": {
@ -316,7 +316,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": {
@ -324,8 +324,8 @@ const config = {
"schemaEnvPath": "../../../.env" "schemaEnvPath": "../../../.env"
}, },
"relativePath": "../../../prisma", "relativePath": "../../../prisma",
"clientVersion": "6.10.1", "clientVersion": "6.13.0",
"engineVersion": "9b628578b3b7cae625e8c927178f15a170e74a9c", "engineVersion": "361e86d0ea4987e9f53a565309b3eed797a6bcbd",
"datasourceNames": [ "datasourceNames": [
"db" "db"
], ],

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.10.1 * Prisma Client JS version: 6.13.0
* Query Engine version: 9b628578b3b7cae625e8c927178f15a170e74a9c * Query Engine version: 361e86d0ea4987e9f53a565309b3eed797a6bcbd
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.10.1", client: "6.13.0",
engine: "9b628578b3b7cae625e8c927178f15a170e74a9c" engine: "361e86d0ea4987e9f53a565309b3eed797a6bcbd"
} }
Prisma.PrismaClientKnownRequestError = () => { Prisma.PrismaClientKnownRequestError = () => {

View File

@ -105,7 +105,7 @@ export const ScheduleStatus: typeof $Enums.ScheduleStatus
*/ */
export class PrismaClient< export class PrismaClient<
ClientOptions extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions, ClientOptions extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
U = 'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never, const U = 'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never,
ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs
> { > {
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] } [K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
@ -381,8 +381,8 @@ export namespace Prisma {
export import Exact = $Public.Exact export import Exact = $Public.Exact
/** /**
* Prisma Client JS version: 6.10.1 * Prisma Client JS version: 6.13.0
* Query Engine version: 9b628578b3b7cae625e8c927178f15a170e74a9c * Query Engine version: 361e86d0ea4987e9f53a565309b3eed797a6bcbd
*/ */
export type PrismaVersion = { export type PrismaVersion = {
client: string client: string
@ -1653,16 +1653,24 @@ export namespace Prisma {
/** /**
* @example * @example
* ``` * ```
* // Defaults to stdout * // Shorthand for `emit: 'stdout'`
* log: ['query', 'info', 'warn', 'error'] * log: ['query', 'info', 'warn', 'error']
* *
* // Emit as events * // Emit as events only
* log: [ * log: [
* { emit: 'event', level: 'query' },
* { emit: 'event', level: 'info' },
* { emit: 'event', level: 'warn' }
* { emit: 'event', level: 'error' }
* ]
*
* / Emit as events and log to stdout
* og: [
* { emit: 'stdout', level: 'query' }, * { emit: 'stdout', level: 'query' },
* { emit: 'stdout', level: 'info' }, * { emit: 'stdout', level: 'info' },
* { emit: 'stdout', level: 'warn' } * { emit: 'stdout', level: 'warn' }
* { emit: 'stdout', level: 'error' } * { emit: 'stdout', level: 'error' }
* ] *
* ``` * ```
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/logging#the-log-option). * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/logging#the-log-option).
*/ */
@ -1714,10 +1722,15 @@ export namespace Prisma {
emit: 'stdout' | 'event' emit: 'stdout' | 'event'
} }
export type GetLogType<T extends LogLevel | LogDefinition> = T extends LogDefinition ? T['emit'] extends 'event' ? T['level'] : never : never export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
export type GetEvents<T extends any> = T extends Array<LogLevel | LogDefinition> ?
GetLogType<T[0]> | GetLogType<T[1]> | GetLogType<T[2]> | GetLogType<T[3]> export type GetLogType<T> = CheckIsLogLevel<
: never T extends LogDefinition ? T['level'] : T
>;
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
? GetLogType<T[number]>
: never;
export type QueryEvent = { export type QueryEvent = {
timestamp: Date timestamp: Date

View File

@ -35,12 +35,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.10.1 * Prisma Client JS version: 6.13.0
* Query Engine version: 9b628578b3b7cae625e8c927178f15a170e74a9c * Query Engine version: 361e86d0ea4987e9f53a565309b3eed797a6bcbd
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.10.1", client: "6.13.0",
engine: "9b628578b3b7cae625e8c927178f15a170e74a9c" engine: "361e86d0ea4987e9f53a565309b3eed797a6bcbd"
} }
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError; Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
@ -303,7 +303,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": {
@ -317,7 +317,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": {
@ -325,8 +325,8 @@ const config = {
"schemaEnvPath": "../../../.env" "schemaEnvPath": "../../../.env"
}, },
"relativePath": "../../../prisma", "relativePath": "../../../prisma",
"clientVersion": "6.10.1", "clientVersion": "6.13.0",
"engineVersion": "9b628578b3b7cae625e8c927178f15a170e74a9c", "engineVersion": "361e86d0ea4987e9f53a565309b3eed797a6bcbd",
"datasourceNames": [ "datasourceNames": [
"db" "db"
], ],

View File

@ -81,6 +81,10 @@
}, },
"./runtime/client": { "./runtime/client": {
"types": "./runtime/client.d.ts", "types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js", "require": "./runtime/client.js",
"import": "./runtime/client.mjs", "import": "./runtime/client.mjs",
"default": "./runtime/client.mjs" "default": "./runtime/client.mjs"
@ -141,6 +145,6 @@
}, },
"./*": "./*" "./*": "./*"
}, },
"version": "6.10.1", "version": "6.13.0",
"sideEffects": false "sideEffects": false
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1179,6 +1179,10 @@ declare type Error_2 = {
} | { } | {
foreignKey: {}; foreignKey: {};
}; };
} | {
kind: 'DatabaseNotReachable';
host?: string;
port?: number;
} | { } | {
kind: 'DatabaseDoesNotExist'; kind: 'DatabaseDoesNotExist';
db?: string; db?: string;
@ -1188,6 +1192,11 @@ declare type Error_2 = {
} | { } | {
kind: 'DatabaseAccessDenied'; kind: 'DatabaseAccessDenied';
db?: string; db?: string;
} | {
kind: 'ConnectionClosed';
} | {
kind: 'TlsConnectionError';
reason: string;
} | { } | {
kind: 'AuthenticationFailed'; kind: 'AuthenticationFailed';
user?: string; user?: string;
@ -2411,7 +2420,7 @@ export declare const objectEnumValues: {
}; };
}; };
declare const officialPrismaAdapters: readonly ["@prisma/adapter-planetscale", "@prisma/adapter-neon", "@prisma/adapter-libsql", "@prisma/adapter-d1", "@prisma/adapter-pg", "@prisma/adapter-mssql"]; declare const officialPrismaAdapters: readonly ["@prisma/adapter-planetscale", "@prisma/adapter-neon", "@prisma/adapter-libsql", "@prisma/adapter-better-sqlite3", "@prisma/adapter-d1", "@prisma/adapter-pg", "@prisma/adapter-mssql", "@prisma/adapter-mariadb"];
export declare type Omission = Record<string, boolean | Skip>; export declare type Omission = Record<string, boolean | Skip>;
@ -2729,6 +2738,7 @@ declare interface Queryable<Query, Result> extends AdapterInfo {
declare type QueryCompiler = { declare type QueryCompiler = {
compile(request: string): {}; compile(request: string): {};
compileBatch(batchRequest: string): BatchResponse; compileBatch(batchRequest: string): BatchResponse;
free(): void;
}; };
declare interface QueryCompilerConstructor { declare interface QueryCompilerConstructor {
@ -2768,6 +2778,11 @@ declare interface QueryEngineConstructor {
declare type QueryEngineInstance = { declare type QueryEngineInstance = {
connect(headers: string, requestId: string): Promise<void>; connect(headers: string, requestId: string): Promise<void>;
disconnect(headers: string, requestId: string): Promise<void>; disconnect(headers: string, requestId: string): Promise<void>;
/**
* Frees any resources allocated by the engine's WASM instance. This method is automatically created by WASM bindgen.
* Noop for other engines.
*/
free?(): void;
/** /**
* @param requestStr JSON.stringified `QueryEngineRequest | QueryEngineBatchRequest` * @param requestStr JSON.stringified `QueryEngineRequest | QueryEngineBatchRequest`
* @param headersStr JSON.stringified `QueryEngineRequestHeaders` * @param headersStr JSON.stringified `QueryEngineRequestHeaders`
@ -3072,6 +3087,7 @@ declare type SchemaArg = ReadonlyDeep_2<{
isNullable: boolean; isNullable: boolean;
isRequired: boolean; isRequired: boolean;
inputTypes: InputTypeRef[]; inputTypes: InputTypeRef[];
requiresOtherFields?: string[];
deprecation?: Deprecation; deprecation?: Deprecation;
}>; }>;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -21,7 +21,7 @@ model User {
location String? location String?
isAdmin Boolean @default(false) isAdmin Boolean @default(false)
teamId String? @unique teamId String?
team Team? @relation("UserTeam", fields: [teamId], references: [id]) team Team? @relation("UserTeam", fields: [teamId], references: [id])
ledTeam Team? @relation("TeamLeader") ledTeam Team? @relation("TeamLeader")

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.10.1 * Prisma Client JS version: 6.13.0
* Query Engine version: 9b628578b3b7cae625e8c927178f15a170e74a9c * Query Engine version: 361e86d0ea4987e9f53a565309b3eed797a6bcbd
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.10.1", client: "6.13.0",
engine: "9b628578b3b7cae625e8c927178f15a170e74a9c" engine: "361e86d0ea4987e9f53a565309b3eed797a6bcbd"
} }
Prisma.PrismaClientKnownRequestError = () => { Prisma.PrismaClientKnownRequestError = () => {