This commit is contained in:
2026-02-03 23:43:13 +08:00
parent f3734621fa
commit 4acf757433
19 changed files with 1965 additions and 158 deletions

92
app/api/config/routes.ts Normal file
View File

@@ -0,0 +1,92 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/app/lib/PrismaClient';
// TODO: admin 验证
// GET: 获取配置
export async function GET() {
try {
let config = await prisma.tournamentConfig.findUnique({
where: { id: 1 },
});
return NextResponse.json(config);
} catch (error) {
console.error('获取比赛配置失败:', error);
return NextResponse.json(
{ error: '获取配置失败' },
{ status: 500 }
);
}
}
// PUT: 更新配置 需要管理验证
export async function PUT(request: NextRequest) {
try {
const data = await request.json();
// 验证必要字段
const requiredFields = [
'tournament_name',
'max_pp_for_registration',
'min_pp_for_registration',
'current_seasonal',
'current_category',
'canRegister',
];
for (const field of requiredFields) {
if (data[field] === undefined) {
return NextResponse.json(
{ error: `缺少必要字段: ${field}` },
{ status: 400 }
);
}
}
// 更新或创建配置
const config = await prisma.tournamentConfig.upsert({
where: { id: 1 },
update: data,
create: {
id: 1,
...data,
},
});
return NextResponse.json(config);
} catch (error) {
console.error('更新比赛配置失败:', error);
return NextResponse.json(
{ error: '更新配置失败' },
{ status: 500 }
);
}
}
// INIT: 初始化配置 仅主办使用
export async function INIT() {
try {
let config = await prisma.tournamentConfig.findUnique({
where: { id: 1 },
});
if (!config) {
config = await prisma.tournamentConfig.create({
data: {
tournament_name: 'Astar Cup',
max_pp_for_registration: 1000,
min_pp_for_registration: 0,
current_seasonal: 'S1',
current_category: 'QUA',
canRegister: false,
},
});
}
return NextResponse.json(config);
} catch (error) {
console.error('初始化比赛配置失败:', error);
return NextResponse.json(
{ error: '初始化配置失败' },
{ status: 500 }
);
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/app/lib/PrismaClient';
// GET: 获取配置
export async function GET() {
try {
let config = await prisma.tournamentConfig.findUnique({
where: { id: 1 },
});
if (!config) {
config = await prisma.tournamentConfig.create({
data: {
id: 1,
tournament_name: 'AstarCup',
max_pp_for_registration: 0,
min_pp_for_registration: 0,
current_seasonal: 'S1',
current_category: 'QUA',
canRegister: false,
},
});
}
return NextResponse.json(config);
} catch (error) {
console.error('获取比赛配置失败:', error);
return NextResponse.json(
{ error: '获取配置失败' },
{ status: 500 }
);
}
}
// PUT: 更新配置
export async function PUT(request: NextRequest) {
try {
const data = await request.json();
// 验证必要字段
const requiredFields = [
'tournament_name',
'max_pp_for_registration',
'min_pp_for_registration',
'current_seasonal',
'current_category',
'canRegister',
];
for (const field of requiredFields) {
if (data[field] === undefined) {
return NextResponse.json(
{ error: `缺少必要字段: ${field}` },
{ status: 400 }
);
}
}
// 更新或创建配置
const config = await prisma.tournamentConfig.upsert({
where: { id: 1 },
update: data,
create: {
id: 1,
...data,
},
});
return NextResponse.json(config);
} catch (error) {
console.error('更新比赛配置失败:', error);
return NextResponse.json(
{ error: '更新配置失败' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,365 @@
"use client"
import { useState, useEffect, use } from 'react';
import { Season, Category } from '@/app/generated/prisma/enums';
import { useSeasonOptions, useCategoryOptions } from '@/app/lib/enum-labels';
interface TournamentConfigData {
id: number;
tournament_name: string;
max_pp_for_registration: number;
min_pp_for_registration: number;
current_seasonal: Season;
current_category: Category;
canRegister: boolean;
createdAt: Date;
updatedAt: Date;
}
interface TournamentConfigCardProps {
initialData?: TournamentConfigData;
onUpdate?: (data: TournamentConfigData) => void;
onError?: (error: Error) => void;
}
export default function TournamentConfigCard({
initialData,
onUpdate,
onError
}: TournamentConfigCardProps) {
// 状态管理
const [config, setConfig] = useState<TournamentConfigData | null>(initialData || null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
// 表单状态
const [formData, setFormData] = useState({
tournament_name: '',
max_pp_for_registration: 0,
min_pp_for_registration: 0,
current_seasonal: 'S1' as Season,
current_category: 'QUA' as Category,
canRegister: false,
});
// 枚举选项
const seasonOptions = useSeasonOptions();
const categoryOptions = useCategoryOptions();
// 初始化函数
const initializeForm = (data: TournamentConfigData) => {
setFormData({
tournament_name: data.tournament_name,
max_pp_for_registration: data.max_pp_for_registration,
min_pp_for_registration: data.min_pp_for_registration,
current_seasonal: data.current_seasonal,
current_category: data.current_category,
canRegister: data.canRegister,
});
};
// 加载配置
const loadConfig = async () => {
setLoading(true);
setError(null);
try {
// 这里需要调用 API 路由
const response = await fetch('/debug/api/tournament-config');
if (!response.ok) {
throw new Error(`加载失败: ${response.status}`);
}
const data = await response.json();
setConfig(data);
initializeForm(data);
setSuccess('配置加载成功');
if (onUpdate) {
onUpdate(data);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '未知错误';
setError(`加载配置失败: ${errorMessage}`);
if (onError) {
onError(err instanceof Error ? err : new Error(errorMessage));
}
} finally {
setLoading(false);
}
};
// 保存配置
const saveConfig = async () => {
setLoading(true);
setError(null);
setSuccess(null);
try {
const response = await fetch('/debug/api/tournament-config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
if (!response.ok) {
throw new Error(`保存失败: ${response.status}`);
}
const data = await response.json();
setConfig(data);
setSuccess('配置保存成功');
if (onUpdate) {
onUpdate(data);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '未知错误';
setError(`保存配置失败: ${errorMessage}`);
if (onError) {
onError(err instanceof Error ? err : new Error(errorMessage));
}
} finally {
setLoading(false);
}
};
// 重置为默认值
const resetToDefaults = () => {
setFormData({
tournament_name: 'AstarCup',
max_pp_for_registration: 0,
min_pp_for_registration: 0,
current_seasonal: 'S1',
current_category: 'QUA',
canRegister: false,
});
setSuccess('已重置为默认值');
};
// 表单变化处理
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
const checked = (e.target as HTMLInputElement).checked;
setFormData(prev => ({ ...prev, [name]: checked }));
} else if (type === 'number') {
setFormData(prev => ({ ...prev, [name]: parseFloat(value) || 0 }));
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
};
// 组件挂载时加载数据
useEffect(() => {
if (!initialData) {
loadConfig();
} else {
initializeForm(initialData);
}
}, [initialData]);
// 渲染组件
return (
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-gray-800"></h2>
<div className="flex space-x-2">
<button
onClick={loadConfig}
disabled={loading}
className="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 disabled:opacity-50"
>
</button>
<button
onClick={resetToDefaults}
disabled={loading}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
>
</button>
</div>
</div>
{/* 状态提示 */}
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded">
{error}
</div>
)}
{success && (
<div className="mb-4 p-3 bg-green-50 border border-green-200 text-green-700 rounded">
{success}
</div>
)}
{/* 配置信息 */}
{config && (
<div className="mb-4 p-3 bg-gray-50 rounded text-sm">
<div className="grid grid-cols-2 gap-2">
<div>ID: <span className="font-mono">{config.id}</span></div>
<div>: <span className="font-mono">{new Date(config.createdAt).toLocaleString()}</span></div>
<div>: <span className="font-mono">{new Date(config.updatedAt).toLocaleString()}</span></div>
</div>
</div>
)}
{/* 表单 */}
<div className="space-y-4">
{/* 比赛名称 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
name="tournament_name"
value={formData.tournament_name}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="输入比赛名称"
/>
</div>
{/* PP 限制 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
PP限制
</label>
<input
type="number"
name="min_pp_for_registration"
value={formData.min_pp_for_registration}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
step="0.1"
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
PP限制
</label>
<input
type="number"
name="max_pp_for_registration"
value={formData.max_pp_for_registration}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
step="0.1"
min="0"
/>
</div>
</div>
{/* 赛季和分类 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
name="current_seasonal"
value={formData.current_seasonal}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{seasonOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
name="current_category"
value={formData.current_category}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{categoryOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
{/* 注册开关 */}
<div className="flex items-center">
<input
type="checkbox"
id="canRegister"
name="canRegister"
checked={formData.canRegister}
onChange={handleInputChange}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="canRegister" className="ml-2 block text-sm text-gray-700">
</label>
<span className="ml-2 text-xs text-gray-500">
{formData.canRegister ? '✓ 注册已开放' : '✗ 注册已关闭'}
</span>
</div>
{/* 操作按钮 */}
<div className="flex justify-end space-x-3 pt-4 border-t">
<button
onClick={loadConfig}
disabled={loading}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded hover:bg-gray-50 disabled:opacity-50"
>
</button>
<button
onClick={saveConfig}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center"
>
{loading ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
...
</>
) : (
'保存配置'
)}
</button>
</div>
</div>
{/* 调试信息(开发环境显示) */}
{process.env.NODE_ENV === 'development' && (
<div className="mt-6 pt-4 border-t">
<h3 className="text-sm font-medium text-gray-700 mb-2"></h3>
<pre className="text-xs bg-gray-50 p-3 rounded overflow-auto">
{JSON.stringify({
formData,
config,
loading,
error,
success,
}, null, 2)}
</pre>
</div>
)}
</div>
);
}

12
app/debug/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
import TournamentConfigCard from '@/app/debug/components/TournamentConfigCard';
export default function DebugPage() {
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6"></h1>
<TournamentConfigCard />
</div>
);
}

View File

@@ -1,19 +1,16 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { prisma } from "@/app/lib/PrismaClient";
const geistSans = Geist({ async function fetchData() {
variable: "--font-geist-sans", const data = await prisma.tournamentConfig.findUnique({
subsets: ["latin"], where: { id: 1 },
}); });
return data;
const geistMono = Geist_Mono({ }
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: fetchData().then((data) => data?.tournament_name || "NoTitle").toString(),
description: "Generated by create next app", description: "Generated by create next app",
}; };
@@ -25,7 +22,7 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`antialiased`}
> >
{children} {children}
</body> </body>

21
app/lib/PrismaClient.ts Normal file
View File

@@ -0,0 +1,21 @@
import { PrismaClient } from '@/app/generated/prisma/client'
import { PrismaMariaDb } from '@prisma/adapter-mariadb'
const adapter = new PrismaMariaDb(
{
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT || '3306'),
password: process.env.DATABASE_PASSWORD || 'pwd',
user: process.env.DATABASE_USER || 'user',
database: process.env.DATABASE_NAME || 'database',
connectionLimit: 5
}
)
export const prisma = new PrismaClient({
adapter,
});
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

33
app/lib/SettingServer.ts Normal file
View File

@@ -0,0 +1,33 @@
import { prisma } from "@/app/lib/PrismaClient";
import { Season, Category } from "@/app/generated/prisma/enums";
export interface Config {
tournament_name?: string;
max_pp_for_registration?: number;
min_pp_for_registration?: number;
current_seasonal?: Season
current_category?: Category
canRegister?: boolean;
}
export async function getServerSideConfig() {
const config = await prisma.tournamentConfig.findUnique({
where: { id: 1 },
});
return config;
}
export async function setServerSideConfig(settings: Config) {
const updatedConfig = await prisma.tournamentConfig.update({
where: { id: 1 },
data: {
tournament_name: settings.tournament_name,
max_pp_for_registration: settings.max_pp_for_registration,
min_pp_for_registration: settings.min_pp_for_registration,
current_seasonal: settings.current_seasonal,
current_category: settings.current_category,
canRegister: settings.canRegister,
},
});
return updatedConfig;
}

0
app/lib/UserOperation.ts Normal file
View File

43
app/lib/enum-labels.ts Normal file
View File

@@ -0,0 +1,43 @@
import { useMemo } from 'react';
import { Season, Category } from '@/app/generated/prisma/enums';
interface EnumOption<T> {
value: T;
label: string;
}
const enumLabels = {
Season: {
[Season.S1]: 'S1',
[Season.S2]: 'S2',
},
Category: {
[Category.QUA]: '资格赛QUA',
[Category.RO16]: 'RO16',
[Category.QF]: '四分之一决赛QF',
[Category.SF]: '半决赛SF',
[Category.F]: '决赛F',
[Category.GF]: '总决赛GF',
},
};
export function useEnumOptions<T extends string>(
enumType: keyof typeof enumLabels,
enumValues: readonly T[]
): EnumOption<T>[] {
return useMemo(() => {
const labels = enumLabels[enumType] as Record<string, string>;
return enumValues.map(value => ({
value,
label: labels[value] || value,
}));
}, [enumType, enumValues]);
}
export function useSeasonOptions() {
return useEnumOptions('Season', Object.values(Season));
}
export function useCategoryOptions() {
return useEnumOptions('Category', Object.values(Category));
}

View File

@@ -64,7 +64,6 @@ export async function getOsuClientToken(): Promise<{
token_type: string; token_type: string;
}> { }> {
try { try {
console.log('Checking OSU_CLIENT_ID and OSU_CLIENT_SECRET...');
if (!OSU_CLIENT_ID || !OSU_CLIENT_SECRET) { if (!OSU_CLIENT_ID || !OSU_CLIENT_SECRET) {
throw new Error('OSU_CLIENT_ID and OSU_CLIENT_SECRET must be configured'); throw new Error('OSU_CLIENT_ID and OSU_CLIENT_SECRET must be configured');
} }

118
app/lib/osuBeatmapApi.ts Normal file
View File

@@ -0,0 +1,118 @@
import { getValidClientToken } from './osuAuth';
export interface BeatmapInfo {
id: number;
beatmapset_id: number;
title: string;
title_unicode: string;
artist: string;
artist_unicode: string;
version: string;
creator: string;
star_rating: number;
bpm: number;
total_length: number;
max_combo: number;
ar: number;
cs: number;
od: number;
hp: number;
url: string;
cover_url: string;
}
/// get beatmap info by beatmap ID
export async function getBeatmapInfo(beatmapId: number): Promise<BeatmapInfo | null> {
try {
const accessToken = await getValidClientToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
};
const response = await fetch(`https://osu.ppy.sh/api/v2/beatmaps/${beatmapId}`, {
headers,
});
if (!response.ok) {
if (response.status === 404) {
throw new Error('Beatmap不存在');
}
throw new Error(`获取Beatmap信息失败: ${response.status}`);
}
const data = await response.json();
return {
id: data.id,
beatmapset_id: data.beatmapset_id,
title: data.beatmapset?.title || '',
title_unicode: data.beatmapset?.title_unicode || data.beatmapset?.title || '',
artist: data.beatmapset?.artist || '',
artist_unicode: data.beatmapset?.artist_unicode || data.beatmapset?.artist || '',
version: data.version || '',
creator: data.beatmapset?.creator || '',
star_rating: data.difficulty_rating || 0,
bpm: data.bpm || 0,
total_length: data.total_length || 0,
max_combo: data.max_combo || 0,
ar: data.ar || 0,
cs: data.cs || 0,
od: data.accuracy || 0,
hp: data.drain || 0,
url: data.url || `https://osu.ppy.sh/beatmaps/${data.id}`,
cover_url: data.beatmapset?.covers?.cover || data.beatmapset?.covers?.card || ''
};
} catch (error) {
console.error('Error fetching beatmap info:', error);
throw error;
}
}
/// get beatmapset info by beatmapset ID
export async function getBeatmapsetInfo(beatmapsetId: number): Promise<BeatmapInfo[]> {
try {
const accessToken = await getValidClientToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
};
const response = await fetch(`https://osu.ppy.sh/api/v2/beatmapsets/${beatmapsetId}`, {
headers,
});
if (!response.ok) {
if (response.status === 404) {
throw new Error('Beatmapset不存在');
}
throw new Error(`获取Beatmapset信息失败: ${response.status}`);
}
const data = await response.json();
return data.beatmaps?.map((beatmap: any) => ({
id: beatmap.id,
beatmapset_id: beatmap.beatmapset_id,
title: data.title || '',
title_unicode: data.title_unicode || data.title || '',
artist: data.artist || '',
artist_unicode: data.artist_unicode || data.artist || '',
version: beatmap.version || '',
creator: data.creator || '',
star_rating: beatmap.difficulty_rating || 0,
bpm: beatmap.bpm || 0,
total_length: beatmap.total_length || 0,
max_combo: beatmap.max_combo || 0,
ar: beatmap.ar || 0,
cs: beatmap.cs || 0,
od: beatmap.accuracy || 0,
hp: beatmap.drain || 0,
url: beatmap.url || `https://osu.ppy.sh/beatmaps/${beatmap.id}`,
cover_url: data.covers?.cover || data.covers?.card || ''
})) || [];
} catch (error) {
console.error('Error fetching beatmapset info:', error);
throw error;
}
}

96
app/lib/osuUserApi.ts Normal file
View File

@@ -0,0 +1,96 @@
import { getValidClientToken } from './osuAuth';
export interface OsuUser {
id: number;
username: string;
avatar_url: string;
country_code: string;
cover?: {
custom_url: string | null;
url: string;
id: string | null;
};
statistics: {
pp: number;
global_rank: number | null;
country_rank: number | null;
country: string;
ranked_score: number;
hit_accuracy: number;
play_count: number;
play_time: number;
level: {
current: number;
progress: number;
};
grade_counts: {
ss: number;
ssh: number;
s: number;
sh: number;
a: number;
};
};
}
/// input osu! user ID, output osu! user data
export async function getUserData(userID: number): Promise<OsuUser | null> {
try {
// 获取客户端token
const accessToken = await getValidClientToken();
const response = await fetch(`https://osu.ppy.sh/api/v2/users/${userID}`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
if (response.status === 404) {
throw new Error('玩家不存在');
}
throw new Error(`获取玩家数据失败: ${response.status}`);
}
const data = await response.json();
return {
id: data.id,
username: data.username,
avatar_url: data.avatar_url,
country_code: data.country_code,
cover: data.cover ? {
custom_url: data.cover.custom_url || null,
url: data.cover.url || '',
id: data.cover.id || null,
} : undefined,
statistics: {
pp: data.statistics?.pp || 0,
global_rank: data.statistics?.global_rank || null,
country_rank: data.statistics?.country_rank || null,
country: data.country_code || '',
ranked_score: data.statistics?.ranked_score || 0,
hit_accuracy: data.statistics?.hit_accuracy || 0,
play_count: data.statistics?.play_count || 0,
play_time: data.statistics?.play_time || 0,
level: {
current: data.statistics?.level?.current || 0,
progress: data.statistics?.level?.progress || 0,
},
grade_counts: {
ss: data.statistics?.grade_counts?.ss || 0,
ssh: data.statistics?.grade_counts?.ssh || 0,
s: data.statistics?.grade_counts?.s || 0,
sh: data.statistics?.grade_counts?.sh || 0,
a: data.statistics?.grade_counts?.a || 0,
},
},
};
} catch (error) {
console.error('Error fetching osu! user data:', error);
throw error;
}
}

View File

@@ -1,65 +1,11 @@
import Image from "next/image"; import Image from "next/image";
import MainLogo from "./components/ui/MainLogo";
export default function Home() { export default function Home() {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <MainLogo />
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div> </div>
); );
} }

946
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,10 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/adapter-mariadb": "^7.3.0", "@prisma/adapter-mariadb": "^7.3.0",
"@prisma/client": "^7.3.0",
"@prisma/extension-accelerate": "^3.0.1",
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
@@ -27,4 +30,4 @@
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5" "typescript": "^5"
} }
} }

View File

@@ -21,6 +21,7 @@ model TournamentConfig {
min_pp_for_registration Float @default(0) min_pp_for_registration Float @default(0)
current_seasonal Season @default(S1) current_seasonal Season @default(S1)
current_category Category @default(QUA) current_category Category @default(QUA)
canRegister Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB