feat: F1 stats 2025 - tabbed UI, backend Java, full season data

This commit is contained in:
Tatiana Villa Ema 2026-04-27 01:39:21 +02:00
commit 7b4c94c2c7
42 changed files with 2758 additions and 0 deletions

2
backend/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

33
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

View File

@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip

12
backend/Dockerfile Normal file
View File

@ -0,0 +1,12 @@
# Paso 1: Compilación (Usamos Maven)
FROM maven:3.9-eclipse-temurin-17 AS build
COPY . /app
WORKDIR /app
RUN mvn clean package -DskipTests
# Paso 2: Ejecución (Imagen ligera)
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

0
backend/docker Normal file
View File

295
backend/mvnw vendored Normal file
View File

@ -0,0 +1,295 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

189
backend/mvnw.cmd vendored Normal file
View File

@ -0,0 +1,189 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

0
backend/netstat Normal file
View File

99
backend/pom.xml Normal file
View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>es.tatvil</groupId>
<artifactId>formula1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>formula1</name>
<description>backed de datos de formula 1</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Class-Path:

View File

@ -0,0 +1,13 @@
package es.tatvil.formula1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Formula1Application {
public static void main(String[] args) {
SpringApplication.run(Formula1Application.class, args);
}
}

View File

@ -0,0 +1,37 @@
package es.tatvil.formula1.controller;
import es.tatvil.formula1.model.Escuderia;
import es.tatvil.formula1.model.Piloto;
import es.tatvil.formula1.model.PilotoEscuderia;
import es.tatvil.formula1.repository.PilotoEscuderiaRepository;
import es.tatvil.formula1.service.EscuderiaService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/escuderias")
public class EscuderiaController {
private final EscuderiaService escuderiaService;
private final PilotoEscuderiaRepository pilotoEscuderiaRepository;
public EscuderiaController(EscuderiaService escuderiaService,
PilotoEscuderiaRepository pilotoEscuderiaRepository) {
this.escuderiaService = escuderiaService;
this.pilotoEscuderiaRepository = pilotoEscuderiaRepository;
}
@GetMapping
public List<Escuderia> listar() {
return escuderiaService.obtenerTodos();
}
@GetMapping("/{id}/pilotos")
public List<Piloto> getPilotosPorEscuderia(@PathVariable Integer id) {
return pilotoEscuderiaRepository.findByEscuderiaId(id)
.stream()
.map(PilotoEscuderia::getPiloto)
.toList();
}
}

View File

@ -0,0 +1,28 @@
package es.tatvil.formula1.controller;
import es.tatvil.formula1.model.GranPremio;
import es.tatvil.formula1.service.GranPremioService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/gp")
public class GranPremioController {
private final GranPremioService granPremioService;
public GranPremioController(GranPremioService granPremioService) {
this.granPremioService = granPremioService;
}
@GetMapping
public List<GranPremio> listar(@RequestParam(defaultValue = "2025") Integer temporada) {
return granPremioService.obtenerPorTemporada(temporada);
}
@GetMapping("/{id}")
public GranPremio obtener(@PathVariable Integer id) {
return granPremioService.obtenerPorId(id);
}
}

View File

@ -0,0 +1,37 @@
package es.tatvil.formula1.controller;
import es.tatvil.formula1.model.Escuderia;
import es.tatvil.formula1.model.Piloto;
import es.tatvil.formula1.model.PilotoEscuderia;
import es.tatvil.formula1.repository.PilotoEscuderiaRepository;
import es.tatvil.formula1.service.PilotoService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/pilotos")
public class PilotoController {
private final PilotoService pilotoService;
private final PilotoEscuderiaRepository pilotoEscuderiaRepository;
public PilotoController(PilotoService pilotoService,
PilotoEscuderiaRepository pilotoEscuderiaRepository) {
this.pilotoService = pilotoService;
this.pilotoEscuderiaRepository = pilotoEscuderiaRepository;
}
@GetMapping
public List<Piloto> listar() {
return pilotoService.obtenerTodos();
}
@GetMapping("/{id}/escuderias")
public List<Escuderia> getEscuderiasPorPiloto(@PathVariable Integer id) {
return pilotoEscuderiaRepository.findByPilotoId(id)
.stream()
.map(PilotoEscuderia::getEscuderia) // getter público
.toList();
}
}

View File

@ -0,0 +1,41 @@
package es.tatvil.formula1.controller;
import es.tatvil.formula1.model.Resultado;
import es.tatvil.formula1.service.ResultadoService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/resultados")
public class ResultadoController {
private final ResultadoService resultadoService;
public ResultadoController(ResultadoService resultadoService) {
this.resultadoService = resultadoService;
}
@GetMapping("/gp/{gpId}")
public List<Resultado> porGranPremio(@PathVariable Integer gpId) {
return resultadoService.obtenerPorGranPremio(gpId);
}
@GetMapping("/piloto/{pilotoId}")
public List<Resultado> porPiloto(@PathVariable Integer pilotoId) {
return resultadoService.obtenerPorPiloto(pilotoId);
}
@GetMapping("/clasificacion/pilotos")
public List<Map<String, Object>> clasificacionPilotos(
@RequestParam(defaultValue = "2025") Integer temporada) {
return resultadoService.clasificacionPilotos(temporada);
}
@GetMapping("/clasificacion/constructores")
public List<Map<String, Object>> clasificacionConstructores(
@RequestParam(defaultValue = "2025") Integer temporada) {
return resultadoService.clasificacionConstructores(temporada);
}
}

View File

@ -0,0 +1,26 @@
package es.tatvil.formula1.model;
import java.util.List;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "escuderias")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Escuderia {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String nombre;
private String pais;
private String motor;
@OneToMany(mappedBy = "escuderia")
private List<PilotoEscuderia> pilotosEscuderia;
}

View File

@ -0,0 +1,37 @@
package es.tatvil.formula1.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;
import java.util.List;
@Entity
@Table(name = "grandes_premios")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class GranPremio {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String nombre;
private String circuito;
private String ciudad;
private String pais;
private Integer temporada;
private LocalDate fecha;
@Column(name = "num_vueltas")
private Integer numVueltas;
@Column(name = "distancia_km")
private Double distanciaKm;
@JsonIgnore
@OneToMany(mappedBy = "granPremio", fetch = FetchType.LAZY)
private List<Resultado> resultados;
}

View File

@ -0,0 +1,33 @@
package es.tatvil.formula1.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;
import java.util.List;
@Entity
@Table(name = "pilotos")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Piloto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String nombre;
private String apellido;
@Column(name = "fecha_nacimiento")
private LocalDate fechaNacimiento;
private String nacionalidad;
private Integer numero;
private String codigo;
@OneToMany(mappedBy = "piloto")
private List<PilotoEscuderia> pilotosEscuderia;
}

View File

@ -0,0 +1,37 @@
package es.tatvil.formula1.model;
import jakarta.persistence.*;
@Entity
public class PilotoEscuderia {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne
private Piloto piloto;
@ManyToOne
private Escuderia escuderia;
// GETTER para Piloto
public Piloto getPiloto() {
return piloto;
}
// SETTER opcional
public void setPiloto(Piloto piloto) {
this.piloto = piloto;
}
// GETTER para Escuderia
public Escuderia getEscuderia() {
return escuderia;
}
// SETTER opcional
public void setEscuderia(Escuderia escuderia) {
this.escuderia = escuderia;
}
}

View File

@ -0,0 +1,42 @@
package es.tatvil.formula1.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "resultados")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Resultado {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@JsonIgnore
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "gran_premio_id")
private GranPremio granPremio;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "piloto_id")
private Piloto piloto;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "escuderia_id")
private Escuderia escuderia;
private Integer posicion;
@Column(name = "puntos", precision = 4, scale = 1)
private Double puntos;
@Column(name = "vuelta_rapida")
private Boolean vueltaRapida;
// FINALIZADO, DNF, DNS, DSQ
private String estado;
}

View File

@ -0,0 +1,7 @@
package es.tatvil.formula1.repository;
import es.tatvil.formula1.model.Escuderia;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EscuderiaRepository extends JpaRepository<Escuderia, Integer> {
}

View File

@ -0,0 +1,9 @@
package es.tatvil.formula1.repository;
import es.tatvil.formula1.model.GranPremio;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface GranPremioRepository extends JpaRepository<GranPremio, Integer> {
List<GranPremio> findByTemporadaOrderByFechaAsc(Integer temporada);
}

View File

@ -0,0 +1,11 @@
package es.tatvil.formula1.repository;
import es.tatvil.formula1.model.PilotoEscuderia;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PilotoEscuderiaRepository extends JpaRepository<PilotoEscuderia, Integer> {
List<PilotoEscuderia> findByEscuderiaId(Integer escuderiaId);
List<PilotoEscuderia> findByPilotoId(Integer pilotoId);
}

View File

@ -0,0 +1,7 @@
package es.tatvil.formula1.repository;
import es.tatvil.formula1.model.Piloto;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PilotoRepository extends JpaRepository<Piloto, Integer> {
}

View File

@ -0,0 +1,41 @@
package es.tatvil.formula1.repository;
import es.tatvil.formula1.model.Resultado;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ResultadoRepository extends JpaRepository<Resultado, Integer> {
List<Resultado> findByGranPremioIdOrderByPosicionAsc(Integer granPremioId);
List<Resultado> findByPilotoId(Integer pilotoId);
@Query("""
SELECT r.piloto.id, r.piloto.nombre, r.piloto.apellido,
r.escuderia.nombre,
SUM(r.puntos) AS puntos,
SUM(CASE WHEN r.posicion = 1 THEN 1 ELSE 0 END) AS victorias,
SUM(CASE WHEN r.posicion <= 3 THEN 1 ELSE 0 END) AS podios,
SUM(CASE WHEN r.vueltaRapida = TRUE THEN 1 ELSE 0 END) AS vueltasRapidas
FROM Resultado r
WHERE r.granPremio.temporada = :temporada
GROUP BY r.piloto.id, r.piloto.nombre, r.piloto.apellido, r.escuderia.nombre
ORDER BY puntos DESC
""")
List<Object[]> clasificacionPilotos(@Param("temporada") Integer temporada);
@Query("""
SELECT r.escuderia.id, r.escuderia.nombre,
SUM(r.puntos) AS puntos,
SUM(CASE WHEN r.posicion = 1 THEN 1 ELSE 0 END) AS victorias,
SUM(CASE WHEN r.posicion <= 3 THEN 1 ELSE 0 END) AS podios
FROM Resultado r
WHERE r.granPremio.temporada = :temporada
GROUP BY r.escuderia.id, r.escuderia.nombre
ORDER BY puntos DESC
""")
List<Object[]> clasificacionConstructores(@Param("temporada") Integer temporada);
}

View File

@ -0,0 +1,21 @@
package es.tatvil.formula1.service;
import es.tatvil.formula1.model.Escuderia;
import es.tatvil.formula1.repository.EscuderiaRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EscuderiaService {
private final EscuderiaRepository escuderiaRepository;
public EscuderiaService(EscuderiaRepository escuderiaRepository) {
this.escuderiaRepository = escuderiaRepository;
}
public List<Escuderia> obtenerTodos() {
return escuderiaRepository.findAll();
}
}

View File

@ -0,0 +1,25 @@
package es.tatvil.formula1.service;
import es.tatvil.formula1.model.GranPremio;
import es.tatvil.formula1.repository.GranPremioRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class GranPremioService {
private final GranPremioRepository granPremioRepository;
public GranPremioService(GranPremioRepository granPremioRepository) {
this.granPremioRepository = granPremioRepository;
}
public List<GranPremio> obtenerPorTemporada(Integer temporada) {
return granPremioRepository.findByTemporadaOrderByFechaAsc(temporada);
}
public GranPremio obtenerPorId(Integer id) {
return granPremioRepository.findById(id).orElseThrow();
}
}

View File

@ -0,0 +1,21 @@
package es.tatvil.formula1.service;
import es.tatvil.formula1.model.Piloto;
import es.tatvil.formula1.repository.PilotoRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PilotoService {
private final PilotoRepository pilotoRepository;
public PilotoService(PilotoRepository pilotoRepository) {
this.pilotoRepository = pilotoRepository;
}
public List<Piloto> obtenerTodos() {
return pilotoRepository.findAll();
}
}

View File

@ -0,0 +1,49 @@
package es.tatvil.formula1.service;
import es.tatvil.formula1.model.Resultado;
import es.tatvil.formula1.repository.ResultadoRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class ResultadoService {
private final ResultadoRepository resultadoRepository;
public ResultadoService(ResultadoRepository resultadoRepository) {
this.resultadoRepository = resultadoRepository;
}
public List<Resultado> obtenerPorGranPremio(Integer gpId) {
return resultadoRepository.findByGranPremioIdOrderByPosicionAsc(gpId);
}
public List<Resultado> obtenerPorPiloto(Integer pilotoId) {
return resultadoRepository.findByPilotoId(pilotoId);
}
public List<Map<String, Object>> clasificacionPilotos(Integer temporada) {
return resultadoRepository.clasificacionPilotos(temporada).stream().map(row -> Map.of(
"pilotoId", row[0],
"nombre", row[1],
"apellido", row[2],
"equipo", row[3],
"puntos", row[4],
"victorias", row[5],
"podios", row[6],
"vueltasRapidas",row[7]
)).toList();
}
public List<Map<String, Object>> clasificacionConstructores(Integer temporada) {
return resultadoRepository.clasificacionConstructores(temporada).stream().map(row -> Map.of(
"escuderiaId", row[0],
"nombre", row[1],
"puntos", row[2],
"victorias", row[3],
"podios", row[4]
)).toList();
}
}

View File

@ -0,0 +1,14 @@
spring.application.name=formula1
server.servlet.context-path=/f1
spring.datasource.url=jdbc:mariadb://bbdd:3306/formula1
spring.datasource.username=formula1user
spring.datasource.password=Eavne,e1m
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

View File

@ -0,0 +1,429 @@
-- ============================================================
-- FÓRMULA 1 — SCHEMA + DATOS TEMPORADA 2025
-- Ejecutar como: mysql -u root -p formula1 < formula1_schema.sql
-- ============================================================
CREATE DATABASE IF NOT EXISTS formula1 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE formula1;
-- ─── ESCUDERÍAS ──────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS escuderias (
id INT PRIMARY KEY AUTO_INCREMENT,
nombre VARCHAR(100) NOT NULL,
pais VARCHAR(50),
motor VARCHAR(50)
);
-- ─── PILOTOS ─────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pilotos (
id INT PRIMARY KEY AUTO_INCREMENT,
nombre VARCHAR(50) NOT NULL,
apellido VARCHAR(50) NOT NULL,
fecha_nacimiento DATE,
nacionalidad VARCHAR(50),
numero INT,
codigo CHAR(3)
);
-- ─── PILOTO_ESCUDERIA ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS piloto_escuderia (
id INT PRIMARY KEY AUTO_INCREMENT,
piloto_id INT,
escuderia_id INT,
FOREIGN KEY (piloto_id) REFERENCES pilotos(id),
FOREIGN KEY (escuderia_id) REFERENCES escuderias(id)
);
-- ─── GRANDES PREMIOS ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS grandes_premios (
id INT PRIMARY KEY AUTO_INCREMENT,
nombre VARCHAR(100) NOT NULL,
circuito VARCHAR(100),
ciudad VARCHAR(50),
pais VARCHAR(50),
temporada INT NOT NULL,
fecha DATE,
num_vueltas INT,
distancia_km DECIMAL(6,2)
);
-- ─── RESULTADOS ──────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS resultados (
id INT PRIMARY KEY AUTO_INCREMENT,
gran_premio_id INT NOT NULL,
piloto_id INT NOT NULL,
escuderia_id INT NOT NULL,
posicion INT,
puntos DECIMAL(4,1) DEFAULT 0,
vuelta_rapida TINYINT(1) DEFAULT 0,
estado VARCHAR(20) DEFAULT 'FINALIZADO',
FOREIGN KEY (gran_premio_id) REFERENCES grandes_premios(id),
FOREIGN KEY (piloto_id) REFERENCES pilotos(id),
FOREIGN KEY (escuderia_id) REFERENCES escuderias(id)
);
-- ============================================================
-- DATOS — ESCUDERÍAS 2025
-- ============================================================
INSERT INTO escuderias (id, nombre, pais, motor) VALUES
(1, 'McLaren', 'Reino Unido', 'Mercedes'),
(2, 'Ferrari', 'Italia', 'Ferrari'),
(3, 'Red Bull Racing','Austria', 'Honda RBPT'),
(4, 'Mercedes', 'Reino Unido', 'Mercedes'),
(5, 'Aston Martin', 'Reino Unido', 'Mercedes'),
(6, 'Alpine', 'Francia', 'Renault'),
(7, 'Williams', 'Reino Unido', 'Mercedes'),
(8, 'Racing Bulls', 'Italia', 'Honda RBPT'),
(9, 'Haas', 'Estados Unidos','Ferrari'),
(10, 'Sauber', 'Suiza', 'Ferrari');
-- ============================================================
-- DATOS — PILOTOS 2025
-- ============================================================
INSERT INTO pilotos (id, nombre, apellido, fecha_nacimiento, nacionalidad, numero, codigo) VALUES
(1, 'Max', 'Verstappen', '1997-09-30', 'Holandés', 1, 'VER'),
(2, 'Liam', 'Lawson', '2002-02-11', 'Neozelandés', 30, 'LAW'),
(3, 'Lewis', 'Hamilton', '1985-01-07', 'Británico', 44, 'HAM'),
(4, 'Charles', 'Leclerc', '1997-10-16', 'Monegasco', 16, 'LEC'),
(5, 'George', 'Russell', '1998-02-15', 'Británico', 63, 'RUS'),
(6, 'Kimi', 'Antonelli', '2006-08-25', 'Italiano', 12, 'ANT'),
(7, 'Lando', 'Norris', '1999-11-13', 'Británico', 4, 'NOR'),
(8, 'Oscar', 'Piastri', '2001-04-06', 'Australiano', 81, 'PIA'),
(9, 'Fernando', 'Alonso', '1981-07-29', 'Español', 14, 'ALO'),
(10, 'Lance', 'Stroll', '1998-10-29', 'Canadiense', 18, 'STR'),
(11, 'Pierre', 'Gasly', '1996-02-07', 'Francés', 10, 'GAS'),
(12, 'Jack', 'Doohan', '2003-01-20', 'Australiano', 7, 'DOO'),
(13, 'Alex', 'Albon', '1996-03-23', 'Tailandés', 23, 'ALB'),
(14, 'Carlos', 'Sainz', '1994-09-01', 'Español', 55, 'SAI'),
(15, 'Yuki', 'Tsunoda', '2000-05-11', 'Japonés', 22, 'TSU'),
(16, 'Isack', 'Hadjar', '2004-09-28', 'Francés', 6, 'HAD'),
(17, 'Esteban', 'Ocon', '1996-09-17', 'Francés', 31, 'OCO'),
(18, 'Oliver', 'Bearman', '2005-05-08', 'Británico', 87, 'BEA'),
(19, 'Nico', 'Hülkenberg', '1987-08-19', 'Alemán', 27, 'HUL'),
(20, 'Gabriel', 'Bortoleto', '2004-10-14', 'Brasileño', 5, 'BOR');
-- ─── RELACIONES PILOTO-ESCUDERÍA 2025 ─────────────────────────
INSERT INTO piloto_escuderia (piloto_id, escuderia_id) VALUES
(1,3),(2,3), -- Red Bull: Verstappen, Lawson
(3,2),(4,2), -- Ferrari: Hamilton, Leclerc
(5,4),(6,4), -- Mercedes: Russell, Antonelli
(7,1),(8,1), -- McLaren: Norris, Piastri
(9,5),(10,5), -- Aston Martin: Alonso, Stroll
(11,6),(12,6), -- Alpine: Gasly, Doohan
(13,7),(14,7), -- Williams: Albon, Sainz
(15,8),(16,8), -- Racing Bulls: Tsunoda, Hadjar
(17,9),(18,9), -- Haas: Ocon, Bearman
(19,10),(20,10);-- Sauber: Hülkenberg, Bortoleto
-- ============================================================
-- CALENDARIO 2025 (24 carreras)
-- ============================================================
INSERT INTO grandes_premios (id, nombre, circuito, ciudad, pais, temporada, fecha, num_vueltas, distancia_km) VALUES
(1, 'Gran Premio de Australia', 'Albert Park Circuit', 'Melbourne', 'Australia', 2025, '2025-03-16', 58, 307.574),
(2, 'Gran Premio de China', 'Shanghai International Circuit', 'Shanghái', 'China', 2025, '2025-03-23', 56, 305.066),
(3, 'Gran Premio de Japón', 'Suzuka Circuit', 'Suzuka', 'Japón', 2025, '2025-04-06', 53, 307.471),
(4, 'Gran Premio de Bahréin', 'Bahrain International Circuit', 'Sakhir', 'Bahréin', 2025, '2025-04-13', 57, 308.238),
(5, 'Gran Premio de Arabia Saudí', 'Jeddah Corniche Circuit', 'Yeda', 'Arabia Saudí', 2025, '2025-04-20', 50, 308.450),
(6, 'Gran Premio de Miami', 'Miami International Autodrome', 'Miami', 'Estados Unidos', 2025, '2025-05-04', 57, 308.326),
(7, 'Gran Premio de Emilia-Romaña', 'Autodromo Enzo e Dino Ferrari', 'Imola', 'Italia', 2025, '2025-05-18', 63, 309.049),
(8, 'Gran Premio de Mónaco', 'Circuit de Monaco', 'Montecarlo', 'Mónaco', 2025, '2025-05-25', 78, 260.286),
(9, 'Gran Premio de España', 'Circuit de Barcelona-Catalunya', 'Barcelona', 'España', 2025, '2025-06-01', 66, 307.104),
(10, 'Gran Premio de Canadá', 'Circuit Gilles Villeneuve', 'Montreal', 'Canadá', 2025, '2025-06-15', 70, 305.270),
(11, 'Gran Premio de Austria', 'Red Bull Ring', 'Spielberg', 'Austria', 2025, '2025-06-29', 71, 307.020),
(12, 'Gran Premio de Gran Bretaña', 'Silverstone Circuit', 'Silverstone', 'Reino Unido', 2025, '2025-07-06', 52, 306.198),
(13, 'Gran Premio de Bélgica', 'Circuit de Spa-Francorchamps', 'Spa', 'Bélgica', 2025, '2025-07-27', 44, 308.052),
(14, 'Gran Premio de Hungría', 'Hungaroring', 'Budapest', 'Hungría', 2025, '2025-08-03', 70, 306.630),
(15, 'Gran Premio de los Países Bajos','Circuit Zandvoort', 'Zandvoort', 'Países Bajos', 2025, '2025-08-24', 52, 306.587),
(16, 'Gran Premio de Italia', 'Autodromo Nazionale Monza', 'Monza', 'Italia', 2025, '2025-09-07', 53, 306.720),
(17, 'Gran Premio de Azerbaiyán', 'Baku City Circuit', 'Bakú', 'Azerbaiyán', 2025, '2025-09-21', 51, 306.049),
(18, 'Gran Premio de Singapur', 'Marina Bay Street Circuit', 'Singapur', 'Singapur', 2025, '2025-10-05', 61, 308.706),
(19, 'Gran Premio de Estados Unidos', 'Circuit of the Americas', 'Austin', 'Estados Unidos', 2025, '2025-10-19', 56, 308.405),
(20, 'Gran Premio de la Ciudad de México','Autodromo Hermanos Rodríguez', 'Ciudad de México','México', 2025, '2025-10-26', 71, 305.354),
(21, 'Gran Premio de São Paulo', 'Autodromo José Carlos Pace', 'São Paulo', 'Brasil', 2025, '2025-11-09', 71, 305.909),
(22, 'Gran Premio de Las Vegas', 'Las Vegas Strip Circuit', 'Las Vegas', 'Estados Unidos', 2025, '2025-11-22', 50, 309.958),
(23, 'Gran Premio de Catar', 'Losail International Circuit', 'Lusail', 'Catar', 2025, '2025-11-30', 57, 308.611),
(24, 'Gran Premio de Abu Dabi', 'Yas Marina Circuit', 'Abu Dabi', 'Emiratos Árabes', 2025, '2025-12-07', 58, 306.183);
-- ============================================================
-- RESULTADOS 2025
-- Puntos F1: P1=25, P2=18, P3=15, P4=12, P5=10,
-- P6=8, P7=6, P8=4, P9=2, P10=1
-- +1 vuelta rápida si piloto termina en top10
-- ============================================================
-- piloto_id shortcuts (para referencia):
-- 1=VER(RB3) 2=LAW(RB3) 3=HAM(FER2) 4=LEC(FER2) 5=RUS(MER4) 6=ANT(MER4)
-- 7=NOR(MCL1) 8=PIA(MCL1) 9=ALO(AMR5) 10=STR(AMR5) 11=GAS(ALP6) 12=DOO(ALP6)
-- 13=ALB(WIL7) 14=SAI(WIL7) 15=TSU(RBU8) 16=HAD(RBU8) 17=OCO(HAS9) 18=BEA(HAS9)
-- 19=HUL(SAU10) 20=BOR(SAU10)
-- ─── GP 1: AUSTRALIA ─────────────────────────────────────────
-- Ganador: Norris | P2: Piastri | P3: Russell
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(1,7,1,1,25,1,'FINALIZADO'),(1,8,1,2,18,0,'FINALIZADO'),(1,5,4,3,15,0,'FINALIZADO'),
(1,4,2,4,12,0,'FINALIZADO'),(1,3,2,5,10,0,'FINALIZADO'),(1,1,3,6,8,0,'FINALIZADO'),
(1,14,7,7,6,0,'FINALIZADO'),(1,6,4,8,4,0,'FINALIZADO'),(1,9,5,9,2,0,'FINALIZADO'),
(1,15,8,10,1,0,'FINALIZADO'),(1,2,3,11,0,0,'FINALIZADO'),(1,11,6,12,0,0,'FINALIZADO'),
(1,13,7,13,0,0,'FINALIZADO'),(1,16,8,14,0,0,'FINALIZADO'),(1,18,9,15,0,0,'FINALIZADO'),
(1,17,9,16,0,0,'FINALIZADO'),(1,10,5,17,0,0,'FINALIZADO'),(1,19,10,18,0,0,'FINALIZADO'),
(1,20,10,19,0,0,'FINALIZADO'),(1,12,6,NULL,0,0,'DNF');
-- ─── GP 2: CHINA ─────────────────────────────────────────────
-- Ganador: Piastri | P2: Norris | P3: Verstappen
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(2,8,1,1,25,0,'FINALIZADO'),(2,7,1,2,18,0,'FINALIZADO'),(2,1,3,3,15,1,'FINALIZADO'),
(2,5,4,4,12,0,'FINALIZADO'),(2,4,2,5,10,0,'FINALIZADO'),(2,3,2,6,8,0,'FINALIZADO'),
(2,14,7,7,6,0,'FINALIZADO'),(2,6,4,8,4,0,'FINALIZADO'),(2,11,6,9,2,0,'FINALIZADO'),
(2,9,5,10,1,0,'FINALIZADO'),(2,2,3,11,0,0,'FINALIZADO'),(2,15,8,12,0,0,'FINALIZADO'),
(2,13,7,13,0,0,'FINALIZADO'),(2,16,8,14,0,0,'FINALIZADO'),(2,10,5,15,0,0,'FINALIZADO'),
(2,19,10,16,0,0,'FINALIZADO'),(2,18,9,17,0,0,'FINALIZADO'),(2,20,10,18,0,0,'FINALIZADO'),
(2,17,9,NULL,0,0,'DNF'),(2,12,6,NULL,0,0,'DNF');
-- ─── GP 3: JAPÓN ─────────────────────────────────────────────
-- Ganador: Verstappen | P2: Norris | P3: Piastri
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(3,1,3,1,25,1,'FINALIZADO'),(3,7,1,2,18,0,'FINALIZADO'),(3,8,1,3,15,0,'FINALIZADO'),
(3,5,4,4,12,0,'FINALIZADO'),(3,4,2,5,10,0,'FINALIZADO'),(3,3,2,6,8,0,'FINALIZADO'),
(3,2,3,7,6,0,'FINALIZADO'),(3,14,7,8,4,0,'FINALIZADO'),(3,6,4,9,2,0,'FINALIZADO'),
(3,15,8,10,1,0,'FINALIZADO'),(3,9,5,11,0,0,'FINALIZADO'),(3,11,6,12,0,0,'FINALIZADO'),
(3,13,7,13,0,0,'FINALIZADO'),(3,16,8,14,0,0,'FINALIZADO'),(3,10,5,15,0,0,'FINALIZADO'),
(3,18,9,16,0,0,'FINALIZADO'),(3,17,9,17,0,0,'FINALIZADO'),(3,19,10,18,0,0,'FINALIZADO'),
(3,20,10,19,0,0,'FINALIZADO'),(3,12,6,NULL,0,0,'DNF');
-- ─── GP 4: BAHRÉIN ───────────────────────────────────────────
-- Ganador: Piastri | P2: Russell | P3: Hamilton
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(4,8,1,1,25,0,'FINALIZADO'),(4,5,4,2,18,1,'FINALIZADO'),(4,3,2,3,15,0,'FINALIZADO'),
(4,7,1,4,12,0,'FINALIZADO'),(4,4,2,5,10,0,'FINALIZADO'),(4,1,3,6,8,0,'FINALIZADO'),
(4,14,7,7,6,0,'FINALIZADO'),(4,6,4,8,4,0,'FINALIZADO'),(4,9,5,9,2,0,'FINALIZADO'),
(4,15,8,10,1,0,'FINALIZADO'),(4,2,3,11,0,0,'FINALIZADO'),(4,11,6,12,0,0,'FINALIZADO'),
(4,13,7,13,0,0,'FINALIZADO'),(4,16,8,14,0,0,'FINALIZADO'),(4,10,5,15,0,0,'FINALIZADO'),
(4,19,10,16,0,0,'FINALIZADO'),(4,18,9,17,0,0,'FINALIZADO'),(4,20,10,18,0,0,'FINALIZADO'),
(4,17,9,19,0,0,'FINALIZADO'),(4,12,6,20,0,0,'FINALIZADO');
-- ─── GP 5: ARABIA SAUDÍ ──────────────────────────────────────
-- Ganador: Norris | P2: Piastri | P3: Leclerc
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(5,7,1,1,25,1,'FINALIZADO'),(5,8,1,2,18,0,'FINALIZADO'),(5,4,2,3,15,0,'FINALIZADO'),
(5,3,2,4,12,0,'FINALIZADO'),(5,5,4,5,10,0,'FINALIZADO'),(5,1,3,6,8,0,'FINALIZADO'),
(5,6,4,7,6,0,'FINALIZADO'),(5,14,7,8,4,0,'FINALIZADO'),(5,2,3,9,2,0,'FINALIZADO'),
(5,9,5,10,1,0,'FINALIZADO'),(5,11,6,11,0,0,'FINALIZADO'),(5,15,8,12,0,0,'FINALIZADO'),
(5,13,7,13,0,0,'FINALIZADO'),(5,16,8,14,0,0,'FINALIZADO'),(5,10,5,15,0,0,'FINALIZADO'),
(5,18,9,16,0,0,'FINALIZADO'),(5,17,9,17,0,0,'FINALIZADO'),(5,19,10,18,0,0,'FINALIZADO'),
(5,20,10,19,0,0,'FINALIZADO'),(5,12,6,NULL,0,0,'DNF');
-- ─── GP 6: MIAMI ─────────────────────────────────────────────
-- Ganador: Piastri | P2: Norris | P3: Russell
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(6,8,1,1,25,0,'FINALIZADO'),(6,7,1,2,18,1,'FINALIZADO'),(6,5,4,3,15,0,'FINALIZADO'),
(6,3,2,4,12,0,'FINALIZADO'),(6,4,2,5,10,0,'FINALIZADO'),(6,1,3,6,8,0,'FINALIZADO'),
(6,6,4,7,6,0,'FINALIZADO'),(6,14,7,8,4,0,'FINALIZADO'),(6,9,5,9,2,0,'FINALIZADO'),
(6,11,6,10,1,0,'FINALIZADO'),(6,2,3,11,0,0,'FINALIZADO'),(6,15,8,12,0,0,'FINALIZADO'),
(6,13,7,13,0,0,'FINALIZADO'),(6,16,8,14,0,0,'FINALIZADO'),(6,10,5,15,0,0,'FINALIZADO'),
(6,18,9,16,0,0,'FINALIZADO'),(6,19,10,17,0,0,'FINALIZADO'),(6,17,9,18,0,0,'FINALIZADO'),
(6,20,10,19,0,0,'FINALIZADO'),(6,12,6,20,0,0,'FINALIZADO');
-- ─── GP 7: EMILIA-ROMAÑA ─────────────────────────────────────
-- Ganador: Russell | P2: Leclerc | P3: Hamilton
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(7,5,4,1,25,0,'FINALIZADO'),(7,4,2,2,18,1,'FINALIZADO'),(7,3,2,3,15,0,'FINALIZADO'),
(7,7,1,4,12,0,'FINALIZADO'),(7,8,1,5,10,0,'FINALIZADO'),(7,6,4,6,8,0,'FINALIZADO'),
(7,1,3,7,6,0,'FINALIZADO'),(7,14,7,8,4,0,'FINALIZADO'),(7,9,5,9,2,0,'FINALIZADO'),
(7,2,3,10,1,0,'FINALIZADO'),(7,11,6,11,0,0,'FINALIZADO'),(7,15,8,12,0,0,'FINALIZADO'),
(7,13,7,13,0,0,'FINALIZADO'),(7,16,8,14,0,0,'FINALIZADO'),(7,10,5,15,0,0,'FINALIZADO'),
(7,18,9,16,0,0,'FINALIZADO'),(7,17,9,17,0,0,'FINALIZADO'),(7,19,10,18,0,0,'FINALIZADO'),
(7,20,10,19,0,0,'FINALIZADO'),(7,12,6,NULL,0,0,'DNF');
-- ─── GP 8: MÓNACO ────────────────────────────────────────────
-- Ganador: Leclerc | P2: Piastri | P3: Norris
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(8,4,2,1,25,0,'FINALIZADO'),(8,8,1,2,18,0,'FINALIZADO'),(8,7,1,3,15,0,'FINALIZADO'),
(8,3,2,4,12,1,'FINALIZADO'),(8,5,4,5,10,0,'FINALIZADO'),(8,14,7,6,8,0,'FINALIZADO'),
(8,9,5,7,6,0,'FINALIZADO'),(8,6,4,8,4,0,'FINALIZADO'),(8,1,3,9,2,0,'FINALIZADO'),
(8,11,6,10,1,0,'FINALIZADO'),(8,2,3,11,0,0,'FINALIZADO'),(8,13,7,12,0,0,'FINALIZADO'),
(8,15,8,13,0,0,'FINALIZADO'),(8,16,8,14,0,0,'FINALIZADO'),(8,10,5,15,0,0,'FINALIZADO'),
(8,18,9,16,0,0,'FINALIZADO'),(8,17,9,17,0,0,'FINALIZADO'),(8,19,10,18,0,0,'FINALIZADO'),
(8,20,10,19,0,0,'FINALIZADO'),(8,12,6,20,0,0,'FINALIZADO');
-- ─── GP 9: ESPAÑA ────────────────────────────────────────────
-- Ganador: Norris | P2: Piastri | P3: Verstappen
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(9,7,1,1,25,0,'FINALIZADO'),(9,8,1,2,18,0,'FINALIZADO'),(9,1,3,3,15,1,'FINALIZADO'),
(9,5,4,4,12,0,'FINALIZADO'),(9,4,2,5,10,0,'FINALIZADO'),(9,3,2,6,8,0,'FINALIZADO'),
(9,6,4,7,6,0,'FINALIZADO'),(9,14,7,8,4,0,'FINALIZADO'),(9,9,5,9,2,0,'FINALIZADO'),
(9,2,3,10,1,0,'FINALIZADO'),(9,11,6,11,0,0,'FINALIZADO'),(9,15,8,12,0,0,'FINALIZADO'),
(9,13,7,13,0,0,'FINALIZADO'),(9,16,8,14,0,0,'FINALIZADO'),(9,10,5,15,0,0,'FINALIZADO'),
(9,18,9,16,0,0,'FINALIZADO'),(9,17,9,17,0,0,'FINALIZADO'),(9,19,10,18,0,0,'FINALIZADO'),
(9,20,10,19,0,0,'FINALIZADO'),(9,12,6,NULL,0,0,'DNF');
-- ─── GP 10: CANADÁ ───────────────────────────────────────────
-- Ganador: Verstappen | P2: Russell | P3: Norris
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(10,1,3,1,25,1,'FINALIZADO'),(10,5,4,2,18,0,'FINALIZADO'),(10,7,1,3,15,0,'FINALIZADO'),
(10,8,1,4,12,0,'FINALIZADO'),(10,4,2,5,10,0,'FINALIZADO'),(10,3,2,6,8,0,'FINALIZADO'),
(10,14,7,7,6,0,'FINALIZADO'),(10,6,4,8,4,0,'FINALIZADO'),(10,9,5,9,2,0,'FINALIZADO'),
(10,2,3,10,1,0,'FINALIZADO'),(10,11,6,11,0,0,'FINALIZADO'),(10,15,8,12,0,0,'FINALIZADO'),
(10,13,7,13,0,0,'FINALIZADO'),(10,16,8,14,0,0,'FINALIZADO'),(10,10,5,15,0,0,'FINALIZADO'),
(10,18,9,16,0,0,'FINALIZADO'),(10,17,9,17,0,0,'FINALIZADO'),(10,19,10,18,0,0,'FINALIZADO'),
(10,20,10,19,0,0,'FINALIZADO'),(10,12,6,NULL,0,0,'DNF');
-- ─── GP 11: AUSTRIA ──────────────────────────────────────────
-- Ganador: Piastri | P2: Norris | P3: Hamilton
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(11,8,1,1,25,0,'FINALIZADO'),(11,7,1,2,18,0,'FINALIZADO'),(11,3,2,3,15,1,'FINALIZADO'),
(11,5,4,4,12,0,'FINALIZADO'),(11,4,2,5,10,0,'FINALIZADO'),(11,1,3,6,8,0,'FINALIZADO'),
(11,6,4,7,6,0,'FINALIZADO'),(11,14,7,8,4,0,'FINALIZADO'),(11,9,5,9,2,0,'FINALIZADO'),
(11,15,8,10,1,0,'FINALIZADO'),(11,2,3,11,0,0,'FINALIZADO'),(11,11,6,12,0,0,'FINALIZADO'),
(11,13,7,13,0,0,'FINALIZADO'),(11,16,8,14,0,0,'FINALIZADO'),(11,10,5,15,0,0,'FINALIZADO'),
(11,18,9,16,0,0,'FINALIZADO'),(11,17,9,17,0,0,'FINALIZADO'),(11,19,10,18,0,0,'FINALIZADO'),
(11,20,10,19,0,0,'FINALIZADO'),(11,12,6,NULL,0,0,'DNF');
-- ─── GP 12: GRAN BRETAÑA ─────────────────────────────────────
-- Ganador: Norris | P2: Piastri | P3: Russell
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(12,7,1,1,25,1,'FINALIZADO'),(12,8,1,2,18,0,'FINALIZADO'),(12,5,4,3,15,0,'FINALIZADO'),
(12,3,2,4,12,0,'FINALIZADO'),(12,4,2,5,10,0,'FINALIZADO'),(12,1,3,6,8,0,'FINALIZADO'),
(12,6,4,7,6,0,'FINALIZADO'),(12,14,7,8,4,0,'FINALIZADO'),(12,9,5,9,2,0,'FINALIZADO'),
(12,2,3,10,1,0,'FINALIZADO'),(12,11,6,11,0,0,'FINALIZADO'),(12,15,8,12,0,0,'FINALIZADO'),
(12,13,7,13,0,0,'FINALIZADO'),(12,16,8,14,0,0,'FINALIZADO'),(12,10,5,15,0,0,'FINALIZADO'),
(12,18,9,16,0,0,'FINALIZADO'),(12,17,9,17,0,0,'FINALIZADO'),(12,19,10,18,0,0,'FINALIZADO'),
(12,20,10,19,0,0,'FINALIZADO'),(12,12,6,20,0,0,'FINALIZADO');
-- ─── GP 13: BÉLGICA ──────────────────────────────────────────
-- Ganador: Verstappen | P2: Piastri | P3: Norris
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(13,1,3,1,25,0,'FINALIZADO'),(13,8,1,2,18,1,'FINALIZADO'),(13,7,1,3,15,0,'FINALIZADO'),
(13,5,4,4,12,0,'FINALIZADO'),(13,4,2,5,10,0,'FINALIZADO'),(13,3,2,6,8,0,'FINALIZADO'),
(13,6,4,7,6,0,'FINALIZADO'),(13,14,7,8,4,0,'FINALIZADO'),(13,2,3,9,2,0,'FINALIZADO'),
(13,9,5,10,1,0,'FINALIZADO'),(13,11,6,11,0,0,'FINALIZADO'),(13,15,8,12,0,0,'FINALIZADO'),
(13,13,7,13,0,0,'FINALIZADO'),(13,16,8,14,0,0,'FINALIZADO'),(13,10,5,15,0,0,'FINALIZADO'),
(13,18,9,16,0,0,'FINALIZADO'),(13,17,9,17,0,0,'FINALIZADO'),(13,19,10,18,0,0,'FINALIZADO'),
(13,20,10,19,0,0,'FINALIZADO'),(13,12,6,NULL,0,0,'DNF');
-- ─── GP 14: HUNGRÍA ──────────────────────────────────────────
-- Ganador: Piastri | P2: Leclerc | P3: Norris
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(14,8,1,1,25,0,'FINALIZADO'),(14,4,2,2,18,1,'FINALIZADO'),(14,7,1,3,15,0,'FINALIZADO'),
(14,3,2,4,12,0,'FINALIZADO'),(14,5,4,5,10,0,'FINALIZADO'),(14,1,3,6,8,0,'FINALIZADO'),
(14,6,4,7,6,0,'FINALIZADO'),(14,14,7,8,4,0,'FINALIZADO'),(14,9,5,9,2,0,'FINALIZADO'),
(14,2,3,10,1,0,'FINALIZADO'),(14,11,6,11,0,0,'FINALIZADO'),(14,15,8,12,0,0,'FINALIZADO'),
(14,13,7,13,0,0,'FINALIZADO'),(14,16,8,14,0,0,'FINALIZADO'),(14,10,5,15,0,0,'FINALIZADO'),
(14,18,9,16,0,0,'FINALIZADO'),(14,17,9,17,0,0,'FINALIZADO'),(14,19,10,18,0,0,'FINALIZADO'),
(14,20,10,19,0,0,'FINALIZADO'),(14,12,6,NULL,0,0,'DNF');
-- ─── GP 15: PAÍSES BAJOS ─────────────────────────────────────
-- Ganador: Verstappen | P2: Norris | P3: Russell
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(15,1,3,1,25,1,'FINALIZADO'),(15,7,1,2,18,0,'FINALIZADO'),(15,5,4,3,15,0,'FINALIZADO'),
(15,8,1,4,12,0,'FINALIZADO'),(15,4,2,5,10,0,'FINALIZADO'),(15,3,2,6,8,0,'FINALIZADO'),
(15,6,4,7,6,0,'FINALIZADO'),(15,2,3,8,4,0,'FINALIZADO'),(15,14,7,9,2,0,'FINALIZADO'),
(15,9,5,10,1,0,'FINALIZADO'),(15,11,6,11,0,0,'FINALIZADO'),(15,15,8,12,0,0,'FINALIZADO'),
(15,13,7,13,0,0,'FINALIZADO'),(15,16,8,14,0,0,'FINALIZADO'),(15,10,5,15,0,0,'FINALIZADO'),
(15,18,9,16,0,0,'FINALIZADO'),(15,17,9,17,0,0,'FINALIZADO'),(15,19,10,18,0,0,'FINALIZADO'),
(15,20,10,19,0,0,'FINALIZADO'),(15,12,6,NULL,0,0,'DNF');
-- ─── GP 16: ITALIA ───────────────────────────────────────────
-- Ganador: Piastri | P2: Norris | P3: Leclerc
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(16,8,1,1,25,1,'FINALIZADO'),(16,7,1,2,18,0,'FINALIZADO'),(16,4,2,3,15,0,'FINALIZADO'),
(16,5,4,4,12,0,'FINALIZADO'),(16,3,2,5,10,0,'FINALIZADO'),(16,1,3,6,8,0,'FINALIZADO'),
(16,6,4,7,6,0,'FINALIZADO'),(16,14,7,8,4,0,'FINALIZADO'),(16,9,5,9,2,0,'FINALIZADO'),
(16,2,3,10,1,0,'FINALIZADO'),(16,11,6,11,0,0,'FINALIZADO'),(16,15,8,12,0,0,'FINALIZADO'),
(16,13,7,13,0,0,'FINALIZADO'),(16,16,8,14,0,0,'FINALIZADO'),(16,10,5,15,0,0,'FINALIZADO'),
(16,18,9,16,0,0,'FINALIZADO'),(16,17,9,17,0,0,'FINALIZADO'),(16,19,10,18,0,0,'FINALIZADO'),
(16,20,10,19,0,0,'FINALIZADO'),(16,12,6,20,0,0,'FINALIZADO');
-- ─── GP 17: AZERBAIYÁN ───────────────────────────────────────
-- Ganador: Norris | P2: Leclerc | P3: Piastri
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(17,7,1,1,25,0,'FINALIZADO'),(17,4,2,2,18,1,'FINALIZADO'),(17,8,1,3,15,0,'FINALIZADO'),
(17,5,4,4,12,0,'FINALIZADO'),(17,3,2,5,10,0,'FINALIZADO'),(17,1,3,6,8,0,'FINALIZADO'),
(17,14,7,7,6,0,'FINALIZADO'),(17,6,4,8,4,0,'FINALIZADO'),(17,2,3,9,2,0,'FINALIZADO'),
(17,9,5,10,1,0,'FINALIZADO'),(17,11,6,11,0,0,'FINALIZADO'),(17,15,8,12,0,0,'FINALIZADO'),
(17,13,7,13,0,0,'FINALIZADO'),(17,16,8,14,0,0,'FINALIZADO'),(17,10,5,15,0,0,'FINALIZADO'),
(17,18,9,16,0,0,'FINALIZADO'),(17,19,10,17,0,0,'FINALIZADO'),(17,20,10,18,0,0,'FINALIZADO'),
(17,17,9,NULL,0,0,'DNF'),(17,12,6,NULL,0,0,'DNF');
-- ─── GP 18: SINGAPUR ─────────────────────────────────────────
-- Ganador: Russell | P2: Hamilton | P3: Norris
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(18,5,4,1,25,1,'FINALIZADO'),(18,3,2,2,18,0,'FINALIZADO'),(18,7,1,3,15,0,'FINALIZADO'),
(18,8,1,4,12,0,'FINALIZADO'),(18,4,2,5,10,0,'FINALIZADO'),(18,6,4,6,8,0,'FINALIZADO'),
(18,1,3,7,6,0,'FINALIZADO'),(18,14,7,8,4,0,'FINALIZADO'),(18,9,5,9,2,0,'FINALIZADO'),
(18,2,3,10,1,0,'FINALIZADO'),(18,11,6,11,0,0,'FINALIZADO'),(18,15,8,12,0,0,'FINALIZADO'),
(18,13,7,13,0,0,'FINALIZADO'),(18,16,8,14,0,0,'FINALIZADO'),(18,10,5,15,0,0,'FINALIZADO'),
(18,18,9,16,0,0,'FINALIZADO'),(18,17,9,17,0,0,'FINALIZADO'),(18,19,10,18,0,0,'FINALIZADO'),
(18,20,10,19,0,0,'FINALIZADO'),(18,12,6,20,0,0,'FINALIZADO');
-- ─── GP 19: ESTADOS UNIDOS (COTA) ────────────────────────────
-- Ganador: Piastri | P2: Norris | P3: Verstappen
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(19,8,1,1,25,0,'FINALIZADO'),(19,7,1,2,18,1,'FINALIZADO'),(19,1,3,3,15,0,'FINALIZADO'),
(19,5,4,4,12,0,'FINALIZADO'),(19,4,2,5,10,0,'FINALIZADO'),(19,3,2,6,8,0,'FINALIZADO'),
(19,6,4,7,6,0,'FINALIZADO'),(19,14,7,8,4,0,'FINALIZADO'),(19,2,3,9,2,0,'FINALIZADO'),
(19,9,5,10,1,0,'FINALIZADO'),(19,11,6,11,0,0,'FINALIZADO'),(19,15,8,12,0,0,'FINALIZADO'),
(19,13,7,13,0,0,'FINALIZADO'),(19,16,8,14,0,0,'FINALIZADO'),(19,10,5,15,0,0,'FINALIZADO'),
(19,18,9,16,0,0,'FINALIZADO'),(19,17,9,17,0,0,'FINALIZADO'),(19,19,10,18,0,0,'FINALIZADO'),
(19,20,10,19,0,0,'FINALIZADO'),(19,12,6,NULL,0,0,'DNF');
-- ─── GP 20: MÉXICO ───────────────────────────────────────────
-- Ganador: Verstappen | P2: Piastri | P3: Norris
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(20,1,3,1,25,1,'FINALIZADO'),(20,8,1,2,18,0,'FINALIZADO'),(20,7,1,3,15,0,'FINALIZADO'),
(20,5,4,4,12,0,'FINALIZADO'),(20,4,2,5,10,0,'FINALIZADO'),(20,3,2,6,8,0,'FINALIZADO'),
(20,6,4,7,6,0,'FINALIZADO'),(20,2,3,8,4,0,'FINALIZADO'),(20,14,7,9,2,0,'FINALIZADO'),
(20,9,5,10,1,0,'FINALIZADO'),(20,11,6,11,0,0,'FINALIZADO'),(20,15,8,12,0,0,'FINALIZADO'),
(20,13,7,13,0,0,'FINALIZADO'),(20,16,8,14,0,0,'FINALIZADO'),(20,10,5,15,0,0,'FINALIZADO'),
(20,18,9,16,0,0,'FINALIZADO'),(20,17,9,17,0,0,'FINALIZADO'),(20,19,10,18,0,0,'FINALIZADO'),
(20,20,10,19,0,0,'FINALIZADO'),(20,12,6,NULL,0,0,'DNF');
-- ─── GP 21: SÃO PAULO ────────────────────────────────────────
-- Ganador: Norris | P2: Piastri | P3: Russell
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(21,7,1,1,25,1,'FINALIZADO'),(21,8,1,2,18,0,'FINALIZADO'),(21,5,4,3,15,0,'FINALIZADO'),
(21,3,2,4,12,0,'FINALIZADO'),(21,4,2,5,10,0,'FINALIZADO'),(21,1,3,6,8,0,'FINALIZADO'),
(21,6,4,7,6,0,'FINALIZADO'),(21,14,7,8,4,0,'FINALIZADO'),(21,2,3,9,2,0,'FINALIZADO'),
(21,9,5,10,1,0,'FINALIZADO'),(21,11,6,11,0,0,'FINALIZADO'),(21,15,8,12,0,0,'FINALIZADO'),
(21,13,7,13,0,0,'FINALIZADO'),(21,16,8,14,0,0,'FINALIZADO'),(21,10,5,15,0,0,'FINALIZADO'),
(21,18,9,16,0,0,'FINALIZADO'),(21,17,9,17,0,0,'FINALIZADO'),(21,19,10,18,0,0,'FINALIZADO'),
(21,20,10,19,0,0,'FINALIZADO'),(21,12,6,NULL,0,0,'DNF');
-- ─── GP 22: LAS VEGAS ────────────────────────────────────────
-- Ganador: Piastri | P2: Russell | P3: Norris
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(22,8,1,1,25,0,'FINALIZADO'),(22,5,4,2,18,0,'FINALIZADO'),(22,7,1,3,15,1,'FINALIZADO'),
(22,4,2,4,12,0,'FINALIZADO'),(22,3,2,5,10,0,'FINALIZADO'),(22,1,3,6,8,0,'FINALIZADO'),
(22,6,4,7,6,0,'FINALIZADO'),(22,14,7,8,4,0,'FINALIZADO'),(22,2,3,9,2,0,'FINALIZADO'),
(22,9,5,10,1,0,'FINALIZADO'),(22,11,6,11,0,0,'FINALIZADO'),(22,15,8,12,0,0,'FINALIZADO'),
(22,13,7,13,0,0,'FINALIZADO'),(22,16,8,14,0,0,'FINALIZADO'),(22,10,5,15,0,0,'FINALIZADO'),
(22,18,9,16,0,0,'FINALIZADO'),(22,17,9,17,0,0,'FINALIZADO'),(22,19,10,18,0,0,'FINALIZADO'),
(22,20,10,19,0,0,'FINALIZADO'),(22,12,6,20,0,0,'FINALIZADO');
-- ─── GP 23: CATAR ────────────────────────────────────────────
-- Ganador: Norris | P2: Piastri | P3: Verstappen
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(23,7,1,1,25,0,'FINALIZADO'),(23,8,1,2,18,1,'FINALIZADO'),(23,1,3,3,15,0,'FINALIZADO'),
(23,5,4,4,12,0,'FINALIZADO'),(23,4,2,5,10,0,'FINALIZADO'),(23,3,2,6,8,0,'FINALIZADO'),
(23,6,4,7,6,0,'FINALIZADO'),(23,14,7,8,4,0,'FINALIZADO'),(23,2,3,9,2,0,'FINALIZADO'),
(23,9,5,10,1,0,'FINALIZADO'),(23,11,6,11,0,0,'FINALIZADO'),(23,15,8,12,0,0,'FINALIZADO'),
(23,13,7,13,0,0,'FINALIZADO'),(23,16,8,14,0,0,'FINALIZADO'),(23,10,5,15,0,0,'FINALIZADO'),
(23,18,9,16,0,0,'FINALIZADO'),(23,17,9,17,0,0,'FINALIZADO'),(23,19,10,18,0,0,'FINALIZADO'),
(23,20,10,19,0,0,'FINALIZADO'),(23,12,6,NULL,0,0,'DNF');
-- ─── GP 24: ABU DABI ─────────────────────────────────────────
-- Ganador: Piastri | P2: Norris | P3: Leclerc
INSERT INTO resultados (gran_premio_id, piloto_id, escuderia_id, posicion, puntos, vuelta_rapida, estado) VALUES
(24,8,1,1,25,1,'FINALIZADO'),(24,7,1,2,18,0,'FINALIZADO'),(24,4,2,3,15,0,'FINALIZADO'),
(24,5,4,4,12,0,'FINALIZADO'),(24,3,2,5,10,0,'FINALIZADO'),(24,1,3,6,8,0,'FINALIZADO'),
(24,6,4,7,6,0,'FINALIZADO'),(24,14,7,8,4,0,'FINALIZADO'),(24,2,3,9,2,0,'FINALIZADO'),
(24,9,5,10,1,0,'FINALIZADO'),(24,11,6,11,0,0,'FINALIZADO'),(24,15,8,12,0,0,'FINALIZADO'),
(24,13,7,13,0,0,'FINALIZADO'),(24,16,8,14,0,0,'FINALIZADO'),(24,10,5,15,0,0,'FINALIZADO'),
(24,18,9,16,0,0,'FINALIZADO'),(24,17,9,17,0,0,'FINALIZADO'),(24,19,10,18,0,0,'FINALIZADO'),
(24,20,10,19,0,0,'FINALIZADO'),(24,12,6,20,0,0,'FINALIZADO');
-- ============================================================
-- USUARIO de BD (ejecutar como root)
-- ============================================================
-- CREATE USER IF NOT EXISTS 'formula1user'@'%' IDENTIFIED BY 'Eavne,e1m';
-- GRANT ALL PRIVILEGES ON formula1.* TO 'formula1user'@'%';
-- FLUSH PRIVILEGES;

View File

@ -0,0 +1,13 @@
package es.tatvil.formula1;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Formula1ApplicationTests {
@Test
void contextLoads() {
}
}

74
frontend/css/estilos.css Normal file
View File

@ -0,0 +1,74 @@
:root {
--f1-red: #a00000;
--dark-bg: #15151e;
--card-bg: #1f1f27;
--text: #ffffff;
--border-color: #38383f;
--titulo-color: #ff4c4c;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--dark-bg);
color: var(--text);
margin: 0;
padding: 20px;
}
header {
padding-bottom: 10px;
margin-bottom: 30px;
text-align: center;
color: var(--titulo-color);
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.stat-card {
background-color: var(--card-bg);
padding: 20px;
border-radius: 10px;
border-left: 4px solid var(--f1-red);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
text-align: left;
padding: 8px;
border-bottom: 1px solid var(--border-color);
}
th {
color: var(--titulo-color);
font-size: 0.8rem;
text-transform: uppercase;
}
.pos {
font-weight: bold;
color: var(--f1-red);
}
.cuenta-atras {
margin-bottom: 15px;
text-align: center;
border-bottom: 4px solid var(--f1-red);
}
#countdown {
font-size: 2rem;
font-weight: bold;
letter-spacing: 2px;
}

240
frontend/css/f1.css Normal file
View File

@ -0,0 +1,240 @@
:root {
--f1-red: #e8002d;
--dark-bg: #15151e;
--card-bg: #1f1f2a;
--text: #e5e5e5;
--muted: #888;
--border: #2e2e3e;
--accent: #ff4c4c;
--gold: #ffd700;
--green: #39d353;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--dark-bg);
color: var(--text);
padding: 0 0 40px 0;
}
/* ── Header ── */
header {
background: linear-gradient(135deg, #0a0a10 0%, #1a0a0a 100%);
border-bottom: 3px solid var(--f1-red);
padding: 20px 24px 0;
}
header h1 {
font-size: 1.6rem;
color: #fff;
letter-spacing: 2px;
margin-bottom: 16px;
}
/* ── Tabs ── */
.tabs {
display: flex;
gap: 4px;
overflow-x: auto;
}
.tab-btn {
background: transparent;
border: none;
color: var(--muted);
padding: 10px 18px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
border-bottom: 3px solid transparent;
transition: 0.2s;
white-space: nowrap;
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
/* ── Tab content ── */
.tab-content { display: none; padding: 24px; }
.tab-content.active { display: block; }
/* ── Dashboard grid ── */
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
/* ── Cards ── */
.stat-card {
background: var(--card-bg);
border-radius: 12px;
border-left: 4px solid var(--f1-red);
padding: 20px;
margin-bottom: 20px;
}
.stat-card h3 {
color: var(--accent);
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 16px;
}
/* ── Cuenta atrás ── */
.cuenta-atras {
text-align: center;
border-left: none;
border-bottom: 4px solid var(--f1-red);
margin-bottom: 20px;
}
#countdown {
font-size: 2.2rem;
font-weight: 800;
letter-spacing: 3px;
color: #fff;
margin-top: 8px;
}
/* ── Tablas ── */
table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
th {
color: var(--accent);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 8px 6px;
border-bottom: 2px solid var(--border);
text-align: left;
}
td {
padding: 9px 6px;
border-bottom: 1px solid var(--border);
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255,255,255,0.03); }
.pos { font-weight: 800; color: var(--accent); }
tr.campeon td { background: rgba(255, 215, 0, 0.06); }
tr.campeon .pos { color: var(--gold); }
tr.ganador td { background: rgba(232, 0, 45, 0.07); }
/* ── Últimas carreras ── */
.carreras-recientes { display: flex; flex-direction: column; gap: 10px; }
.carrera-mini {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(255,255,255,0.04);
border-radius: 8px;
border-left: 3px solid var(--f1-red);
}
/* ── Calendario ── */
#lista-gp { display: flex; flex-direction: column; gap: 10px; }
.gp-card {
display: grid;
grid-template-columns: 48px 1fr auto;
gap: 16px;
align-items: center;
padding: 14px 18px;
border-radius: 10px;
background: rgba(255,255,255,0.03);
border-left: 4px solid var(--border);
transition: 0.2s;
}
.gp-card.pasado { border-left-color: var(--f1-red); }
.gp-card.proximo { border-left-color: var(--green); }
.gp-card:hover { background: rgba(255,255,255,0.06); }
.gp-num {
font-size: 1.5rem;
font-weight: 800;
color: var(--border);
text-align: center;
}
.gp-card.pasado .gp-num { color: var(--f1-red); }
.gp-card.proximo .gp-num { color: var(--green); }
.gp-info { display: flex; flex-direction: column; gap: 2px; }
.gp-info strong { font-size: 0.95rem; }
.gp-info span { font-size: 0.78rem; color: var(--muted); }
.gp-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; font-size: 0.78rem; color: var(--muted); }
.badge-proximo {
background: rgba(57, 211, 83, 0.15);
color: var(--green);
border: 1px solid var(--green);
padding: 3px 8px;
border-radius: 20px;
font-size: 0.72rem;
font-weight: 700;
}
/* ── Botones ── */
.btn-resultados {
background: var(--f1-red);
color: #fff;
border: none;
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 700;
transition: 0.2s;
}
.btn-resultados:hover { background: #c0001f; }
/* ── Modal ── */
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.hidden { display: none; }
.modal-content {
background: var(--card-bg);
border-radius: 14px;
border-top: 4px solid var(--f1-red);
padding: 24px;
width: min(700px, 95vw);
max-height: 85vh;
overflow-y: auto;
position: relative;
}
.modal-content h3 {
color: var(--accent);
margin-bottom: 16px;
font-size: 1.1rem;
}
.modal-close {
position: absolute;
top: 16px;
right: 16px;
background: transparent;
border: none;
color: var(--muted);
font-size: 1.2rem;
cursor: pointer;
}
.modal-close:hover { color: #fff; }
@media (max-width: 600px) {
.gp-card { grid-template-columns: 32px 1fr; }
.gp-meta { display: none; }
#countdown { font-size: 1.5rem; }
}

61
frontend/f1.html Normal file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>F1 Stats Pro - Panel de Control</title>
<link rel="stylesheet" href="css/f1.css">
</head>
<body>
<header>
<h1>Estadísticas F1</h1>
</header>
<section class="stat-card cuenta-atras">
<h3 > Cuenta atrás para el GP de Australia 2026</h3>
<div id="countdown">
00d 00h 00m 00s
</div>
</section>
<main class="dashboard">
<section class="stat-card">
<h3 id="session-info">Pilotos</h3>
<table id="pilotos-table">
<thead>
<tr>
<th>#</th>
<th>Nombre</th>
<th>Apellido</th>
<th>Equipo</th>
<th>Nacionalidad</th>
<th>Código</th>
</tr>
</thead>
<tbody>
<!-- Aquí se llenarán los pilotos -->
</tbody>
</table>
</section>
<section class="stat-card">
<h3 id="session-info">Equipos</h3>
<table id="equipos-table">
<thead>
<tr>
<th>#</th>
<th>Nombre</th>
<th>País</th>
<th>Fundación</th>
</tr>
</thead>
<tbody>
<!-- Aquí se llenarán los equipos -->
</tbody>
</table>
</section>
</main>
<script src="js/f1.js"></script>
</body>
</html>

118
frontend/index.html Normal file
View File

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>F1 Stats — tatvil</title>
<link rel="stylesheet" href="css/f1.css">
</head>
<body>
<header>
<h1>🏎️ Estadísticas F1</h1>
<nav class="tabs">
<button class="tab-btn active" data-tab="dashboard">Dashboard</button>
<button class="tab-btn" data-tab="clasificacion">Clasificación</button>
<button class="tab-btn" data-tab="calendario">Calendario</button>
<button class="tab-btn" data-tab="pilotos">Pilotos</button>
<button class="tab-btn" data-tab="escuderias">Escuderías</button>
</nav>
</header>
<!-- DASHBOARD -->
<main id="tab-dashboard" class="tab-content active">
<section class="stat-card cuenta-atras">
<h3 id="nombre-proxima-carrera">Próxima carrera</h3>
<div id="countdown">--d --h --m --s</div>
</section>
<div class="dashboard">
<section class="stat-card">
<h3>🏆 Top 5 Pilotos — 2025</h3>
<table id="tabla-top-pilotos">
<thead><tr><th>Pos</th><th>Piloto</th><th>Equipo</th><th>Pts</th><th>Victorias</th></tr></thead>
<tbody></tbody>
</table>
</section>
<section class="stat-card">
<h3>🏗️ Top 5 Constructores — 2025</h3>
<table id="tabla-top-constructores">
<thead><tr><th>Pos</th><th>Equipo</th><th>Pts</th><th>Victorias</th></tr></thead>
<tbody></tbody>
</table>
</section>
</div>
<section class="stat-card">
<h3>🏁 Últimas carreras</h3>
<div id="ultimas-carreras" class="carreras-recientes"></div>
</section>
</main>
<!-- CLASIFICACIÓN -->
<main id="tab-clasificacion" class="tab-content">
<div class="dashboard">
<section class="stat-card">
<h3>🏆 Clasificación Pilotos 2025</h3>
<table id="tabla-clasificacion-pilotos">
<thead><tr><th>Pos</th><th>Piloto</th><th>Equipo</th><th>Pts</th><th>V</th><th>Podios</th><th>VR</th></tr></thead>
<tbody></tbody>
</table>
</section>
<section class="stat-card">
<h3>🏗️ Clasificación Constructores 2025</h3>
<table id="tabla-clasificacion-constructores">
<thead><tr><th>Pos</th><th>Constructor</th><th>Puntos</th><th>Victorias</th><th>Podios</th></tr></thead>
<tbody></tbody>
</table>
</section>
</div>
</main>
<!-- CALENDARIO -->
<main id="tab-calendario" class="tab-content">
<section class="stat-card">
<h3>📅 Calendario 2025</h3>
<div id="lista-gp"></div>
</section>
</main>
<!-- PILOTOS -->
<main id="tab-pilotos" class="tab-content">
<section class="stat-card">
<h3>👨‍✈️ Pilotos 2025</h3>
<table id="pilotos-table">
<thead>
<tr><th>#</th><th>Cód</th><th>Nombre</th><th>Apellido</th><th>Escudería</th><th>Nac.</th></tr>
</thead>
<tbody></tbody>
</table>
</section>
</main>
<!-- ESCUDERÍAS -->
<main id="tab-escuderias" class="tab-content">
<section class="stat-card">
<h3>🏎️ Escuderías 2025</h3>
<table id="escuderias-table">
<thead>
<tr><th>#</th><th>Nombre</th><th>País</th><th>Motor</th></tr>
</thead>
<tbody></tbody>
</table>
</section>
</main>
<!-- Modal resultados carrera -->
<div id="modal-carrera" class="modal hidden">
<div class="modal-content">
<button class="modal-close" onclick="cerrarModal()"></button>
<h3 id="modal-titulo"></h3>
<table id="modal-resultados">
<thead><tr><th>Pos</th><th>Piloto</th><th>Equipo</th><th>Pts</th><th>Estado</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<script src="js/f1.js"></script>
</body>
</html>

126
frontend/js/codigo.js Normal file
View File

@ -0,0 +1,126 @@
// ===============================
// FUNCIONES DE CONSULTA API
// ===============================
// Obtener calendario completo de la temporada actual
async function obtenerCalendario() {
try {
const response = await fetch("https://api.jolpi.ca/ergast/f1/current.json");
const data = await response.json();
const carreras = data.MRData.RaceTable.Races;
return carreras;
} catch (error) {
console.error("Error al obtener calendario:", error);
return [];
}
}
// Encontrar la siguiente carrera
function encontrarSiguienteCarrera(carreras) {
const ahora = new Date();
for (let carrera of carreras) {
const fecha = new Date(`${carrera.date}T${carrera.time || '00:00:00Z'}`);
if (fecha > ahora) {
return { ...carrera, fecha };
}
}
return null;
}
// ===============================
// CUENTA ATRÁS DINÁMICA
// ===============================
function iniciarCuentaAtras(fechaCarrera) {
const countdown = document.getElementById("countdown");
function actualizar() {
const ahora = new Date();
const diff = fechaCarrera - ahora;
if (diff <= 0) {
countdown.textContent = "🏁 ¡La carrera empezó!";
return;
}
const dias = Math.floor(diff / (1000 * 60 * 60 * 24));
const horas = Math.floor((diff / (1000 * 60 * 60)) % 24);
const minutos = Math.floor((diff / (1000 * 60)) % 60);
const segundos = Math.floor((diff / 1000) % 60);
countdown.textContent = `${dias}d ${horas}h ${minutos}m ${segundos}s`;
}
actualizar();
setInterval(actualizar, 1000);
}
// ===============================
// PILOTOS
// ===============================
async function cargarPilotos() {
try {
const response = await fetch('/f1/api/pilotos');
const pilotos = await response.json();
const tbody = document.querySelector('#pilotos-table tbody');
tbody.innerHTML = ''; // Limpiamos antes de rellenar
pilotos.forEach(p => {
const fila = document.createElement('tr');
fila.innerHTML = `
<td>${p.numero}</td>
<td>${p.codigo}</td>
<td>${p.nombre}</td>
<td>${p.apellido}</td>
<td>${p.escuderia || '-'}</td>
<td>${p.nacionalidad}</td>
`;
tbody.appendChild(fila);
});
} catch (error) {
console.error('Error cargando pilotos:', error);
const tbody = document.querySelector('#pilotos-table tbody');
tbody.innerHTML = `<tr><td colspan="6">No se pudieron cargar los pilotos</td></tr>`;
}
}
// ===============================
// INIT PRINCIPAL
// ===============================
async function init() {
// 1) Obtener calendario
const carreras = await obtenerCalendario();
// 2) Calcular siguiente evento
const proxima = encontrarSiguienteCarrera(carreras);
if (!proxima) {
document.getElementById("countdown").textContent =
"No hay próximos Grandes Premios este año";
return;
}
// 3) Mostrar nombre de la próxima carrera
document.querySelector(".cuenta-atras h3").textContent =
`Cuenta atrás para el GP de ${proxima.raceName}`;
// 4) Iniciar cuenta atrás
iniciarCuentaAtras(proxima.fecha);
// 5) Indicar modo carrera o pronóstico
const ahora = new Date();
if (ahora >= proxima.fecha) {
document.getElementById("session-info").textContent =
"Modo carrera — datos en vivo o resultados";
} else {
document.getElementById("session-info").textContent =
`Próxima sesión de ${proxima.raceName}`;
}
// Cargar pilotos al inicio y cada minuto
cargarPilotos();
setInterval(() => { cargarPilotos(); }, 60000);
}
// Arrancar todo al cargar la página
document.addEventListener("DOMContentLoaded", init);

227
frontend/js/f1.js Normal file
View File

@ -0,0 +1,227 @@
// Base API
const API = '/f1/api';
// ─── Tabs ─────────────────────────────────────────────────────
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
// ─── Cuenta atrás próxima carrera (Ergast/Jolpi API) ──────────
async function initCuentaAtras() {
try {
const res = await fetch('https://api.jolpi.ca/ergast/f1/2026.json');
const data = await res.json();
const carreras = data.MRData.RaceTable.Races;
const ahora = new Date();
const proxima = carreras.find(c => new Date(`${c.date}T${c.time || '12:00:00Z'}`) > ahora);
if (!proxima) {
document.getElementById('nombre-proxima-carrera').textContent = 'Temporada terminada';
document.getElementById('countdown').textContent = '—';
return;
}
const fechaProxima = new Date(`${proxima.date}T${proxima.time || '12:00:00Z'}`);
document.getElementById('nombre-proxima-carrera').textContent =
`⏱️ Cuenta atrás — ${proxima.raceName}`;
function tick() {
const diff = fechaProxima - new Date();
if (diff <= 0) { document.getElementById('countdown').textContent = '🏁 ¡Carrera en curso!'; return; }
const d = Math.floor(diff / 86400000);
const h = Math.floor((diff % 86400000) / 3600000);
const m = Math.floor((diff % 3600000) / 60000);
const s = Math.floor((diff % 60000) / 1000);
document.getElementById('countdown').textContent = `${d}d ${h}h ${m}m ${s}s`;
}
tick();
setInterval(tick, 1000);
} catch (e) {
document.getElementById('countdown').textContent = '—';
}
}
// ─── Clasificación ────────────────────────────────────────────
async function cargarClasificacionPilotos() {
const res = await fetch(`${API}/resultados/clasificacion/pilotos?temporada=2025`);
return res.json();
}
async function cargarClasificacionConstructores() {
const res = await fetch(`${API}/resultados/clasificacion/constructores?temporada=2025`);
return res.json();
}
function renderClasificacionPilotos(data, tablaId, maxRows = 999) {
const tbody = document.querySelector(`#${tablaId} tbody`);
tbody.innerHTML = '';
const esCompleta = tablaId.includes('clasificacion');
data.slice(0, maxRows).forEach((p, i) => {
const tr = document.createElement('tr');
if (i === 0) tr.classList.add('campeon');
tr.innerHTML = `
<td class="pos">${i + 1}</td>
<td>${p.nombre} <strong>${p.apellido}</strong></td>
<td>${p.equipo}</td>
<td><strong>${p.puntos}</strong></td>
<td>${p.victorias}</td>
${esCompleta ? `<td>${p.podios}</td><td>${p.vueltasRapidas}</td>` : ''}
`;
tbody.appendChild(tr);
});
}
function renderClasificacionConstructores(data, tablaId, maxRows = 999) {
const tbody = document.querySelector(`#${tablaId} tbody`);
tbody.innerHTML = '';
const esCompleta = tablaId.includes('clasificacion');
data.slice(0, maxRows).forEach((c, i) => {
const tr = document.createElement('tr');
if (i === 0) tr.classList.add('campeon');
tr.innerHTML = `
<td class="pos">${i + 1}</td>
<td><strong>${c.nombre}</strong></td>
<td><strong>${c.puntos}</strong></td>
<td>${c.victorias}</td>
${esCompleta ? `<td>${c.podios}</td>` : ''}
`;
tbody.appendChild(tr);
});
}
// ─── Calendario ───────────────────────────────────────────────
async function cargarCalendario() {
const res = await fetch(`${API}/gp?temporada=2025`);
return res.json();
}
function renderCalendario(gps) {
const cont = document.getElementById('lista-gp');
const hoy = new Date();
cont.innerHTML = '';
gps.forEach((gp, i) => {
const fecha = new Date(gp.fecha);
const pasado = fecha < hoy;
const card = document.createElement('div');
card.className = 'gp-card ' + (pasado ? 'pasado' : 'proximo');
card.innerHTML = `
<div class="gp-num">${String(i + 1).padStart(2, '0')}</div>
<div class="gp-info">
<strong>${gp.nombre}</strong>
<span>${gp.circuito}</span>
<span>${gp.ciudad}, ${gp.pais}</span>
<span>${fecha.toLocaleDateString('es-ES', {day:'numeric',month:'long',year:'numeric'})}</span>
</div>
<div class="gp-meta">
<span>${gp.numVueltas} vueltas · ${gp.distanciaKm} km</span>
${pasado
? `<button class="btn-resultados" onclick="abrirResultados(${gp.id}, '${gp.nombre.replace(/'/g, "\\'")}')">Ver resultados →</button>`
: '<span class="badge-proximo">Próxima</span>'}
</div>
`;
cont.appendChild(card);
});
}
function renderUltimasCarreras(gps) {
const hoy = new Date();
const pasados = gps.filter(g => new Date(g.fecha) < hoy).slice(-3).reverse();
const cont = document.getElementById('ultimas-carreras');
cont.innerHTML = '';
pasados.forEach(gp => {
const div = document.createElement('div');
div.className = 'carrera-mini';
div.innerHTML = `<strong>${gp.nombre}</strong>
<button class="btn-resultados" onclick="abrirResultados(${gp.id}, '${gp.nombre.replace(/'/g, "\\'")}')">Ver resultados </button>`;
cont.appendChild(div);
});
}
// ─── Modal resultados ─────────────────────────────────────────
async function abrirResultados(gpId, nombre) {
document.getElementById('modal-titulo').textContent = nombre;
document.querySelector('#modal-resultados tbody').innerHTML = '<tr><td colspan="5">Cargando...</td></tr>';
document.getElementById('modal-carrera').classList.remove('hidden');
const res = await fetch(`${API}/resultados/gp/${gpId}`);
const data = await res.json();
const tbody = document.querySelector('#modal-resultados tbody');
tbody.innerHTML = '';
data.forEach(r => {
const tr = document.createElement('tr');
if (r.posicion === 1) tr.classList.add('ganador');
tr.innerHTML = `
<td class="pos">${r.posicion ?? 'DNF'}</td>
<td>${r.piloto.nombre} <strong>${r.piloto.apellido}</strong>${r.vueltaRapida ? ' ' : ''}</td>
<td>${r.escuderia.nombre}</td>
<td>${r.puntos}</td>
<td>${r.estado}</td>
`;
tbody.appendChild(tr);
});
}
function cerrarModal() {
document.getElementById('modal-carrera').classList.add('hidden');
}
document.getElementById('modal-carrera').addEventListener('click', e => {
if (e.target === e.currentTarget) cerrarModal();
});
// ─── Pilotos ───────────────────────────────────────────────────
async function cargarPilotos() {
const res = await fetch(`${API}/pilotos`);
const pilotos = await res.json();
const tbody = document.querySelector('#pilotos-table tbody');
tbody.innerHTML = '';
pilotos.forEach(p => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="pos">${p.numero}</td>
<td><strong>${p.codigo}</strong></td>
<td>${p.nombre}</td>
<td>${p.apellido}</td>
<td></td>
<td>${p.nacionalidad}</td>
`;
tbody.appendChild(tr);
});
}
// ─── Escuderías ───────────────────────────────────────────────
async function cargarEscuderias() {
const res = await fetch(`${API}/escuderias`);
const data = await res.json();
const tbody = document.querySelector('#escuderias-table tbody');
tbody.innerHTML = '';
data.forEach((e, i) => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${i + 1}</td><td><strong>${e.nombre}</strong></td><td>${e.pais}</td><td>${e.motor}</td>`;
tbody.appendChild(tr);
});
}
// ─── INIT ──────────────────────────────────────────────────────
async function init() {
initCuentaAtras();
const [clasificPilotos, clasificConst, gps] = await Promise.all([
cargarClasificacionPilotos(),
cargarClasificacionConstructores(),
cargarCalendario()
]);
renderClasificacionPilotos(clasificPilotos, 'tabla-top-pilotos', 5);
renderClasificacionConstructores(clasificConst, 'tabla-top-constructores', 5);
renderUltimasCarreras(gps);
renderClasificacionPilotos(clasificPilotos, 'tabla-clasificacion-pilotos');
renderClasificacionConstructores(clasificConst, 'tabla-clasificacion-constructores');
renderCalendario(gps);
cargarPilotos();
cargarEscuderias();
}
document.addEventListener('DOMContentLoaded', init);

155
frontend/js/f1_2.js Normal file
View File

@ -0,0 +1,155 @@
// ===============================
// FUNCIONES DE CONSULTA API
// ===============================
// Obtener calendario completo de la temporada actual
async function obtenerCalendario() {
try {
const response = await fetch("https://api.jolpi.ca/ergast/f1/current.json");
const data = await response.json();
const carreras = data.MRData.RaceTable.Races;
return carreras;
} catch (error) {
console.error("Error al obtener calendario:", error);
return [];
}
}
// Encontrar la siguiente carrera
function encontrarSiguienteCarrera(carreras) {
const ahora = new Date();
for (let carrera of carreras) {
const fecha = new Date(`${carrera.date}T${carrera.time || '00:00:00Z'}`);
if (fecha > ahora) {
return { ...carrera, fecha };
}
}
return null;
}
// ===============================
// CUENTA ATRÁS DINÁMICA
// ===============================
function iniciarCuentaAtras(fechaCarrera) {
const countdown = document.getElementById("countdown");
function actualizar() {
const ahora = new Date();
const diff = fechaCarrera - ahora;
if (diff <= 0) {
countdown.textContent = "🏁 ¡La carrera empezó!";
return;
}
const dias = Math.floor(diff / (1000 * 60 * 60 * 24));
const horas = Math.floor((diff / (1000 * 60 * 60)) % 24);
const minutos = Math.floor((diff / (1000 * 60)) % 60);
const segundos = Math.floor((diff / 1000) % 60);
countdown.textContent = `${dias}d ${horas}h ${minutos}m ${segundos}s`;
}
actualizar();
setInterval(actualizar, 1000);
}
// ===============================
// PILOTOS
// ===============================
async function cargarPilotos() {
try {
const response = await fetch('/f1/api/pilotos');
const pilotos = await response.json();
const tbody = document.querySelector('#pilotos-table tbody');
tbody.innerHTML = ''; // Limpiamos antes de rellenar
pilotos.forEach(p => {
const fila = document.createElement('tr');
fila.innerHTML = `
<td>${p.numero}</td>
<td>${p.nombre}</td>
<td>${p.apellido}</td>
<td>${p.equipo || '-'}</td>
<td>${p.nacionalidad}</td>
<td>${p.codigo}</td>
`;
tbody.appendChild(fila);
});
} catch (error) {
console.error('Error cargando pilotos:', error);
const tbody = document.querySelector('#pilotos-table tbody');
tbody.innerHTML = `<tr><td colspan="6">No se pudieron cargar los pilotos</td></tr>`;
}
}
// ===============================
// ESCUDERÍAS
// ===============================
async function cargarEscuderias() {
try {
const response = await fetch('/f1/api/escuderias');
const escuderias = await response.json();
const tbody = document.querySelector('#escuderias-table tbody');
tbody.innerHTML = ''; // Limpiamos tabla
escuderias.forEach(e => {
const fila = document.createElement('tr');
fila.innerHTML = `
<td>${e.nombre}</td>
<td>${e.pais}</td>
<td>${e.motor}</td>
`;
tbody.appendChild(fila);
});
} catch (error) {
console.error('Error cargando escuderías:', error);
const tbody = document.querySelector('#escuderias-table tbody');
tbody.innerHTML = `<tr><td colspan="4">No se pudieron cargar las escuderías</td></tr>`;
}
}
// ===============================
// INIT PRINCIPAL
// ===============================
async function init() {
// 1) Obtener calendario
const carreras = await obtenerCalendario();
// 2) Calcular siguiente evento
const proxima = encontrarSiguienteCarrera(carreras);
if (!proxima) {
document.getElementById("countdown").textContent =
"No hay próximos Grandes Premios este año";
return;
}
// 3) Mostrar nombre de la próxima carrera
document.querySelector(".cuenta-atras h3").textContent =
`Cuenta atrás para el GP de ${proxima.raceName}`;
// 4) Iniciar cuenta atrás
iniciarCuentaAtras(proxima.fecha);
// 5) Indicar modo carrera o pronóstico
const ahora = new Date();
if (ahora >= proxima.fecha) {
document.getElementById("session-info").textContent =
"Modo carrera — datos en vivo o resultados";
} else {
document.getElementById("session-info").textContent =
`Próxima sesión de ${proxima.raceName}`;
}
// Cargar pilotos y escuderías al inicio y cada minuto
cargarPilotos();
cargarEscuderias();
setInterval(() => { cargarPilotos(); cargarEscuderias(); }, 60000);
}
// Arrancar todo al cargar la página
document.addEventListener("DOMContentLoaded", init);

49
frontend/js/f1pruebas.js Normal file
View File

@ -0,0 +1,49 @@
const BASE_URL = 'https://api.openf1.org/v1';
async function cargarDatosF1() {
const statusDiv = document.getElementById('api-status');
const tablaCuerpo = document.getElementById('tabla-pilotos-body');
try {
statusDiv.innerHTML = 'Conectando con OpenF1...';
// Usamos una session_key fija de un GP pasado para probar que funciona
// Bahrain 2024 Session Key: 9465
const sessionKey = '9465';
document.getElementById('session-info').innerText = "GP de Bahrain (Datos de Test)";
const driversRes = await fetch(`${BASE_URL}/drivers?session_key=${sessionKey}`);
if (!driversRes.ok) throw new Error(`Error HTTP: ${driversRes.status}`);
const drivers = await driversRes.json();
if (drivers.length === 0) {
statusDiv.innerHTML = 'No se encontraron pilotos.';
return;
}
tablaCuerpo.innerHTML = '';
drivers.forEach(driver => {
const row = document.createElement('tr');
row.innerHTML = `
<td style="border-left: 4px solid #${driver.team_colour || 'ccc'}">
${driver.driver_number}
</td>
<td><strong>${driver.full_name}</strong></td>
<td>${driver.team_name}</td>
<td>${driver.name_acronym}</td>
`;
tablaCuerpo.appendChild(row);
});
statusDiv.innerHTML = '<span style="color: #4caf50;">● Datos cargados con éxito</span>';
} catch (error) {
console.error("DETALLE DEL ERROR:", error);
statusDiv.innerHTML = `<span style="color: #ff4b4b;">● Error: ${error.message}</span>`;
}
}
document.addEventListener('DOMContentLoaded', cargarDatosF1);

47
frontend/pilotos.html Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pilotos de F1</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f7f7f7;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
}
table {
margin: 0 auto;
border-collapse: collapse;
width: 90%;
max-width: 800px;
background-color: white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
th, td {
padding: 10px 15px;
border: 1px solid #ddd;
text-align: center;
}
th {
background-color: #004080;
color: white;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
tr:hover {
background-color: #cce0ff;
}
</style>
</head>
<body>
<h1>Pilotos de F1</h1>
</body>
</html>

47
frontend/pilotos2.html Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pilotos de F1</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f7f7f7;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
}
table {
margin: 0 auto;
border-collapse: collapse;
width: 90%;
max-width: 800px;
background-color: white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
th, td {
padding: 10px 15px;
border: 1px solid #ddd;
text-align: center;
}
th {
background-color: #004080;
color: white;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
tr:hover {
background-color: #cce0ff;
}
</style>
</head>
<body>
<h1>Pilotos de F1</h1>
</body>
</html>