first commit

This commit is contained in:
Tatiana Villa Ema 2026-04-27 00:07:09 +02:00
commit 09583e1112
75 changed files with 41102 additions and 0 deletions

57
backend/Java/.classpath Normal file
View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

64
backend/Java/.project Normal file
View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>backend</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.jboss.tools.jst.web.kb.kbbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.jboss.tools.cdi.core.cdibuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
<nature>org.jboss.tools.jst.web.kb.kbnature</nature>
<nature>org.jboss.tools.cdi.core.cdinature</nature>
</natures>
<filteredResources>
<filter>
<id>1771870854708</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=false

View File

@ -0,0 +1,22 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
org.eclipse.jdt.core.compiler.annotation.nonnull=org.springframework.lang.NonNull
org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.springframework.lang.NonNullApi
org.eclipse.jdt.core.compiler.annotation.nullable=org.springframework.lang.Nullable
org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning
org.eclipse.jdt.core.compiler.problem.nullReference=warning
org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning
org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=17

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
<wb-module deploy-name="backend">
<wb-resource deploy-path="/" source-path="/src/main/java"/>
<wb-resource deploy-path="/" source-path="/src/main/resources"/>
<wb-resource deploy-path="/" source-path="/target/generated-sources/annotations"/>
</wb-module>
</project-modules>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<faceted-project>
<installed facet="java" version="17"/>
<installed facet="jst.utility" version="1.0"/>
</faceted-project>

View File

@ -0,0 +1,2 @@
disabled=06target
eclipse.preferences.version=1

View File

@ -0,0 +1,2 @@
boot.validation.initialized=true
eclipse.preferences.version=1

12
backend/Java/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"]

295
backend/Java/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/Java/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"

68
backend/Java/pom.xml Normal file
View File

@ -0,0 +1,68 @@
<?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>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<description>Backend para servir la base de datos del VPS a todos mis proyectos</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.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
</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.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

2
backend/Java/spring/.gitattributes vendored Normal file
View File

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

33
backend/Java/spring/.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

295
backend/Java/spring/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/Java/spring/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"

View File

@ -0,0 +1,68 @@
<?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>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<description>Backend para servir la base de datos del VPS a todos mis proyectos</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>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</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.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

@ -0,0 +1 @@
spring.application.name=backend

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
package es.tatvil.backend.controllers;
import es.tatvil.backend.entities.Weather;
import es.tatvil.backend.repositories.WeatherRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/api/weather")
@CrossOrigin(origins = "*") // Para que tu web pueda consultar los datos sin errores de CORS
public class WeatherController {
@Autowired
private WeatherRepository weatherRepository;
@GetMapping("/all")
public List<Weather> getAllWeather() {
return weatherRepository.findAll();
}
@GetMapping("/filter")
public List<Weather> getWeatherFiltered(
@RequestParam String ciudad,
@RequestParam LocalDate desde,
@RequestParam LocalDate hasta
) {
return weatherRepository.findByCiudadAndFechaBetween(ciudad, desde, hasta);
}
}

View File

@ -0,0 +1,146 @@
package es.tatvil.backend.entities;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
@Entity
@Table(name = "weather")
public class Weather {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@JsonProperty("dia")
private LocalDate fecha; // Para el tipo DATE de MariaDB
private String ciudad;
private LocalTime amanecer; // Para el tipo TIME
private LocalTime anochecer;
@Column(name = "temp_min")
@JsonProperty("temp_min")
private Integer tempMin;
@Column(name = "temp_max")
@JsonProperty("temp_max")
private Integer tempMax;
private Integer humedad;
@Column(name = "viento_velocidad")
@JsonProperty("viento_velocidad")
private Integer vientoVelocidad;
@Column(name = "viento_direccion")
private Integer vientoDireccion;
private Integer nubes;
private Integer lluvia;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public LocalDate getFecha() {
return fecha;
}
public void setFecha(LocalDate fecha) {
this.fecha = fecha;
}
public String getCiudad() {
return ciudad;
}
public void setCiudad(String ciudad) {
this.ciudad = ciudad;
}
public LocalTime getAmanecer() {
return amanecer;
}
public void setAmanecer(LocalTime amanecer) {
this.amanecer = amanecer;
}
public LocalTime getAnochecer() {
return anochecer;
}
public void setAnochecer(LocalTime anochecer) {
this.anochecer = anochecer;
}
public Integer getTempMin() {
return tempMin;
}
public void setTempMin(Integer tempMin) {
this.tempMin = tempMin;
}
public Integer getTempMax() {
return tempMax;
}
public void setTempMax(Integer tempMax) {
this.tempMax = tempMax;
}
public Integer getHumedad() {
return humedad;
}
public void setHumedad(Integer humedad) {
this.humedad = humedad;
}
public Integer getVientoVelocidad() {
return vientoVelocidad;
}
public void setVientoVelocidad(Integer vientoVelocidad) {
this.vientoVelocidad = vientoVelocidad;
}
public Integer getVientoDireccion() {
return vientoDireccion;
}
public void setVientoDireccion(Integer vientoDireccion) {
this.vientoDireccion = vientoDireccion;
}
public Integer getNubes() {
return nubes;
}
public void setNubes(Integer nubes) {
this.nubes = nubes;
}
public Integer getLluvia() {
return lluvia;
}
public void setLluvia(Integer lluvia) {
this.lluvia = lluvia;
}
@Override
public int hashCode() {
return Objects.hash(amanecer, anochecer, ciudad, fecha, humedad, id, lluvia, nubes, tempMax, tempMin,
vientoDireccion, vientoVelocidad);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Weather other = (Weather) obj;
return Objects.equals(amanecer, other.amanecer) && Objects.equals(anochecer, other.anochecer)
&& Objects.equals(ciudad, other.ciudad) && Objects.equals(fecha, other.fecha)
&& Objects.equals(humedad, other.humedad) && Objects.equals(id, other.id)
&& Objects.equals(lluvia, other.lluvia) && Objects.equals(nubes, other.nubes)
&& Objects.equals(tempMax, other.tempMax) && Objects.equals(tempMin, other.tempMin)
&& Objects.equals(vientoDireccion, other.vientoDireccion)
&& Objects.equals(vientoVelocidad, other.vientoVelocidad);
}
@Override
public String toString() {
return "Weather [id=" + id + ", fecha=" + fecha + ", ciudad=" + ciudad + ", amanecer=" + amanecer
+ ", anochecer=" + anochecer + ", tempMin=" + tempMin + ", tempMax=" + tempMax + ", humedad=" + humedad
+ ", vientoVelocidad=" + vientoVelocidad + ", vientoDireccion=" + vientoDireccion + ", nubes=" + nubes
+ ", lluvia=" + lluvia + "]";
}
}

View File

@ -0,0 +1,15 @@
package es.tatvil.backend.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import es.tatvil.backend.entities.Weather;
@Repository
public interface WeatherRepository extends JpaRepository<Weather, Integer> {
// Spring entenderá que debe filtrar por 'ciudad' y por el campo 'fecha'
List<Weather> findByCiudadAndFechaBetween(String ciudad, LocalDate desde, LocalDate hasta);
}

View File

@ -0,0 +1,5 @@
package es.tatvil.model;
public class clima {
}

View File

@ -0,0 +1,9 @@
spring.datasource.url=jdbc:mysql://db:3306/clima?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.username=admin
spring.datasource.password=Eavne,e1m
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
# spring.jpa.database-platform=org.hibernate.dialect.MariaDBDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
server.servlet.context-path=/apis

View File

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

View File

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

View File

@ -0,0 +1,9 @@
spring.datasource.url=jdbc:mysql://db:3306/clima?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.username=admin
spring.datasource.password=Eavne,e1m
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
# spring.jpa.database-platform=org.hibernate.dialect.MariaDBDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
server.servlet.context-path=/apis

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

View File

@ -0,0 +1,150 @@
<?php
// ============================
// 1. HEADERS
// ============================
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=utf-8");
// ============================
// 2. CONFIGURACIÓN BD
// (En producción esto debería ir
// en un archivo fuera del webroot)
// ============================
define('DB_HOST', 'localhost');
define('DB_USER', 'admin');
define('DB_PASS', 'Eavne,e1m');
define('DB_NAME', 'clima');
// ============================
// 3. VALIDACIÓN DE ENTRADA
// ============================
$ciudad = $_GET['ciudad'] ?? '';
$fecha = $_GET['fecha'] ?? '';
$desde = $_GET['desde'] ?? '';
$hasta = $_GET['hasta'] ?? '';
if (empty($ciudad)) {
http_response_code(400);
echo json_encode(["error" => "El parámetro 'ciudad' es obligatorio."]);
exit();
}
// Validar formato fecha YYYY-MM-DD
function fechaValida($fecha) {
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha);
}
if (!empty($fecha) && !fechaValida($fecha)) {
http_response_code(400);
echo json_encode(["error" => "El parámetro 'fecha' debe tener formato YYYY-MM-DD."]);
exit();
}
if (!empty($desde) && !fechaValida($desde)) {
http_response_code(400);
echo json_encode(["error" => "El parámetro 'desde' debe tener formato YYYY-MM-DD."]);
exit();
}
if (!empty($hasta) && !fechaValida($hasta)) {
http_response_code(400);
echo json_encode(["error" => "El parámetro 'hasta' debe tener formato YYYY-MM-DD."]);
exit();
}
// ============================
// 4. CONEXIÓN BD
// ============================
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
http_response_code(500);
echo json_encode(["error" => "Error de conexión a la base de datos."]);
exit();
}
// ============================
// 5. CONSTRUIR QUERY DINÁMICA
// ============================
$sql = "
SELECT
DATE(fecha) AS dia,
MIN(fecha) AS primera_fecha_del_dia,
MIN(amanecer) AS amanecer,
MAX(anochecer) AS anochecer,
MAX(temp_max) AS temp_max,
MIN(temp_min) AS temp_min,
AVG(humedad) AS humedad,
SUM(lluvia) AS lluvia,
AVG(nubes) AS nubes,
AVG(viento_velocidad) AS viento_velocidad,
AVG(viento_direccion) AS viento_direccion
FROM weather
WHERE ciudad LIKE CONCAT('%', ?, '%')
";
$params = [$ciudad];
$types = "s";
// Filtro por fecha exacta
if (!empty($fecha)) {
$sql .= " AND DATE(fecha) = ?";
$params[] = $fecha;
$types .= "s";
}
// Filtro por rango
if (!empty($desde) && !empty($hasta)) {
$sql .= " AND DATE(fecha) BETWEEN ? AND ?";
$params[] = $desde;
$params[] = $hasta;
$types .= "ss";
} elseif (!empty($desde)) {
$sql .= " AND DATE(fecha) >= ?";
$params[] = $desde;
$types .= "s";
} elseif (!empty($hasta)) {
$sql .= " AND DATE(fecha) <= ?";
$params[] = $hasta;
$types .= "s";
}
$sql .= "
GROUP BY DATE(fecha)
ORDER BY DATE(fecha);
";
// ============================
// 6. PREPARAR Y EJECUTAR
// ============================
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
echo json_encode(["error" => "Error al preparar la consulta."]);
$conn->close();
exit();
}
$stmt->bind_param($types, ...$params);
$stmt->execute();
$result = $stmt->get_result();
$datos = [];
while ($row = $result->fetch_assoc()) {
$datos[] = $row;
}
// ============================
// 7. RESPUESTA
// ============================
http_response_code(200);
echo json_encode($datos);
// ============================
// 8. CIERRE
// ============================
$stmt->close();
$conn->close();
?>

View File

@ -0,0 +1,153 @@
<?php
// ============================
// 1. HEADERS
// ============================
header("Access-Control-Allow-Origin: *");
header("Content-Type: text/plain; charset=utf-8");
// ============================
// 2. CONFIGURACIÓN BD
// (En producción esto debería ir
// en un archivo fuera del webroot)
// ============================
define('DB_HOST', 'localhost');
define('DB_USER', 'admin');
define('DB_PASS', 'Eavne,e1m');
define('DB_NAME', 'clima');
// ============================
// 3. VALIDACIÓN DE ENTRADA
// ============================
$ciudad = $_GET['ciudad'] ?? '';
$fecha = $_GET['fecha'] ?? '';
$desde = $_GET['desde'] ?? '';
$hasta = $_GET['hasta'] ?? '';
if (empty($ciudad)) {
http_response_code(400);
echo json_encode(["error" => "El parámetro 'ciudad' es obligatorio."]);
exit();
}
// Validar formato fecha YYYY-MM-DD
function fechaValida($fecha) {
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha);
}
if (!empty($fecha) && !fechaValida($fecha)) {
http_response_code(400);
echo ["error" => "El parámetro 'fecha' debe tener formato YYYY-MM-DD."];
exit();
}
if (!empty($desde) && !fechaValida($desde)) {
http_response_code(400);
echo ["error" => "El parámetro 'desde' debe tener formato YYYY-MM-DD."];
exit();
}
if (!empty($hasta) && !fechaValida($hasta)) {
http_response_code(400);
echo ["error" => "El parámetro 'hasta' debe tener formato YYYY-MM-DD."];
exit();
}
// ============================
// 4. CONEXIÓN BD
// ============================
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
http_response_code(500);
echo ["error" => "Error de conexión a la base de datos."];
exit();
}
// ============================
// 5. CONSTRUIR QUERY DINÁMICA
// ============================
$sql = "
SELECT
DATE(fecha) AS dia,
MIN(fecha) AS primera_fecha_del_dia,
MIN(amanecer) AS amanecer,
MAX(anochecer) AS anochecer,
MAX(temp_max) AS temp_max,
MIN(temp_min) AS temp_min,
AVG(humedad) AS humedad,
SUM(lluvia) AS lluvia,
AVG(nubes) AS nubes,
AVG(viento_velocidad) AS viento_velocidad,
AVG(viento_direccion) AS viento_direccion
FROM weather
WHERE ciudad LIKE CONCAT('%', ?, '%')
";
$params = [$ciudad];
$types = "s";
// Filtro por fecha exacta
if (!empty($fecha)) {
$sql .= " AND DATE(fecha) = ?";
$params[] = $fecha;
$types .= "s";
}
// Filtro por rango
if (!empty($desde) && !empty($hasta)) {
$sql .= " AND DATE(fecha) BETWEEN ? AND ?";
$params[] = $desde;
$params[] = $hasta;
$types .= "ss";
} elseif (!empty($desde)) {
$sql .= " AND DATE(fecha) >= ?";
$params[] = $desde;
$types .= "s";
} elseif (!empty($hasta)) {
$sql .= " AND DATE(fecha) <= ?";
$params[] = $hasta;
$types .= "s";
}
$sql .= "
GROUP BY DATE(fecha)
ORDER BY DATE(fecha);
";
echo $sql; // Para depuración: muestra la consulta generada
echo "\nParámetros: " . implode(", ", $params) . "\n"; // Para depuración: muestra los parámetros
// ============================
// 6. PREPARAR Y EJECUTAR
// ============================
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
// echo json_encode(["error" => "Error al preparar la consulta."]);
$conn->close();
exit();
}
$stmt->bind_param($types, ...$params);
$stmt->execute();
$result = $stmt->get_result();
$datos = [];
while ($row = $result->fetch_assoc()) {
// $datos[] = $row;
echo $row['dia'] . " - " . $row['temp_max'] . "°C / " . $row['temp_min'] . "°C\n";
}
// ============================
// 7. RESPUESTA
// ============================
//http_response_code(200);
//echo json_encode($datos);
// ============================
// 8. CIERRE
// ============================
$stmt->close();
$conn->close();
?>

View File

@ -0,0 +1,87 @@
<?php
// Permitir solicitudes desde cualquier origen (CORS)
header("Access-Control-Allow-Origin: *");
header('Content-Type: application/json; charset=utf-8');
// --- 1. Database Configuration (Example with external config) ---
// In a real scenario, this would come from a file outside the web root.
// For demonstration, let's just make it clear that this should be external.
define('DB_HOST', 'localhost');
define('DB_USER', 'admin');
define('DB_PASS', 'Eavne,e1m'); // !! IMPORTANT: Store this securely in a real application !!
define('DB_NAME', 'clima');
// --- 2. Input Validation ---
// Assuming 'ciudad' comes from a GET request.
$ciudad = $_GET['ciudad'] ?? ''; // Use null coalescing operator for cleaner default
if (empty($ciudad)) {
http_response_code(400); // Bad Request
echo json_encode(["error" => "Parámetro 'ciudad' es requerido."]);
exit(); // Stop script execution
}
// Optional: Further sanitize/validate the city name if needed (e.g., alphanumeric only)
// if (!preg_match('/^[a-zA-Z\s]+$/', $ciudad)) {
// http_response_code(400);
// echo json_encode(["error" => "El nombre de la ciudad contiene caracteres inválidos."]);
// exit();
// }
// --- 3. Database Connection ---
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
http_response_code(500); // Internal Server Error
echo json_encode(["error" => "Error de conexión a la base de datos: " . $conn->connect_error]);
exit();
}
// --- 4. Prepare and Execute Query ---
$stmt = $conn->prepare("
SELECT DATE(fecha) AS dia,
MIN(fecha) AS primera_fecha_del_dia,
MIN(amanecer) AS amanecer,
MAX(anochecer) AS anochecer,
MAX(temp_max) AS temp_max,
MIN(temp_min) AS temp_min,
AVG(humedad) AS humedad,
AVG(lluvia) AS lluvia,
AVG(nubes) AS nubes,
AVG(viento_velocidad) AS viento_velocidad,
AVG(viento_direccion) AS viento_direccion
FROM weather
WHERE DATE(fecha) >= '2024-10-01'
AND ciudad LIKE CONCAT('%', ?, '%')
GROUP BY DATE(fecha)
ORDER BY DATE(fecha) DESC
");
if (!$stmt) {
http_response_code(500); // Internal Server Error
echo json_encode(["error" => "Error al preparar la consulta: " . $conn->error]);
$conn->close();
exit();
}
$stmt->bind_param("s", $ciudad);
$stmt->execute();
$result = $stmt->get_result();
$datos = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$datos[] = $row;
}
http_response_code(200); // OK
echo json_encode($datos);
} else {
http_response_code(200);
echo json_encode([]); // array vacío
}
// --- 5. Close Resources ---
$stmt->close();
$conn->close();
?>

View File

@ -0,0 +1,96 @@
<?php
// Permitir solicitudes desde cualquier origen (CORS)
header("Access-Control-Allow-Origin: *");
header('Content-Type: application/json; charset=utf-8');
// --- 1. Database Configuration (Example with external config) ---
// In a real scenario, this would come from a file outside the web root.
// For demonstration, let's just make it clear that this should be external.
define('DB_HOST', 'localhost');
define('DB_USER', 'admin');
define('DB_PASS', 'Eavne,e1m'); // !! IMPORTANT: Store this securely in a real application !!
define('DB_NAME', 'clima');
// --- 2. Input Validation ---
// Assuming 'ciudad' comes from a GET request.
$ciudad = $_GET['ciudad'] ?? ''; // Use null coalescing operator for cleaner default
$fecha = $_GET['fecha'] ?? ''; // Optional: filter by date
if (empty($ciudad)) {
http_response_code(400); // Bad Request
echo json_encode(["error" => "Parámetro 'ciudad' es requerido."]);
exit(); // Stop script execution
}
if (!empty($fecha) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha)) {
http_response_code(400); // Bad Request
echo json_encode(["error" => "Parámetro 'fecha' debe tener el formato YYYY-MM-DD."]);
exit();
}
// Optional: Further sanitize/validate the city name if needed (e.g., alphanumeric only)
// if (!preg_match('/^[a-zA-Z\s]+$/', $ciudad)) {
// http_response_code(400);
// echo json_encode(["error" => "El nombre de la ciudad contiene caracteres inválidos."]);
// exit();
// }
// --- 3. Database Connection ---
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
http_response_code(500); // Internal Server Error
echo json_encode(["error" => "Error de conexión a la base de datos: " . $conn->connect_error]);
exit();
}
// --- 4. Prepare and Execute Query ---
$stmt = $conn->prepare("
SELECT DATE(fecha) AS dia,
MIN(fecha) AS primera_fecha_del_dia,
MIN(amanecer) AS amanecer,
MAX(anochecer) AS anochecer,
MAX(temp_max) AS temp_max,
MIN(temp_min) AS temp_min,
AVG(humedad) AS humedad,
SUM(lluvia) AS lluvia,
AVG(nubes) AS nubes,
AVG(viento_velocidad) AS viento_velocidad,
AVG(viento_direccion) AS viento_direccion
FROM weather
WHERE DATE(fecha) >= '2024-10-01'
AND ciudad LIKE CONCAT('%', ?, '%')
AND DATE(fecha) LIKE CONCAT('%', ?, '%')
GROUP BY DATE(fecha)
ORDER BY DATE(fecha);
");
if (!$stmt) {
http_response_code(500); // Internal Server Error
echo json_encode(["error" => "Error al preparar la consulta: " . $conn->error]);
$conn->close();
exit();
}
$stmt->bind_param("ss", $ciudad, $fecha);
$stmt->execute();
$result = $stmt->get_result();
$datos = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$datos[] = $row;
}
http_response_code(200); // OK
echo json_encode($datos);
} else {
http_response_code(200);
echo json_encode([]); // array vacío
}
// --- 5. Close Resources ---
$stmt->close();
$conn->close();
?>

View File

@ -0,0 +1,41 @@
<?php
// Permitir solicitudes desde cualquier origen (CORS)
header("Access-Control-Allow-Origin: *");
header('Content-Type: application/json; charset=utf-8');
// --- 1. Database Configuration (Example with external config) ---
// In a real scenario, this would come from a file outside the web root.
// For demonstration, let's just make it clear that this should be external.
define('DB_HOST', 'localhost');
define('DB_USER', 'admin');
define('DB_PASS', 'Eavne,e1m'); // !! IMPORTANT: Store this securely in a real application !!
define('DB_NAME', 'clima');
// Crear conexión
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
// Verificar conexión
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$ciudad = $_GET['ciudad'] ?? '';
$hoy = date("Y-m-d");
$sql = "SELECT amanecer, anochecer, MIN(temp_min), MAX(temp_max), MAX(viento_velocidad), AVG(viento_direccion), MAX(nubes), MAX(lluvia)
FROM weather
WHERE fecha LIKE '%" . $hoy . "%' AND ciudad LIKE '%" . $ciudad . "%'";
$result = $conn->query($sql);
$data = array();
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$data[] = $row;
}
echo json_encode($data);
} else {
echo json_encode(array("message" => "No data found"));
}
$conn->close();
?>

BIN
backend/sql/.clima.sql.swp Normal file

Binary file not shown.

35781
backend/sql/clima.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
SELECT
DATE(fecha) AS dia,
MAX(temp_max) AS temp_max,
MIN(temp_min) AS temp_min,
AVG(humedad) AS humedad,
SUM(lluvia) AS lluvia,
AVG(nubes) AS nubes,
AVG(viento_velocidad) AS viento_velocidad,
AVG(viento_direccion) AS viento_direccion
FROM weather
WHERE ciudad LIKE CONCAT('%', 'madrid', '%')
AND DATE(fecha) BETWEEN '2026-02-01' AND '2026-02-17'
GROUP BY DATE(fecha)
ORDER BY DATE(fecha);

8
cron-clima/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM python:3.11-slim
WORKDIR /app
# Instalamos las librerías necesarias para el script
RUN pip install --no-cache-dir requests pymysql
# Copiamos el script al contenedor
COPY weather.py .
# Ejecutamos el script
CMD ["python", "weather.py"]

99
cron-clima/weather.py Normal file
View File

@ -0,0 +1,99 @@
import requests
import pymysql
from datetime import datetime
# -----------------------------
# CONFIGURACIÓN
# -----------------------------
DB_USER = "climauser"
DB_PASS = "climapass123"
DB_HOST = "db"
DB_NAME = "clima"
API_KEY = "69ef7f26726bba12b03c74b1e97b550f"
CIUDADES = [
"Madrid,ES",
"Alfas del Pi,ES",
"L'Ampolla,ES"
]
# -----------------------------
# FUNCIONES
# -----------------------------
def limpiar_nombre_ciudad(nombre):
reemplazos = {
'Á':'A','É':'E','Í':'I','Ó':'O','Ú':'U',
'á':'a','é':'e','í':'i','ó':'o','ú':'u',
'ñ':'n','Ñ':'N'
}
for k, v in reemplazos.items():
nombre = nombre.replace(k, v)
return nombre
def convertir_fecha(timestamp):
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
def obtener_datos_tiempo(ciudad):
url = f"http://api.openweathermap.org/data/2.5/weather?q={ciudad}&appid={API_KEY}&units=metric"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"Error obteniendo datos de {ciudad}: {e}")
return None
def guardar_datos(conn, datos):
ciudad = limpiar_nombre_ciudad(datos["name"])
fecha = convertir_fecha(datos["dt"])
amanecer = convertir_fecha(datos["sys"]["sunrise"])
anochecer = convertir_fecha(datos["sys"]["sunset"])
temp_min = datos["main"]["temp_min"]
temp_max = datos["main"]["temp_max"]
humedad = datos["main"]["humidity"]
viento_velocidad = datos["wind"]["speed"]
viento_direccion = datos["wind"].get("deg", 0)
nubes = datos["clouds"]["all"]
lluvia = datos.get("rain", {}).get("1h", 0)
sql = """
INSERT INTO weather
(fecha, ciudad, amanecer, anochecer, temp_min, temp_max, humedad, viento_velocidad, viento_direccion, nubes, lluvia)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
with conn.cursor() as cursor:
cursor.execute(sql, (fecha, ciudad, amanecer, anochecer, temp_min, temp_max,
humedad, viento_velocidad, viento_direccion, nubes, lluvia))
conn.commit()
print(f"Datos de {ciudad} guardados correctamente.")
# -----------------------------
# PROGRAMA PRINCIPAL
# -----------------------------
def main():
try:
conn = pymysql.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASS,
database=DB_NAME,
charset="utf8mb4"
)
except Exception as e:
print("Error conectando a MySQL:", e)
return
for ciudad in CIUDADES:
datos = obtener_datos_tiempo(ciudad)
if datos:
guardar_datos(conn, datos)
conn.close()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agenda Espiritual AGE</title>
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,600;1,400&family=Nunito:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/bullet-journal.css">
</head>
<body>
<div class="no-print">
<header>
<h1>Bullet Jounal</h1>
</header>
<div class="toolbar">
<div class="day-nav">
<button onclick="changeDay(-1)"></button>
<span id="currentDateDisplay"></span>
<button onclick="changeDay(1)"></button>
</div>
<div class="print-actions">
<button onclick="printFullMonth('A5')">Imprimir Mes (A5)</button>
<button onclick="printPlannerA4()">Planner Corcho (A4)</button>
</div>
</div>
<section class="card daily-panel">
<h3 id="saintDisplay">Cargando santoral...</h3>
<p id="cumpleDisplay" style="color: var(--color-acento);"></p>
<div class="tracker-group">
<label><input type="checkbox" id="rosario" class="checkbox"> Rosario</label>
<label><input type="checkbox" id="vitaminas" class="checkbox"> Vitaminas/Medicina</label>
</div>
<div class="input-group">
<label>Pasos: <input type="number" id="caminar" placeholder="0"></label>
<label>Agua (vasos): <input type="number" id="agua" placeholder="0"></label>
</div>
<label>Reflexión / Notas:</label>
<textarea id="mood" placeholder="¿Cómo ha ido el día?"></textarea>
</section>
<button class="reset" onclick="resetData()">Borrar Historial</button>
</div>
<div id="printArea" class="only-print"></div>
<script src="js/bullet-journal.js"></script>
</body>
</html>

View File

@ -0,0 +1,83 @@
:root {
--color-primario: #F5F5F5;
--color-hover: #1E3A5F;
--color-fondo: #0D0D0D;
--color-texto: #E5E5E5;
--color-acento: #5FAEDB;
--color-borde: rgba(95, 174, 219, 0.3);
--color-tarjeta: rgba(20,20,20,0.85);
}
body {
margin: 0;
font-family: 'Nunito', sans-serif;
background: radial-gradient(circle at top, #111827, #0D0D0D);
color: var(--color-texto);
padding: 15px;
}
/* INTERFAZ PANTALLA */
.no-print { max-width: 600px; margin: auto; }
.toolbar { text-align: center; margin-bottom: 20px; }
.day-nav { display: flex; justify-content: center; align-items: center; gap: 20px; margin-bottom: 15px; }
button {
padding: 10px 15px;
border-radius: 12px;
border: 1px solid var(--color-borde);
background: var(--color-tarjeta);
color: white;
cursor: pointer;
transition: 0.3s;
}
button:hover { background: var(--color-hover); }
.card {
background: var(--color-tarjeta);
padding: 20px;
border-radius: 15px;
border: 1px solid var(--color-borde);
backdrop-filter: blur(10px);
}
textarea {
width: 100%; height: 150px; margin-top: 10px;
background: #111; color: white; border: 1px solid #333; border-radius: 8px; padding: 10px;
}
/* LÓGICA DE IMPRESIÓN */
.only-print { display: none; }
@media print {
.no-print { display: none !important; }
.only-print { display: block !important; }
body { background: white; color: black; }
.page-a5 {
width: 148mm; height: 210mm;
padding: 15mm;
page-break-after: always;
border-bottom: 1px dashed #ccc;
position: relative;
color: black;
}
.dots-bg {
background-image: radial-gradient(#ddd 1px, transparent 1px);
background-size: 5mm 5mm;
height: 120mm;
border: 1px solid #eee;
}
.planner-a4 {
width: 210mm; height: 297mm;
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
background: black;
}
.planner-day { background: white; height: 40mm; padding: 5px; font-size: 10px; }
}

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

@ -0,0 +1,184 @@
/* Variables — mismas que el portfolio tatvilweb */
:root {
--color-primario: #F5F5F5;
--color-hover: #1E3A5F;
--color-secundario: #A1A1A1;
--color-fondo: #0D0D0D;
--color-texto: #E5E5E5;
--blanco-puro: #FFFFFF;
--sombra: rgba(47, 58, 86, 0.15);
--color-tarjeta: #1A1A1A;
--color-acento: #a4d7f4;
--gray: #acb3bf;
--color-borde: rgba(95, 174, 219, 0.3);
--color-cabecera: rgba(0, 0, 0, 0.80);
--bg-nav: #252526;
--bg-card: #2d2d2d;
--border-editor: #3e3e42;
}
body {
font-family: Consolas, 'Inter', sans-serif;
background-color: var(--color-fondo);
color: var(--color-texto);
}
/* Navbar */
.navbar {
background-color: var(--color-cabecera);
border-color: var(--color-borde) !important;
}
.navbar-brand {
font-weight: bold;
color: var(--color-acento) !important;
}
.nav-link {
color: var(--color-texto) !important;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.nav-link:hover,
.nav-link.active {
color: var(--color-acento) !important;
}
/* Selector de ciudad */
.ciudad-select {
background-color: var(--bg-nav);
color: var(--color-texto);
border: 1px solid var(--color-borde);
font-size: 0.85rem;
min-width: 160px;
}
.ciudad-select:focus {
background-color: var(--bg-nav);
color: var(--color-texto);
border-color: var(--color-acento);
box-shadow: 0 0 0 0.15rem rgba(164, 215, 244, 0.2);
}
.ciudad-select:invalid,
.ciudad-select.is-invalid {
border-color: var(--color-borde);
background-image: none;
}
/* Título de sección */
.section-title {
border-left: 5px solid var(--bg-nav);
padding-left: 15px;
color: var(--color-acento);
font-family: Consolas, monospace;
}
/* Navegación de mes */
.mes-navegacion {
font-family: Consolas, monospace;
}
.mes-btn {
background-color: var(--bg-nav);
color: var(--color-acento);
border: 1px solid var(--color-borde);
font-size: 1.2rem;
line-height: 1;
padding: 2px 10px;
transition: 0.2s;
}
.mes-btn:hover {
background-color: var(--color-hover);
color: var(--blanco-puro);
border-color: var(--color-hover);
}
.mes-label {
font-size: 0.95rem;
color: var(--color-texto);
min-width: 90px;
text-align: center;
display: inline-block;
}
/* Cards */
.project-card {
background-color: var(--color-tarjeta);
border: 1px solid var(--color-borde);
border-radius: 1.5rem;
transition: 0.3s;
overflow: hidden;
}
.project-card:hover {
border-color: var(--color-hover);
transform: translateY(-6px);
}
.card-title {
color: var(--color-acento);
background-color: var(--bg-nav);
width: 100%;
padding: 6px 12px;
text-align: center;
border-radius: 10px;
margin-bottom: 1rem;
}
/* Lista de datos */
.dato-list li {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid var(--border-editor);
font-size: 0.9rem;
color: var(--color-texto);
}
.dato-list li:last-child {
border-bottom: none;
}
.dato-label {
color: var(--gray);
font-style: italic;
font-size: 0.8rem;
}
/* Tabla de tendencia histórica */
.trend-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.trend-table th {
color: var(--color-acento);
border-bottom: 1px solid var(--color-borde);
padding: 6px 8px;
text-align: left;
font-weight: 600;
}
.trend-table td {
color: var(--color-texto);
padding: 5px 8px;
border-bottom: 1px solid var(--border-editor);
}
.trend-table tr:last-child td {
border-bottom: none;
}
.trend-table tr:hover td {
background-color: var(--bg-card);
}
/* Footer */
footer {
border-top: 1px solid var(--color-borde);
}

View File

@ -0,0 +1,376 @@
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,600;1,400&family=Nunito:wght@400;600&display=swap');
:root {
/* Paleta refinada */
--color-primario: #F5F5F5; /* Azul noche espiritual */
--color-hover: #1E3A5F; /* Azul profundo para hover */
--color-secundario: #A1A1A1; /* Azul muy suave */
--color-fondo: #0D0D0D; /* Fondo principal */
--color-texto: #E5E5E5; /* Gris claro para texto */
--blanco-puro: #FFFFFF;
--sombra: rgba(47, 58, 86, 0.15);
--color-tarjeta: #1A1A1A;
--color-acento: #5FAEDB;
--color-borde: rgba(95, 174, 219, 0.3);
}
body {
margin: 0;
font-family: 'Nunito', sans-serif;
background: linear-gradient(180deg, #0D0D0D 0%, #111827 100%);
color: var(--color-texto);
line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
header {
padding: 1.5rem 0;
text-align: center;
}
header h1 {
font-family: 'EB Garamond', serif;
letter-spacing: 1px;
color: var(--color-acento);
margin: 0;
}
#city-select {
margin-top: 12px;
padding: 8px 16px;
background: rgba(20, 20, 20, 0.8);
color: var(--color-primario);
border: 1px solid var(--color-borde);
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
nav ul {
list-style: none;
padding: 0;
margin: 0 auto 2rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
}
nav a {
color: var(--color-primario);
border-radius: 20px;
transition: all 0.3s ease;
}
nav a:hover {
background-color: var(--color-hover);
transform: translateY(-2px);
}
.container {
width: 92%;
max-width: 1200px;
margin: auto;
padding: 2rem 0;
display: grid;
gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.tarjeta {
background: rgba(20, 20, 20, 0.75);
backdrop-filter: blur(4px);
border-radius: 14px;
border: 1px solid var(--color-borde);
box-shadow: 0 10px 20px rgba(0,0,0,.35);
transition: all 0.3s ease;
overflow: hidden;
cursor: pointer;
}
.tarjeta:hover {
transform: translateY(-6px);
box-shadow: 0 16px 30px rgba(0,0,0,.5);
}
.tarjeta h2 {
margin: 0;
padding: 12px;
font-family: 'EB Garamond', serif;
font-size: 1.3rem;
text-align: center;
color: var(--color-acento);
background: rgba(13, 13, 13, 0.8);
border-bottom: 1px solid rgba(255,255,255,.1);
}
/* --- Párrafos generales de todas las tarjetas --- */
.tarjeta p {
text-align: center;
color: var(--color-texto);
font-size: 1rem;
margin: 2px 0; /* menos espacio entre líneas */
padding: 0; /* eliminar padding extra */
}
/* --- Ajuste para dispositivos pequeños --- */
@media (max-width: 600px) {
header h1 {
font-size: 1.6rem;
}
nav ul {
flex-direction: column;
align-items: center;
}
.container {
gap: 1.2rem;
}
}
footer {
text-align: center;
padding: 1.5rem 0;
color: var(--color-secundario);
margin-top: 3rem;
background: transparent;
}
/* --- Tarjeta Amanecer/Anochecer: más compacta --- */
#moon-mini-calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 6px;
margin-top: 10px;
text-align: center;
}
.moon-day {
padding: 4px;
border-radius: 6px;
background: var(--color-fondo);
font-size: 0.9rem;
}
.moon-today {
background: var(--color-hover);
font-weight: bold;
}
.moon-icon {
font-size: 1.4rem;
display: block;
}
#moon-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin-top: 10px;
margin-bottom: 6px;
text-align: center;
font-weight: 600;
color: var(--color-acento);
font-size: 0.85rem;
}
#moon-weekdays span {
padding: 4px 0;
border-bottom: 1px solid var(--color-borde);
}
/* ---------- BULLET JOURNAL SUMMARY ---------- */
#bullet-summary {
cursor: default;
}
#bullet-summary .bj-resumen {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
padding: 14px;
}
#bullet-summary .bj-item {
background: rgba(13,13,13,0.8);
border: 1px solid var(--color-borde);
border-radius: 10px;
padding: 10px;
text-align: center;
box-shadow: inset 0 0 12px rgba(0,0,0,.4);
}
#bullet-summary .bj-item span {
display: block;
font-size: 0.75rem;
color: var(--color-secundario);
letter-spacing: .5px;
}
#bullet-summary .bj-item strong {
font-size: 1.4rem;
font-family: 'EB Garamond', serif;
color: var(--color-acento);
}
/* Botón */
#bullet-summary .btn , #weather-card .btn {
display: block;
width: 75%;
margin: 12px auto 14px;
padding: 10px;
background: linear-gradient(135deg, #1E3A5F, #111827);
color: var(--blanco-puro);
border-radius: 12px;
text-align: center;
text-decoration: none;
font-size: 0.9rem;
border: 1px solid var(--color-borde);
transition: all .3s ease;
}
#bullet-summary .btn:hover, #weather-card .btn:hover {
background: linear-gradient(135deg, #2b4f80, #1E293B);
transform: translateY(-2px);
}
/* ICONOS */
.termometro {
width: 48px;
}
.termometro.neutro {
fill: var(--color-primario);
}
.termometro.frio {
fill: var(--color-acento);
}
.termometro.calor {
fill: #f44336;
}
.termometro.templado { fill: #4caf50; }
#mes-navegacion {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.mes-btn {
background: rgba(20, 20, 20, 0.8);
color: var(--color-acento);
border: 1px solid var(--color-borde);
border-radius: 8px;
padding: 4px 12px;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'EB Garamond', serif;
}
.mes-btn:hover {
background: rgba(31, 53, 88, 0.9);
transform: translateY(-2px);
}
#mes-nombre {
font-family: 'EB Garamond', serif;
font-size: 1.2rem;
color: var(--color-acento);
min-width: 80px;
text-align: center;
}
/* =======================
Tabla de tendencia
======================= */
.trend-table {
width: 100%;
border-collapse: collapse;
margin-top: 1em;
font-family: 'Nunito', sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-radius: 8px;
overflow: hidden;
}
/* Cabecera */
.trend-table thead {
color: var(--color-acento);
background-color: rgba(13, 13, 13, 0.85);
}
.trend-table thead th {
padding: 10px 15px;
text-align: center;
font-weight: 600;
}
/* Filas del cuerpo */
.trend-table tbody tr {
border-bottom: 1px solid var(--color-borde);
}
.trend-table tbody tr:nth-child(even) {
background-color: rgba(20, 20, 20, 0.8);
}
.trend-table tbody td {
padding: 8px 12px;
text-align: center;
font-weight: 500;
}
/* Colores según tipo de dato */
.trend-table tbody td:nth-child(2) { /* Máx */
color: #e74c3c; /* rojo */
font-weight: 600;
}
.trend-table tbody td:nth-child(3) { /* Mín */
color: #3498db; /* azul */
font-weight: 600;
}
.trend-table tbody td:nth-child(4) { /* Lluvia */
color: #27ae60; /* verde */
font-weight: 600;
}
/* Hover para destacar fila */
.trend-table tbody tr:hover {
background-color: rgba(31, 53, 88, 0.9);
}
/* Adaptable a móviles */
@media (max-width: 600px) {
.trend-table thead {
display: none;
}
.trend-table tbody td {
display: block;
text-align: right;
padding-left: 50%;
position: relative;
}
.trend-table tbody td::before {
content: attr(data-label);
position: absolute;
left: 10px;
font-weight: 600;
text-align: left;
}
}

View File

@ -0,0 +1,12 @@
[
{ "fecha": "01-27", "persona": "Prueba Prueba" },
{ "fecha": "02-14", "persona": "Andrea Postlbauer" },
{ "fecha": "04-08", "persona": "Tatiana Villa" },
{ "fecha": "10-14", "persona": "Jose Luis Villa" },
{ "fecha": "10-17", "persona": "Patricia Villa" },
{ "fecha": "10-19", "persona": "Mercedes Ema" },
{ "fecha": "10-24", "persona": "Arantxa Villa" },
{ "fecha": "11-24", "persona": "Nicolas Postlbauer" },
{ "fecha": "11-27", "persona": "Leo Postlbauer" }
]

368
frontend/data/santos.json Normal file
View File

@ -0,0 +1,368 @@
[
{ "fecha": "2026-01-01", "santo": "Santa María, Madre de Dios", "color": "rosa" },
{ "fecha": "2026-01-02", "santo": "San Basilio Magno y San Gregorio Nacianceno" },
{ "fecha": "2026-01-03", "santo": "Santísimo Nombre de Jesús" },
{ "fecha": "2026-01-04", "santo": "Santa Ángela de Foligno" },
{ "fecha": "2026-01-05", "santo": "San Genovevo Torres"},
{ "fecha": "2026-01-06", "santo": "Epifanía del Señor", "color": "rojo" },
{ "fecha": "2026-01-07", "santo": "San Raimundo de Peñafort" },
{ "fecha": "2026-01-08", "santo": "San Severino" },
{ "fecha": "2026-01-09", "santo": "San Eulogio de Córdoba" },
{ "fecha": "2026-01-10", "santo": "San Gonzalo" },
{ "fecha": "2026-01-11", "santo": "Bautismo del Señor", "color": "blanco" },
{ "fecha": "2026-01-12", "santo": "Santa Tatiana" },
{ "fecha": "2026-01-13", "santo": "San Hilario de Poitiers" },
{ "fecha": "2026-01-14", "santo": "San Félix de Nola" },
{ "fecha": "2026-01-15", "santo": "San Mauro" },
{ "fecha": "2026-01-16", "santo": "San Marcelo I, Papa" },
{ "fecha": "2026-01-17", "santo": "San Antonio Abad", "descripcion":"Patron de los animales" },
{ "fecha": "2026-01-18", "santo": "Santa Prisca" },
{ "fecha": "2026-01-19", "santo": "San Mario", "descripcion":"y familia" },
{ "fecha": "2026-01-20", "santo": "San Sebastián", "descripcion":"mártir" },
{ "fecha": "2026-01-21", "santo": "Santa Inés", "descripcion":"virgen y mártir" },
{ "fecha": "2026-01-22", "santo": "San Vicente", "descripcion":"mártir" },
{ "fecha": "2026-01-23", "santo": "San Ildefonso de Toledo", "descripcion":"obispo" },
{ "fecha": "2026-01-24", "santo": "San Francisco de Sales", "descripcion":"Doctor de la Iglesia" },
{ "fecha": "2026-01-25", "santo": "Conversión de San Pablo", "descripcion":"apóstol" },
{ "fecha": "2026-01-26", "santo": "San Timoteo y San Tito", "descripcion":"obispos" },
{ "fecha": "2026-01-27", "santo": "Santa Ángela Merici" },
{ "fecha": "2026-01-28", "santo": "Santo Tomás de Aquino", "descripcion":"Doctor de la Iglesia. Patrón de los estudiantes, teólogo, filósofo" },
{ "fecha": "2026-01-29", "santo": "San Valero de Zaragoza" },
{ "fecha": "2026-01-30", "santo": "Santa Martina" },
{ "fecha": "2026-01-31", "santo": "San Juan Bosco", "descripcion":"Fundador de los Salesianos" },
{ "fecha": "2026-02-01", "santo": "Santa Brígida", "descripcion":"Patrona de Europa" },
{ "fecha": "2026-02-02", "santo": "Presentación del Señor (Candelaria)", "descripcion":"La Virgen María y San José presentan al Niño Jesús en el Templo" },
{ "fecha": "2026-02-03", "santo": "San Blas", "descripcion":"Protector de las enfermedades de garganta" },
{ "fecha": "2026-02-04", "santo": "San Gilberto", "descripcion":"obispo" },
{ "fecha": "2026-02-05", "santo": "Santa Águeda", "descripcion":"Patrona de las mujeres" },
{ "fecha": "2026-02-06", "santo": "San Pablo Miki", "descripcion":"y compañeros mártires" },
{ "fecha": "2026-02-07", "santo": "San Ricardo", "descripcion":"mártir" },
{ "fecha": "2026-02-08", "santo": "San Jerónimo Emiliani", "descripcion":"Patrón de los huérfanos" },
{ "fecha": "2026-02-09", "santo": "Santa Apolonia", "descripcion":"mártir" },
{ "fecha": "2026-02-10", "santo": "Santa Escolástica", "descripcion":"hermana de San Benito" },
{ "fecha": "2026-02-11", "santo": "Nuestra Señora de Lourdes", "descripcion":"La Virgen María se apareció en Lourdes a Santa Bernardita" },
{ "fecha": "2026-02-12", "santo": "Santa Eulalia de Barcelona", "descripcion":"virgen y mártir" },
{ "fecha": "2026-02-13", "santo": "San Benigno", "descripcion":"mártir" },
{ "fecha": "2026-02-14", "santo": "San Valentín","descripcion":"Patrón de los enamorados" },
{ "fecha": "2026-02-15", "santo": "San Claudio de la Colombière","descripcion":"confesor" },
{ "fecha": "2026-02-16", "santo": "San Onésimo", "descripcion":"esclavo convertido por San Pablo" },
{ "fecha": "2026-02-17", "santo": "Los Siete Santos Fundadores","descripcion":"de la Orden de los Servitas" },
{ "fecha": "2026-02-18", "santo": "Miércoles de Ceniza","descripcion":"Inicio de la Cuaresma" },
{ "fecha": "2026-02-19", "santo": "San Álvaro de Córdoba", "descripcion":"confesor" },
{ "fecha": "2026-02-20", "santo": "San Eleuterio" },
{ "fecha": "2026-02-21", "santo": "San Pedro Damián", "descripcion":"obispo y doctor de la Iglesia" },
{ "fecha": "2026-02-22", "santo": "Cátedra de San Pedro", "descripcion":"Fiesta de San Pedro, apóstol" },
{ "fecha": "2026-02-23", "santo": "San Policarpo", "descripcion":"obispo y mártir" },
{ "fecha": "2026-02-24", "santo": "San Modesto", "descripcion":"mártir" },
{ "fecha": "2026-02-25", "santo": "San Cesáreo de Nazianzo", "descripcion":"obispo" },
{ "fecha": "2026-02-26", "santo": "San Alejandro de Alejandría", "descripcion":"mártir" },
{ "fecha": "2026-02-27", "santo": "San Leandro de Sevilla", "descripcion":"obispo" },
{ "fecha": "2026-02-28", "santo": "San Román", "descripcion":"mártir" },
{ "fecha": "2026-02-29", "santo": "San Osvaldo" },
{ "fecha": "2026-03-01", "santo": "San David de Gales" },
{ "fecha": "2026-03-02", "santo": "San Chad de Mercia" },
{ "fecha": "2026-03-03", "santo": "San Casimiro", "descripcion":"Patrón de Polonia" },
{ "fecha": "2026-03-04", "santo": "San Lucio I, Papa" },
{ "fecha": "2026-03-05", "santo": "San Adrián de Nicomedia" },
{ "fecha": "2026-03-06", "santo": "San Coleta de Corbie" },
{ "fecha": "2026-03-07", "santo": "San Perpetuo y San Feliciano" },
{ "fecha": "2026-03-08", "santo": "San Juan de Dios", "descripcion":"Patrón de los hospitales" },
{ "fecha": "2026-03-09", "santo": "San Francescó de Borgia" },
{ "fecha": "2026-03-10", "santo": "San Macario de Alejandría" },
{ "fecha": "2026-03-11", "santo": "San Eulogio de Córdoba" },
{ "fecha": "2026-03-12", "santo": "San Gregorio Nacianceno" },
{ "fecha": "2026-03-13", "santo": "Santa Luisa de Marillac" },
{ "fecha": "2026-03-14", "santo": "San Matías", "descripcion":"apóstol" },
{ "fecha": "2026-03-15", "santo": "San Longino" },
{ "fecha": "2026-03-16", "santo": "Santa Juana de Chantal" },
{ "fecha": "2026-03-17", "santo": "San Patricio", "descripcion":"Patrón de Irlanda" },
{ "fecha": "2026-03-18", "santo": "San Cirilo de Jerusalén" },
{ "fecha": "2026-03-19", "santo": "San José", "descripcion":"Esposo de la Virgen María" },
{ "fecha": "2026-03-20", "santo": "San Cándido", "descripcion":"mártir" },
{ "fecha": "2026-03-21", "santo": "Santa Benedicta de la Cruz" },
{ "fecha": "2026-03-22", "santo": "San León I, Papa" },
{ "fecha": "2026-03-23", "santo": "San Turibio de Mogrovejo" },
{ "fecha": "2026-03-24", "santo": "San Gabriel de la Dolorosa" },
{ "fecha": "2026-03-25", "santo": "Anunciación del Señor", "descripcion":"El angel se apareció a la Virgen María"},
{ "fecha": "2026-03-26", "santo": "San Ezequiel Moreno" },
{ "fecha": "2026-03-27", "santo": "San Ruperto" },
{ "fecha": "2026-03-28", "santo": "San Guntrán" },
{ "fecha": "2026-03-29", "santo": "San Bertoldo" },
{ "fecha": "2026-03-30", "santo": "San Amando de Maastricht" },
{ "fecha": "2026-03-31", "santo": "San Benedicto José Labre" },
{ "fecha": "2026-04-01", "santo": "San Hugo de Grenoble" },
{ "fecha": "2026-04-02", "santo": "San Francisco de Paula" },
{ "fecha": "2026-04-03", "santo": "San Ricardo Pampuri" },
{ "fecha": "2026-04-04", "santo": "San Isidoro de Sevilla" },
{ "fecha": "2026-04-05", "santo": "San Vicente Ferrer" },
{ "fecha": "2026-04-06", "santo": "San Marcelino Champagnat" },
{ "fecha": "2026-04-07", "santo": "San Juan Bautista de la Salle" },
{ "fecha": "2026-04-08", "santo": "San Dionisio", "descripcion":"(obispo) y compañeros mártires" },
{ "fecha": "2026-04-09", "santo": "San Casimiro" },
{ "fecha": "2026-04-10", "santo": "San Ezequiel Moreno" },
{ "fecha": "2026-04-11", "santo": "San Esteban I", "descripcion":"Papa y mártir" },
{ "fecha": "2026-04-12", "santo": "San León IX", "descripcion":"Papa" },
{ "fecha": "2026-04-13", "santo": "San Hermenegildo" },
{ "fecha": "2026-04-14", "santo": "San Matías", "descripcion":"apóstol" },
{ "fecha": "2026-04-15", "santo": "San Dámaso I", "descripcion":"Papa" },
{ "fecha": "2026-04-16", "santo": "San Bernabé", "descripcion":"apóstol" },
{ "fecha": "2026-04-17", "santo": "San Aniceto", "descripcion":"Papa y mártir" },
{ "fecha": "2026-04-18", "santo": "San Apuleyo" },
{ "fecha": "2026-04-19", "santo": "San Expedito" },
{ "fecha": "2026-04-20", "santo": "San Jorge", "descripcion":"mártir" },
{ "fecha": "2026-04-21", "santo": "San Anselmo de Canterbury" },
{ "fecha": "2026-04-22", "santo": "San Soter y San Calixto", "descripcion":"Papas y mártires" },
{ "fecha": "2026-04-23", "santo": "San Jorge", "descripcion":"mártir" },
{ "fecha": "2026-04-24", "santo": "San Fidel de Sigmaringa" },
{ "fecha": "2026-04-25", "santo": "San Marcos", "descripcion":"evangelista" },
{ "fecha": "2026-04-26", "santo": "San Pedro de Verona" },
{ "fecha": "2026-04-27", "santo": "San Zita" },
{ "fecha": "2026-04-28", "santo": "San Luis María Grignion de Montfort" },
{ "fecha": "2026-04-29", "santo": "San Pedro Chanel" },
{ "fecha": "2026-04-30", "santo": "San Pío V", "descripcion":"Papa" },
{ "fecha": "2026-05-01", "santo": "San José Obrero"},
{ "fecha": "2026-05-02", "santo": "San Atanasio" },
{ "fecha": "2026-05-03", "santo": "San Felipe y Santiago", "descripcion":"apóstoles" },
{ "fecha": "2026-05-04", "santo": "San Florencio de Orange" },
{ "fecha": "2026-05-05", "santo": "San Hilario de Arlés" },
{ "fecha": "2026-05-06", "santo": "San Juan de Ávila" },
{ "fecha": "2026-05-07", "santo": "San Esteban de Hungría", "descripcion":"Rey de Hungría" },
{ "fecha": "2026-05-08", "santo": "San Miguel Garicoits" },
{ "fecha": "2026-05-09", "santo": "San Gregorio Magno" },
{ "fecha": "2026-05-10", "santo": "San Antonino de Florencia" },
{ "fecha": "2026-05-11", "santo": "San Ignacio de Loyola", "descripcion":"Fundador de la Compañía de Jesús" },
{ "fecha": "2026-05-12", "santo": "Santa Nereida" },
{ "fecha": "2026-05-13", "santo": "Nuestra Señora de Fátima", "descripcion":"La Virgen María se apareció en Fátima a tres pastorcitos" },
{ "fecha": "2026-05-14", "santo": "San Matías", "descripcion":"apóstol" },
{ "fecha": "2026-05-15", "santo": "San Isidro Labrador", "descripcion":"Patrón de los agricultores" },
{ "fecha": "2026-05-16", "santo": "San Juan Nepomuceno" },
{ "fecha": "2026-05-17", "santo": "San Pasquale Baylón" },
{ "fecha": "2026-05-18", "santo": "San Venancio" },
{ "fecha": "2026-05-19", "santo": "San Celestino V", "descripcion":"Papa" },
{ "fecha": "2026-05-20", "santo": "San Bernardino de Siena" },
{ "fecha": "2026-05-21", "santo": "Santa María Magdalena de Pazzi" },
{ "fecha": "2026-05-22", "santo": "Santa Rita de Casia" },
{ "fecha": "2026-05-23", "santo": "San Desiderio" },
{ "fecha": "2026-05-24", "santo": "Nuestra Señora, Auxilio de los Cristianos" },
{ "fecha": "2026-05-25", "santo": "San Gregorio VII", "descripcion":"Papa" },
{ "fecha": "2026-05-26", "santo": "San Felipe Neri" },
{ "fecha": "2026-05-27", "santo": "San Agustín de Cantorbery" },
{ "fecha": "2026-05-28", "santo": "San Germán de París" },
{ "fecha": "2026-05-29", "santo": "San Maximiliano Kolbe" },
{ "fecha": "2026-05-30", "santo": "Santa Juana de Arco" },
{ "fecha": "2026-05-31", "santo": "Visita de la Virgen María a su prima Santa Isabel" },
{ "fecha": "2026-06-01", "santo": "San Justino Mártir" },
{ "fecha": "2026-06-02", "santo": "San Marcelino de París" },
{ "fecha": "2026-06-03", "santo": "Santos Carlos Lwanga y compañeros mártires" },
{ "fecha": "2026-06-04", "santo": "San Francisco Caracciolo" },
{ "fecha": "2026-06-05", "santo": "San Bonifacio M. de Ligorio" },
{ "fecha": "2026-06-06", "santo": "San Norberto" },
{ "fecha": "2026-06-07", "santo": "San Roberto Belarmino" },
{ "fecha": "2026-06-08", "santo": "San Medardo" },
{ "fecha": "2026-06-09", "santo": "San Efrén" },
{ "fecha": "2026-06-10", "santo": "San Guillermo de Vercelli" },
{ "fecha": "2026-06-11", "santo": "San Bernabé, apóstol" },
{ "fecha": "2026-06-12", "santo": "Santos Juan y Pablo, mártires" },
{ "fecha": "2026-06-13", "santo": "San Antonio de Padua", "descripcion":"Doctor de la Iglesia" },
{ "fecha": "2026-06-14", "santo": "San Elías Profeta" },
{ "fecha": "2026-06-15", "santo": "San Vito", "descripcion":"y compañeros mártires" },
{ "fecha": "2026-06-16", "santo": "San Juan Francisco Régis" },
{ "fecha": "2026-06-17", "santo": "San Alberto Chmielowski" },
{ "fecha": "2026-06-18", "santo": "San Gregorio Barbarigo" },
{ "fecha": "2026-06-19", "santo": "Santo Romualdo", "descripcion":"y compañeros monjes" },
{ "fecha": "2026-06-20", "santo": "San Silverio", "descripcion":"Papa y mártir" },
{ "fecha": "2026-06-21", "santo": "San Luis Gonzaga" },
{ "fecha": "2026-06-22", "santo": "Santa Paulina" },
{ "fecha": "2026-06-23", "santo": "San José Cafasso" },
{ "fecha": "2026-06-24", "santo": "Natividad de San Juan Bautista", "descripcion":"Nacimiento de San Juan Bautista" },
{ "fecha": "2026-06-25", "santo": "Santos Cirilo y Metodio" },
{ "fecha": "2026-06-26", "santo": "San José María de Yermo y Parres" },
{ "fecha": "2026-06-27", "santo": "San Ladislao" },
{ "fecha": "2026-06-28", "santo": "San Ireneo", "descripcion":"obispo y mártir" },
{ "fecha": "2026-06-29", "santo": "San Pedro y San Pablo", "descripcion":"apóstoles" },
{ "fecha": "2026-06-30", "santo": "San Justo de Alcalá" },
{ "fecha": "2026-07-01", "santo": "Santa María Goretti" },
{ "fecha": "2026-07-02", "santo": "San Martín de Porres" },
{ "fecha": "2026-07-03", "santo": "Santos Tomás y Feliciano", "descripcion":"mártires" },
{ "fecha": "2026-07-04", "santo": "San Ulrico de Augsburgo" },
{ "fecha": "2026-07-05", "santo": "San Antonio María Zaccaria" },
{ "fecha": "2026-07-06", "santo": "San María Isabel de la Trinidad" },
{ "fecha": "2026-07-07", "santo": "San Cayetano" },
{ "fecha": "2026-07-08", "santo": "San Procopio" },
{ "fecha": "2026-07-09", "santo": "San Agustín Zhao Rong y compañeros mártires" },
{ "fecha": "2026-07-10", "santo": "San Benito", "descripcion":"Abad. Fundador de la Orden Benedictina. Medalla de San Benito ()" },
{ "fecha": "2026-07-11", "santo": "San Juan Bautista de la Salle" },
{ "fecha": "2026-07-12", "santo": "San Nabor y San Félix", "descripcion":"mártires" },
{ "fecha": "2026-07-13", "santo": "San Enrique" },
{ "fecha": "2026-07-14", "santo": "San Camilo de Lelis" },
{ "fecha": "2026-07-15", "santo": "Santa María Gorretti" },
{ "fecha": "2026-07-16", "santo": "Nuestra Señora del Carmen", "descripcion":"Patrona de los Carmelitas y los pescadores. Escapulario" },
{ "fecha": "2026-07-17", "santo": "San Alejo" },
{ "fecha": "2026-07-18", "santo": "San Camilo de Lelis" },
{ "fecha": "2026-07-19", "santo": "San Vicente de Paúl" },
{ "fecha": "2026-07-20", "santo": "San Apolinario" },
{ "fecha": "2026-07-21", "santo": "San Lorenzo de Brindis" },
{ "fecha": "2026-07-22", "santo": "Santa María Magdalena", "descripcion":"la apóstol de los apóstoles" },
{ "fecha": "2026-07-23", "santo": "Santos Apeles y Clemente", "descripcion":"mártires" },
{ "fecha": "2026-07-24", "santo": "San Cristóbal Magallanes y compañeros", "descripcion":"mártires" },
{ "fecha": "2026-07-25", "santo": "Santiago", "descripcion":"apóstol" },
{ "fecha": "2026-07-26", "santo": "San Joaquín y Santa Ana" },
{ "fecha": "2026-07-27", "santo": "Santa Marta" },
{ "fecha": "2026-07-28", "santo": "San Pedro Crisólogo" },
{ "fecha": "2026-07-29", "santo": "Santa María de los Ángeles" },
{ "fecha": "2026-07-30", "santo": "San Abdon y San Sennen", "descripcion":"mártires" },
{ "fecha": "2026-07-31", "santo": "San Ignacio de Loyola" },
{ "fecha": "2026-08-01", "santo": "San Alfonso María de Ligorio" },
{ "fecha": "2026-08-02", "santo": "Santa Eusebia" },
{ "fecha": "2026-08-03", "santo": "San Lamberto", "descripcion":"obispo y mártir" },
{ "fecha": "2026-08-04", "santo": "San Juan María Vianney" },
{ "fecha": "2026-08-05", "santo": "Dedicatoria de la Basílica de Letrán" },
{ "fecha": "2026-08-06", "santo": "Transfiguración del Señor"},
{ "fecha": "2026-08-07", "santo": "San Cajetano" },
{ "fecha": "2026-08-08", "santo": "San Dominico" },
{ "fecha": "2026-08-09", "santo": "San Román" },
{ "fecha": "2026-08-10", "santo": "San Lorenzo", "descripcion":"diácono y mártir" },
{ "fecha": "2026-08-11", "santo": "Santa Clara de Asís" },
{ "fecha": "2026-08-12", "santo": "San Maximiliano Kolbe" },
{ "fecha": "2026-08-13", "santo": "San Poncio", "descripcion":"mártir" },
{ "fecha": "2026-08-14", "santo": "San Maximiliano Kolbe" },
{ "fecha": "2026-08-15", "santo": "Asunción de la Virgen María" },
{ "fecha": "2026-08-16", "santo": "San Esteban de Hungría" },
{ "fecha": "2026-08-17", "santo": "San Jacinto" },
{ "fecha": "2026-08-18", "santo": "San Alberto Hurtado" },
{ "fecha": "2026-08-19", "santo": "San Juan Eudes" },
{ "fecha": "2026-08-20", "santo": "San Bernardo de Claraval" },
{ "fecha": "2026-08-21", "santo": "San Pío X, Papa" },
{ "fecha": "2026-08-22", "santo": "Santa María Reina" },
{ "fecha": "2026-08-23", "santo": "San Rosa de Lima" },
{ "fecha": "2026-08-24", "santo": "San Bartolomé", "descripcion":"apóstol" },
{ "fecha": "2026-08-25", "santo": "San Luis IX", "descripcion":"rey de Francia" },
{ "fecha": "2026-08-26", "santo": "San José de Calasanz" },
{ "fecha": "2026-08-27", "santo": "Santa Mónica" },
{ "fecha": "2026-08-28", "santo": "San Agustín", "descripcion":"obispo y doctor de la Iglesia. Fundador de los agustinos" },
{ "fecha": "2026-08-29", "santo": "Martirio de San Juan Bautista" },
{ "fecha": "2026-08-30", "santo": "Santa Rosa de Lima" },
{ "fecha": "2026-08-31", "santo": "San Ramón Nonato", "descripcion":"santo de las parturientas" },
{ "fecha": "2026-09-01", "santo": "San Egidio", "descripcion":"abate" },
{ "fecha": "2026-09-02", "santo": "Santa María de la Cabeza" },
{ "fecha": "2026-09-03", "santo": "San Gregorio Magno" },
{ "fecha": "2026-09-04", "santo": "San Rosendo" },
{ "fecha": "2026-09-05", "santo": "Santa Teresa de Calcuta" },
{ "fecha": "2026-09-06", "santo": "San Zacarías, profeta" },
{ "fecha": "2026-09-07", "santo": "San Cayetano" },
{ "fecha": "2026-09-08", "santo": "Natividad de la Virgen María", "color": "blanco" },
{ "fecha": "2026-09-09", "santo": "San Pedro Claver" },
{ "fecha": "2026-09-10", "santo": "San Nicolás de Tolentino" },
{ "fecha": "2026-09-11", "santo": "San Juan Gabriel Perboyre" },
{ "fecha": "2026-09-12", "santo": "Santísimo Nombre de María" },
{ "fecha": "2026-09-13", "santo": "San Juan Crisóstomo, obispo y doctor de la Iglesia" },
{ "fecha": "2026-09-14", "santo": "Exaltación de la Santa Cruz", "color": "rojo" },
{ "fecha": "2026-09-15", "santo": "Nuestra Señora de los Dolores" },
{ "fecha": "2026-09-16", "santo": "San Cornelio, Papa y San Cipriano, obispo, mártires" },
{ "fecha": "2026-09-17", "santo": "San Roberto Bellarmino" },
{ "fecha": "2026-09-18", "santo": "San José de Cupertino" },
{ "fecha": "2026-09-19", "santo": "San Januario, obispo y mártir" },
{ "fecha": "2026-09-20", "santo": "San Andrés Kim Taegon y compañeros mártires" },
{ "fecha": "2026-09-21", "santo": "San Mateo, apóstol y evangelista", "color": "rojo" },
{ "fecha": "2026-09-22", "santo": "San Maurilio" },
{ "fecha": "2026-09-23", "santo": "San Pío de Pietrelcina" },
{ "fecha": "2026-09-24", "santo": "Nuestra Señora de la Merced" },
{ "fecha": "2026-09-25", "santo": "San Cleofás" },
{ "fecha": "2026-09-26", "santo": "San Cosme y San Damián, mártires" },
{ "fecha": "2026-09-27", "santo": "San Vicente de Paúl" },
{ "fecha": "2026-09-28", "santo": "San Wenceslao" },
{ "fecha": "2026-09-29", "santo": "Santos Arcángeles Miguel, Gabriel y Rafael", "color": "blanco" },
{ "fecha": "2026-09-30", "santo": "San Jerónimo, sacerdote y doctor de la Iglesia" },
{ "fecha": "2026-10-01", "santo": "Santa Teresa de Lisieux" },
{ "fecha": "2026-10-02", "santo": "Ángel de la Guarda" },
{ "fecha": "2026-10-03", "santo": "San Gerardo Majella" },
{ "fecha": "2026-10-04", "santo": "San Francisco de Asís", "color": "rojo" },
{ "fecha": "2026-10-05", "santo": "Santa Faustina Kowalska" },
{ "fecha": "2026-10-06", "santo": "San Bruno" },
{ "fecha": "2026-10-07", "santo": "Nuestra Señora del Rosario", "color": "blanco" },
{ "fecha": "2026-10-08", "santo": "San Dionisio y compañeros mártires" },
{ "fecha": "2026-10-09", "santo": "San Juan Leonardi" },
{ "fecha": "2026-10-10", "santo": "San Daniel Comboni" },
{ "fecha": "2026-10-11", "santo": "San Juan XXIII, Papa" },
{ "fecha": "2026-10-12", "santo": "Nuestra Señora de Guadalupe", "color": "blanco" },
{ "fecha": "2026-10-13", "santo": "San Eduardo el Confesor" },
{ "fecha": "2026-10-14", "santo": "San Calixto I, Papa y mártir" },
{ "fecha": "2026-10-15", "santo": "Santa Teresa de Ávila, virgen y doctora de la Iglesia" },
{ "fecha": "2026-10-16", "santo": "San Gerardo de Brogne" },
{ "fecha": "2026-10-17", "santo": "San Ignacio de Antioquía, obispo y mártir" },
{ "fecha": "2026-10-18", "santo": "San Lucas, evangelista", "color": "rojo" },
{ "fecha": "2026-10-19", "santo": "San Pablo de la Cruz", "color": "rojo" },
{ "fecha": "2026-10-20", "santo": "San Juan de Capistrano" },
{ "fecha": "2026-10-21", "santo": "San Hilarión" },
{ "fecha": "2026-10-22", "santo": "San Juan Pablo II, Papa" },
{ "fecha": "2026-10-23", "santo": "San Juan de Brébeuf y San Isaac Jogues, sacerdotes y compañeros mártires" },
{ "fecha": "2026-10-24", "santo": "San Antonio María Claret" },
{ "fecha": "2026-10-25", "santo": "San Crispín y San Crispiniano, mártires" },
{ "fecha": "2026-10-26", "santo": "San Evaristo, Papa y mártir" },
{ "fecha": "2026-10-27", "santo": "San Frumencio" },
{ "fecha": "2026-10-28", "santo": "San Simón y San Judas, apóstoles", "color": "rojo" },
{ "fecha": "2026-10-29", "santo": "San Narciso de Jerusalén" },
{ "fecha": "2026-10-30", "santo": "San Andrés Avellino" },
{ "fecha": "2026-10-31", "santo": "San Wolfgang de Ratisbona" },
{ "fecha": "2026-11-01", "santo": "Todos los Santos", "color": "blanco" },
{ "fecha": "2026-11-02", "santo": "Conmemoración de los Fieles Difuntos", "color": "negro" },
{ "fecha": "2026-11-03", "santo": "San Martín de Tours" },
{ "fecha": "2026-11-04", "santo": "San Carlos Borromeo" },
{ "fecha": "2026-11-05", "santo": "San Leónidas y compañeros mártires" },
{ "fecha": "2026-11-06", "santo": "San Leonardo de Noblac" },
{ "fecha": "2026-11-07", "santo": "San Willibrord" },
{ "fecha": "2026-11-08", "santo": "San Godofredo de Amiens" },
{ "fecha": "2026-11-09", "santo": "Dedicación de la Basílica de San Juan de Letrán", "color": "blanco" },
{ "fecha": "2026-11-10", "santo": "San León III, Papa" },
{ "fecha": "2026-11-11", "santo": "San Martín de Tours" },
{ "fecha": "2026-11-12", "santo": "San Josafat Kuncevyc" },
{ "fecha": "2026-11-13", "santo": "Santa Francesca Romana" },
{ "fecha": "2026-11-14", "santo": "San Gerardo Sagredo" },
{ "fecha": "2026-11-15", "santo": "Santa Margarita de Escocia" },
{ "fecha": "2026-11-16", "santo": "San Gerardo Majella" },
{ "fecha": "2026-11-17", "santo": "San Gregorio III, Papa" },
{ "fecha": "2026-11-18", "santo": "San Romualdo" },
{ "fecha": "2026-11-19", "santo": "San Elredo de Rievaulx" },
{ "fecha": "2026-11-20", "santo": "Santa Felicidad y compañeros mártires" },
{ "fecha": "2026-11-21", "santo": "Presentación de la Virgen María", "color": "blanco" },
{ "fecha": "2026-11-22", "santo": "San Cecilia, virgen y mártir", "descripcion":"Patrona de la música" },
{ "fecha": "2026-11-23", "santo": "San Clemente I, Papa y mártir" },
{ "fecha": "2026-11-24", "santo": "San Andrés Dung-Lac y compañeros mártires" },
{ "fecha": "2026-11-25", "santo": "San Catalina de Alejandría, virgen y mártir" },
{ "fecha": "2026-11-26", "santo": "San Silvestre I, Papa" },
{ "fecha": "2026-11-27", "santo": "San Virgilio de Salzburgo" },
{ "fecha": "2026-11-28", "santo": "San Leandro de Sevilla" },
{ "fecha": "2026-11-29", "santo": "San Saturnino" },
{ "fecha": "2026-11-30", "santo": "San Andrés, apóstol", "color": "rojo" },
{ "fecha": "2026-12-01", "santo": "Santa Elena" },
{ "fecha": "2026-12-02", "santo": "San Bibiano, mártir" },
{ "fecha": "2026-12-03", "santo": "San Francisco Javier" },
{ "fecha": "2026-12-04", "santo": "San Juan Damasceno" },
{ "fecha": "2026-12-05", "santo": "Santa Sabela" },
{ "fecha": "2026-12-06", "santo": "San Nicolás de Bari" },
{ "fecha": "2026-12-07", "santo": "San Ambrosio, obispo y doctor de la Iglesia" },
{ "fecha": "2026-12-08", "santo": "Inmaculada Concepción de la Virgen María", "color": "blanco" },
{ "fecha": "2026-12-09", "santo": "San Juan Diego Cuauhtlatoatzin" },
{ "fecha": "2026-12-10", "santo": "San Efrén" },
{ "fecha": "2026-12-11", "santo": "San Dámaso I, Papa" },
{ "fecha": "2026-12-12", "santo": "Nuestra Señora de Guadalupe", "color": "blanco" },
{ "fecha": "2026-12-13", "santo": "Santa Lucía, virgen y mártir" },
{ "fecha": "2026-12-14", "santo": "San Juan de la Cruz, sacerdote y doctor de la Iglesia" },
{ "fecha": "2026-12-15", "santo": "Santa Ninfa" },
{ "fecha": "2026-12-16", "santo": "San Ezequiel Moreno" },
{ "fecha": "2026-12-17", "santo": "San Lázaro de Betania" },
{ "fecha": "2026-12-18", "santo": "San Gatiano" },
{ "fecha": "2026-12-19", "santo": "San Urbano I, Papa y mártir" },
{ "fecha": "2026-12-20", "santo": "San Dámaso I, Papa" },
{ "fecha": "2026-12-21", "santo": "San Pedro Canisio" },
{ "fecha": "2026-12-22", "santo": "San Francisco de Sales, obispo y doctor de la Iglesia" },
{ "fecha": "2026-12-23", "santo": "San Juan Kanty" },
{ "fecha": "2026-12-24", "santo": "Nochebuena" },
{ "fecha": "2026-12-25", "santo": "Navidad del Señor", "color": "blanco" },
{ "fecha": "2026-12-26", "santo": "San Esteban, primer mártir", "color": "rojo" },
{ "fecha": "2026-12-27", "santo": "San Juan, apóstol y evangelista", "color": "rojo" },
{ "fecha": "2026-12-28", "santo": "Inocentes, mártires", "color": "rojo" },
{ "fecha": "2026-12-29", "santo": "San Tomás Becket, obispo y mártir" },
{ "fecha": "2026-12-30", "santo": "Santos Adolfo y Juan Fisher, mártires" },
{ "fecha": "2026-12-31", "santo": "San Silvestre I, Papa" }
]

250
frontend/estilos.css Normal file
View File

@ -0,0 +1,250 @@
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,600;1,400&family=Nunito:wght@400;600&display=swap');
:root {
/* Paleta refinada */
--color-primario: #F5F5F5; /* Azul noche espiritual */
--color-hover: #1E3A5F; /* Azul profundo para hover */
--color-secundario: #A1A1A1; /* Azul muy suave */
--color-fondo: #0D0D0D; /* Fondo principal */
--color-texto: #E5E5E5; /* Gris claro para texto */
--blanco-puro: #FFFFFF;
--sombra: rgba(47, 58, 86, 0.15);
--color-tarjeta: #1A1A1A;
--color-acento: #5FAEDB;
--color-borde: rgba(95, 174, 219, 0.3);
}
body {
margin: 0;
font-family: 'Nunito', sans-serif;
background: linear-gradient(180deg, #0D0D0D 0%, #111827 100%);
color: var(--color-texto);
line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
header {
padding: 1.5rem 0;
text-align: center;
}
header h1 {
font-family: 'EB Garamond', serif;
letter-spacing: 1px;
color: var(--color-acento);
margin: 0;
}
nav ul {
list-style: none;
padding: 0;
margin: 0 auto 2rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
}
nav a {
color: var(--color-primario);
border-radius: 20px;
transition: all 0.3s ease;
}
nav a:hover {
background-color: var(--color-hover);
transform: translateY(-2px);
}
.container {
width: 92%;
max-width: 1200px;
margin: auto;
padding: 2rem 0;
display: grid;
gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.tarjeta {
background: rgba(20, 20, 20, 0.75);
backdrop-filter: blur(4px);
border-radius: 14px;
border: 1px solid var(--color-borde);
box-shadow: 0 10px 20px rgba(0,0,0,.35);
transition: all 0.3s ease;
overflow: hidden;
cursor: pointer;
}
.tarjeta:hover {
transform: translateY(-6px);
box-shadow: 0 16px 30px rgba(0,0,0,.5);
}
.tarjeta h2 {
margin: 0;
padding: 12px;
font-family: 'EB Garamond', serif;
font-size: 1.3rem;
text-align: center;
color: var(--color-acento);
background: rgba(13, 13, 13, 0.8);
border-bottom: 1px solid rgba(255,255,255,.1);
}
/* --- Párrafos generales de todas las tarjetas --- */
.tarjeta p {
text-align: center;
color: var(--color-texto);
font-size: 1rem;
margin: 2px 0; /* menos espacio entre líneas */
padding: 0; /* eliminar padding extra */
}
/* --- Ajuste para dispositivos pequeños --- */
@media (max-width: 600px) {
header h1 {
font-size: 1.6rem;
}
nav ul {
flex-direction: column;
align-items: center;
}
.container {
gap: 1.2rem;
}
}
footer {
text-align: center;
padding: 1.5rem 0;
color: var(--color-secundario);
margin-top: 3rem;
background: transparent;
}
/* --- Tarjeta Amanecer/Anochecer: más compacta --- */
#moon-mini-calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 6px;
margin-top: 10px;
text-align: center;
}
.moon-day {
padding: 4px;
border-radius: 6px;
background: var(--color-fondo);
font-size: 0.9rem;
}
.moon-today {
background: var(--color-hover);
font-weight: bold;
}
.moon-icon {
font-size: 1.4rem;
display: block;
}
#moon-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin-top: 10px;
margin-bottom: 6px;
text-align: center;
font-weight: 600;
color: var(--color-acento);
font-size: 0.85rem;
}
#moon-weekdays span {
padding: 4px 0;
border-bottom: 1px solid var(--color-borde);
}
/* ---------- BULLET JOURNAL SUMMARY ---------- */
#bullet-summary {
cursor: default;
}
#bullet-summary .bj-resumen {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
padding: 14px;
}
#bullet-summary .bj-item {
background: rgba(13,13,13,0.8);
border: 1px solid var(--color-borde);
border-radius: 10px;
padding: 10px;
text-align: center;
box-shadow: inset 0 0 12px rgba(0,0,0,.4);
}
#bullet-summary .bj-item span {
display: block;
font-size: 0.75rem;
color: var(--color-secundario);
letter-spacing: .5px;
}
#bullet-summary .bj-item strong {
font-size: 1.4rem;
font-family: 'EB Garamond', serif;
color: var(--color-acento);
}
/* Botón */
#bullet-summary .btn , #weather-card .btn {
display: block;
width: 75%;
margin: 12px auto 14px;
padding: 10px;
background: linear-gradient(135deg, #1E3A5F, #111827);
color: var(--blanco-puro);
border-radius: 12px;
text-align: center;
text-decoration: none;
font-size: 0.9rem;
border: 1px solid var(--color-borde);
transition: all .3s ease;
}
#bullet-summary .btn:hover, #weather-card .btn:hover {
background: linear-gradient(135deg, #2b4f80, #1E293B);
transform: translateY(-2px);
}
/* ICONOS */
.termometro {
width: 48px;
}
.termometro.neutro {
fill: var(--color-primario);
}
.termometro.frio {
fill: var(--color-acento);
}
.termometro.calor {
fill: #f44336;
}
.termometro.templado { fill: #4caf50; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

BIN
frontend/img/humedad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
frontend/img/lluvia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
frontend/img/sunrise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
frontend/img/termometro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,22 @@
<svg class="termometro" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true">
<!-- cuerpo -->
<path d="M32 2
a10 10 0 0 0-10 10v26.2
a16 16 0 1 0 20 0V12
a10 10 0 0 0-10-10z
m0 4
a6 6 0 0 1 6 6v28.1
l.9.6
a12 12 0 1 1-13.8 0l.9-.6V12
a6 6 0 0 1 6-6z"></path>
<!-- mercurio -->
<rect x="30" y="18" width="4" height="18" rx="2"></rect>
<circle cx="32" cy="46" r="6"></circle>
<!-- marcas -->
<rect x="46" y="14" width="12" height="4" rx="2"></rect>
<rect x="46" y="24" width="8" height="4" rx="2"></rect>
<rect x="46" y="34" width="12" height="4" rx="2"></rect>
<rect x="46" y="44" width="8" height="4" rx="2"></rect>
</svg>

After

Width:  |  Height:  |  Size: 765 B

BIN
frontend/img/viento.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

132
frontend/index.html Normal file
View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>El Tiempo | tatvil</title>
<link rel="icon" href="img/tatianalogo.png" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/estilos.css">
</head>
<body class="bg-black text-light">
<!-- Header -->
<header>
<nav class="navbar navbar-expand-sm navbar-dark shadow-sm border-bottom">
<div class="container-fluid">
<a class="navbar-brand font-weight-bold" href="https://tatianavilla.es" style="font-family: 'Consolas', monospace;">
<span class="text-primary">{</span>tatvil <span class="text-primary">}</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#menuNav"
aria-controls="menuNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menuNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" href="index.html">Estadísticas</a>
</li>
</ul>
<!-- Selector de ciudad -->
<div class="d-flex align-items-center gap-3">
<select id="city-select" class="form-select form-select-sm ciudad-select" novalidate>
<option value="Madrid">Madrid</option>
<option value="l'Alfàs del Pi">l'Alfàs del Pi</option>
<option value="l'Ampolla">l'Ampolla</option>
</select>
</div>
</div>
</div>
</nav>
</header>
<main class="container-fluid px-4 py-4">
<!-- Cabecera de sección -->
<div class="d-flex flex-wrap align-items-center justify-content-between mb-4 gap-3">
<div>
<h2 class="section-title mb-0">El Tiempo</h2>
<p class="text-muted mb-0 small" id="stats-location">Cargando datos…</p>
</div>
<!-- Navegación de mes -->
<div class="d-flex align-items-center gap-2 mes-navegacion">
<button id="prev-month" class="btn btn-sm mes-btn">&#8249;</button>
<span id="mes-nombre" class="mes-label">Enero</span>
<button id="next-month" class="btn btn-sm mes-btn">&#8250;</button>
</div>
</div>
<!-- Grid de tarjetas -->
<div class="row g-4">
<!-- ÚLTIMO DATO -->
<div class="col-md-6 col-lg-4">
<div class="card project-card h-100">
<div class="card-body">
<h5 class="card-title fw-bold">Hoy</h5>
<ul class="list-unstyled dato-list mb-0">
<li><span class="dato-label">Fecha</span> <strong id="last-date">--</strong></li>
<li><span class="dato-label">Temperatura</span> <strong id="last-temp">--</strong></li>
<li><span class="dato-label">Humedad</span> <strong id="last-humidity">--</strong></li>
<li><span class="dato-label">Lluvia</span> <strong id="last-rain">--</strong></li>
<li><span class="dato-label">Viento</span> <strong id="last-wind">--</strong></li>
<li><span class="dato-label">Amanecer</span> <strong id="last-sunrise">--</strong></li>
<li><span class="dato-label">Anochecer</span> <strong id="last-sunset">--</strong></li>
</ul>
<hr class="border-secondary my-3">
<h6 class="card-title fw-bold">Luna</h6>
<ul class="list-unstyled dato-list mb-0">
<li><span class="dato-label">Fase</span> <strong id="moon-phase">Calculando…</strong> <span id="moon-icon"></span></li>
</ul>
</div>
</div>
</div>
<!-- RESUMEN DEL MES -->
<div class="col-md-6 col-lg-4">
<div class="card project-card h-100">
<div class="card-body">
<h5 class="card-title fw-bold">Resumen del mes</h5>
<ul class="list-unstyled dato-list mb-0">
<li><span class="dato-label">Días registrados</span> <strong id="month-days">--</strong></li>
<li><span class="dato-label">Máxima absoluta</span> <strong id="month-max">--</strong></li>
<li><span class="dato-label">Mínima absoluta</span> <strong id="month-min">--</strong></li>
<li><span class="dato-label">Lluvia</span> <strong id="month-rain">--</strong></li>
<li><span class="dato-label">Humedad</span> <strong id="month-humidity">--</strong></li>
</ul>
</div>
</div>
</div>
<!-- TENDENCIA HISTÓRICA -->
<div class="col-md-12 col-lg-4">
<div class="card project-card h-100">
<div class="card-body">
<h5 class="card-title fw-bold">Tendencia histórica</h5>
<div id="trend-container">
<p class="text-muted small">Cargando tendencia…</p>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="py-4 text-center">
<div class="container">
<p class="mb-0 small text-muted">El Tiempo &copy; <span id="year"></span> &mdash; Tatiana Villa</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/estadisticas.js?v=2"></script>
</body>
</html>

135
frontend/index20260216.html Normal file
View File

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>El Tiempo</title>
<link rel="icon" href="img/eltiempo-logo.png" type="image/x-icon">
<link rel="stylesheet" href="css/estilos.css">
</head>
<body class="app-tiempo">
<header>
<h1>El Tiempo</h1>
<h2 id="location">Obteniendo ubicación...</h2>
<h3 id="fecha-actual">Fecha</h3>
<h3 id="santo-del-dia">Santo del dia</h3>
</header>
<main class="container">
<article class="tarjeta" id="sun-card">
<h2 id="sun-title">Amanecer y Anochecer</h2>
<p>Amanecer: <strong id="sunrise">--:--</strong></p>
<p>Anochecer: <strong id="sunset">--:--</strong></p>
<p>Duración del día: <strong id="day-length">--:--</strong></p>
<p id="countdown">Tiempo hasta anochecer: --:--:--</p>
<p id="sun-icon">☀️</p>
<hr>
<p>Fase lunar actual: <strong id="moon-phase">Calculando...</strong></p>
<p id="moon-icon">🌑</p>
</article>
<!--
<article class="tarjeta" id="moon-card">
<h2>Calendario lunar vs tiempo</h2>
<p>Fase lunar actual: <strong id="moon-phase">Calculando...</strong></p>
<p id="moon-icon">🌑</p>
<h3>Calendario lunar del mes</h3>
<div id="moon-weekdays">
<span>L</span>
<span>M</span>
<span>X</span>
<span>J</span>
<span>V</span>
<span>S</span>
<span>D</span>
</div>
<div id="moon-mini-calendar"></div>
</article>
-->
<article class="tarjeta" id="weather-card">
<h2>Tiempo Actual 🌤️</h2>
<p class="temp-linea">
<svg
class="termometro calor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 64 64"
width="24"
aria-hidden="true"
>
<path d="M32 2
a10 10 0 0 0-10 10v26.2
a16 16 0 1 0 20 0V12
a10 10 0 0 0-10-10z
m0 4
a6 6 0 0 1 6 6v28.1
l.9.6
a12 12 0 1 1-13.8 0l.9-.6V12
a6 6 0 0 1 6-6z"/>
<rect x="30" y="18" width="4" height="18" rx="2"/>
<circle cx="32" cy="46" r="6"/>
</svg>
<strong id="temperature">--°C</strong>
</p>
<p>Condición: <strong id="condition">--</strong></p>
<p>Humedad: <strong id="humidity">--%</strong></p>
<p>Viento: <strong id="wind">-- km/h</strong></p>
<a class="btn" href="estadisticas.html">Ver estadísticas</a>
</article>
<!--
<article class="tarjeta" id="bullet-summary">
<h2>Bullet Journal</h2>
<div class="bj-resumen">
<div class="bj-item">
<span>ROSARIO</span>
<strong id="bj-rosario">0</strong>
</div>
<div class="bj-item">
<span>CAMINAR</span>
<strong id="bj-caminar">0</strong>
</div>
<div class="bj-item">
<span>VITAMINAS</span>
<strong id="bj-vitaminas">0</strong>
</div>
<div class="bj-item">
<span>AGUA</span>
<strong id="bj-agua">0</strong>
</div>
</div>
<a class="btn" href="bullet-journal.html">Abrir Bullet Journal</a>
</article>
-->
</main>
<footer>
<p>El Tiempo &copy; <span id="year"></span> Tatiana Villa</p>
</footer>
<script src="js/codigo.js"></script>
</body>
</html>

73
frontend/index_mio.html Normal file
View File

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Estadísticas | El Tiempo</title>
<link rel="shortcut icon" href="/img/tatianalogo.png" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/estilos_mio.css">
</head>
<body class="app-tiempo">
<header>
<h1>Estadísticas del Tiempo</h1>
<select id="city-select">
<option value="Madrid">Madrid</option>
<option value="l'Alfàs del Pi">l'Alfàs del Pi</option>
<option value="l'Ampolla">l'Ampolla</option>
</select>
<h2 id="stats-location">Cargando datos…</h2>
<div id="mes-navegacion">
<button id="prev-month" class="mes-btn"></button>
<span id="mes-nombre">Enero</span>
<button id="next-month" class="mes-btn"></button>
</div>
</header>
<main class="container">
<!-- ÚLTIMO DATO -->
<article class="tarjeta">
<h2>Hoy</h2>
<p>Fecha: <strong id="last-date">--</strong></p>
<p>Temperatura: <strong id="last-temp">--</strong></p>
<p>Humedad: <strong id="last-humidity">--</strong></p>
<p>Lluvia: <strong id="last-rain">--</strong></p>
<p>Viento: <strong id="last-wind">--</strong></p>
<p>Amanecer: <strong id="last-sunrise">--</strong></p>
<p>Anochecer: <strong id="last-sunset">--</strong></p>
<h2>Luna</h2>
<p>Fase lunar actual: <strong id="moon-phase">Calculando...</strong></p>
<p id="moon-icon">🌑</p>
</article>
<!-- RESUMEN MES ACTUAL -->
<article class="tarjeta">
<h2>Resumen del mes actual</h2>
<p>Días registrados: <strong id="month-days">--</strong></p>
<p>Máxima absoluta: <strong id="month-max">--</strong></p>
<p>Mínima absoluta: <strong id="month-min">--</strong></p>
<p>Lluvia: <strong id="month-rain">--</strong></p>
<p>Humedad: <strong id="month-humidity">--</strong></p>
</article>
<!-- TENDENCIA HISTÓRICA -->
<article class="tarjeta">
<h2>Tendencia histórica del mes actual</h2>
<div id="trend-container">
<p>Cargando tendencia…</p>
</div>
</article>
</main>
<footer>
<p>El Tiempo &copy; <span id="year"></span> Tatiana Villa</p>
</footer>
<script src="js/estadisticas.js?v=2"></script>
</body>
</html>

View File

@ -0,0 +1,113 @@
let currentDate = new Date();
const fields = ['rosario', 'vitaminas', 'caminar', 'agua', 'mood'];
// Inicialización
document.addEventListener('DOMContentLoaded', () => {
setupAutoSave();
updateUI();
});
function formatDate(date) {
return date.toISOString().split('T')[0];
}
function updateUI() {
const dateString = currentDate.toLocaleDateString('es-ES', {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
});
document.getElementById('currentDateDisplay').innerText = dateString;
loadDayData();
updateExtras();
}
function changeDay(offset) {
currentDate.setDate(currentDate.getDate() + offset);
updateUI();
}
/* STORAGE */
function loadDayData() {
const data = JSON.parse(localStorage.getItem('journalData')) || {};
const day = data[formatDate(currentDate)] || {};
fields.forEach(f => {
const el = document.getElementById(f);
if (el.type === 'checkbox') el.checked = day[f] || false;
else el.value = day[f] || '';
});
}
function setupAutoSave() {
fields.forEach(f => {
document.getElementById(f).addEventListener('change', () => {
const data = JSON.parse(localStorage.getItem('journalData')) || {};
const key = formatDate(currentDate);
if (!data[key]) data[key] = {};
fields.forEach(field => {
const el = document.getElementById(field);
data[key][field] = el.type === 'checkbox' ? el.checked : el.value;
});
localStorage.setItem('journalData', JSON.stringify(data));
});
});
}
/* EXTRAS: SANTORAL Y CUMPLES */
async function updateExtras() {
const iso = formatDate(currentDate);
// Santoral
try {
const resS = await fetch('data/santos.json');
const santos = await resS.json();
const s = santos.find(x => x.fecha === iso);
document.getElementById('saintDisplay').innerText = s ? `Santo: ${s.santo}` : "Santoral";
document.getElementById('santodeldia').innerText = s ? `Santo del día: ${s.santo}` : "Santo del día desconocido";
} catch (e) { console.log("Falta santos.json"); }
// Cumples
try {
const resC = await fetch('data/cumples.json');
const cumples = await resC.json();
const c = cumples.find(x => x.fecha === iso.slice(5));
document.getElementById('cumpleDisplay').innerText = c ? `🎂 ${c.persona}` : "";
} catch (e) { }
}
/* IMPRESIÓN MENSUAL A5 */
async function printFullMonth() {
const data = JSON.parse(localStorage.getItem('journalData')) || {};
const month = currentDate.getMonth();
const year = currentDate.getFullYear();
const daysInMonth = new Date(year, month + 1, 0).getDate();
let html = "";
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const key = formatDate(date);
const dayData = data[key] || {};
html += `
<div class="page-a5">
<h1> ${date.toLocaleDateString('es-ES',{weekday:'long'})}, ${i} ${date.toLocaleDateString('es-ES',{month:'long'})} de ${date.getFullYear()}</h1>
<div id="santodeldia">Santo del dia</div>
<p style="margin-top:10px">
Rosario: ${dayData.rosario ? '✔' : '☐'} | Pasos: ${dayData.caminar || 0}
| Vitaminas: ${dayData.vitaminas ? '✔' : '☐'} | Agua: ${dayData.agua || 0}ml
</p>
<div class="dots-bg"></div>
</div>
`;
}
document.getElementById('printArea').innerHTML = html;
window.print();
}
function resetData() {
if(confirm("¿Seguro que quieres borrar todo el historial?")) {
localStorage.clear();
location.reload();
}
}

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

@ -0,0 +1,330 @@
// API URL absoluta
const API_URL = "https://aplicacionesdevanguardia.es/eltiempo/servidor/weather-hoy.php?ciudad=madrid";
/* ==============================================
FUNCIONES AUXILIARES
============================================== */
function formatTime(dateString) {
const date = new Date(dateString);
return date.toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" });
}
function formatDuration(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
function ponerlaFechaActual() {
const now = new Date();
const dateString = now.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
document.getElementById("fecha-actual").textContent = dateString;
}
/* ================================
6. SANTO DEL DÍA
================================ */
async function santoDelDia() {
const hoy = new Date();
const offset = hoy.getTimezoneOffset() * 60000;
const fechaISO = new Date(hoy - offset).toISOString().split('T')[0];
const santoDelDiaElem = document.getElementById("santo-del-dia");
try {
const res = await fetch('data/santos.json');
const listaSantos = await res.json();
const elSanto = listaSantos.find(d => d.fecha === fechaISO);
if (elSanto) {
document.getElementById("santo-del-dia").textContent = elSanto.santo;
santoDelDiaElem.textContent = elSanto.santo;
}
} catch (e) {
console.error("Error en la carga de santos:", e);
}
}
/* ==============================================
GEOLOCALIZACIÓN CENTRAL
============================================== */
async function getLocationOnce() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) reject("Geolocalización no soportada");
navigator.geolocation.getCurrentPosition(
pos => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
err => reject(err.message)
);
});
}
async function getLocationName(lat, lon) {
try {
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`;
const res = await fetch(url);
const data = await res.json();
if (data.address) {
if (data.address.city) return data.address.city + ", " + data.address.country;
if (data.address.town) return data.address.town + ", " + data.address.country;
if (data.address.village) return data.address.village + ", " + data.address.country;
// if (data.address.hamlet) return data.address.hamlet + ", " + data.address.country;
}
return "Ubicación desconocida";
} catch {
return "Ubicación desconocida";
}
}
/* ==============================================
TARJETA AMANECER / ANOCHECER
============================================== */
async function getSunTimes(lat, lon) {
const url = `https://api.sunrise-sunset.org/json?lat=${lat}&lng=${lon}&formatted=0`;
const response = await fetch(url);
const data = await response.json();
return data.results;
}
async function updateSunCard(lat, lon) {
try {
const sun = await getSunTimes(lat, lon);
const cityName = await getLocationName(lat, lon);
const sunrise = new Date(sun.sunrise);
const sunset = new Date(sun.sunset);
document.getElementById("sunrise").textContent = formatTime(sun.sunrise);
document.getElementById("sunset").textContent = formatTime(sun.sunset);
document.getElementById("day-length").textContent = formatDuration((sunset - sunrise)/1000);
document.getElementById("location").textContent = cityName;
const card = document.getElementById("sun-card");
const sunIcon = document.getElementById("sun-icon");
function updateMode() {
const now = new Date();
if (now >= sunrise && now <= sunset) {
card.style.background = "rgba(26,26,26,0.85)";
sunIcon.textContent = "☀️";
} else {
card.style.background = "rgba(10,10,30,0.85)";
sunIcon.textContent = "🌙";
}
}
function updateCountdown() {
const now = new Date();
let target;
let text = "Queda ";
if (now < sunrise) target = sunrise;
else if (now >= sunrise && now <= sunset) target = sunset;
else target = new Date(sunrise.getTime() + 24*60*60*1000);
const diff = Math.floor((target - now) / 1000);
const durationStr = formatDuration(diff);
text += (target.getTime() === sunrise.getTime())
? durationStr + " hasta anochecer"
: durationStr + " hasta amanecer";
document.getElementById("countdown").textContent = text;
}
updateMode();
updateCountdown();
setInterval(() => { updateMode(); updateCountdown(); }, 1000);
} catch (err) {
document.getElementById("location").textContent = "Error obteniendo datos: " + err.message;
}
}
/* ==============================================
TIEMPO ACTUAL
============================================== */
async function getWeather(position) {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
// Usamos 'current' para obtener datos en tiempo real
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,relative_humidity_2m,rain,precipitation,wind_speed_10m&timezone=auto`;
console.log("Fetching weather from:", url);
try {
const res = await fetch(url);
if (!res.ok) throw new Error("Error en la red");
const data = await res.json();
// La API devuelve los datos dentro de 'current'
const current = data.current;
// Inyectamos los datos en tu HTML asegurándonos de que los IDs coinciden
document.getElementById("temperature").textContent = `${current.temperature_2m}°C`;
document.getElementById("rain").textContent = `${current.rain} mm`;
document.getElementById("precipitation").textContent = `${current.precipitation} mm`;
document.getElementById("humidity").textContent = `${current.relative_humidity_2m}%`;
document.getElementById("wind").textContent = `${current.wind_speed_10m} km/h`;
} catch (err) {
console.error("Error en el tiempo:", err);
// Evitamos machacar el ID 'location' para no borrar el nombre de la ciudad
}
}
function showError(error) {
switch(error.code) {
case error.PERMISSION_DENIED:
document.getElementById("location").textContent = "Permiso denegado para obtener ubicación.";
break;
case error.POSITION_UNAVAILABLE:
document.getElementById("location").textContent = "Ubicación no disponible.";
break;
case error.TIMEOUT:
document.getElementById("location").textContent = "Tiempo de espera agotado.";
break;
default:
document.getElementById("location").textContent = "Error desconocido.";
}
}
/* ==============================================
FASES LUNARES
============================================== */
function getMoonPhase() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
const c = Math.floor(365.25 * year);
const e = Math.floor(30.6 * (month + 1));
const jd = c + e + day - 694039.09;
const phase = (jd / 29.53) % 1;
const age = phase * 29.53;
let phaseName = "", icon = "";
if (age < 1.84566) { phaseName = "Luna Nueva"; icon = "🌑"; }
else if (age < 5.53699) { phaseName = "Creciente Iluminante"; icon = "🌒"; }
else if (age < 9.22831) { phaseName = "Cuarto Creciente"; icon = "🌓"; }
else if (age < 12.91963) { phaseName = "Gibosa Creciente"; icon = "🌔"; }
else if (age < 16.61096) { phaseName = "Luna Llena"; icon = "🌕"; }
else if (age < 20.30228) { phaseName = "Gibosa Menguante"; icon = "🌖"; }
else if (age < 23.99361) { phaseName = "Cuarto Menguante"; icon = "🌗"; }
else if (age < 27.68493) { phaseName = "Creciente Menguante"; icon = "🌘"; }
else { phaseName = "Luna Nueva"; icon = "🌑"; }
document.getElementById("moon-phase").textContent = phaseName;
document.getElementById("moon-icon").textContent = icon;
}
function moonPhaseForDate(year, month, day) {
const c = Math.floor(365.25 * year);
const e = Math.floor(30.6 * (month + 1));
const jd = c + e + day - 694039.09;
const phase = (jd / 29.53) % 1;
const age = phase * 29.53;
if (age < 1.84566) return "🌑";
if (age < 5.53699) return "🌒";
if (age < 9.22831) return "🌓";
if (age < 12.91963) return "🌔";
if (age < 16.61096) return "🌕";
if (age < 20.30228) return "🌖";
if (age < 23.99361) return "🌗";
if (age < 27.68493) return "🌘";
return "🌑";
}
function generateMiniMoonCalendar() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const today = now.getDate();
const daysInMonth = new Date(year, month + 1, 0).getDate();
// const container = document.getElementById("moon-mini-calendar");
//container.innerHTML = ".";
let firstDay = new Date(year, month, 1).getDay();
firstDay = (firstDay === 0) ? 6 : firstDay - 1;
for (let i = 0; i < firstDay; i++) {
const empty = document.createElement("div");
empty.classList.add("moon-day");
empty.style.visibility = "hidden";
// container.appendChild(empty);
}
for (let day = 1; day <= daysInMonth; day++) {
const icon = moonPhaseForDate(year, month + 1, day);
const div = document.createElement("div");
div.classList.add("moon-day");
if (day === today) div.classList.add("moon-today");
div.innerHTML = `<span>${day}</span><span class="moon-icon">${icon}</span>`;
// container.appendChild(div);
}
}
/* ==============================================
BULLET JOURNAL RESUMEN
============================================== */
function loadBulletSummary() {
const data = JSON.parse(localStorage.getItem("journalData")) || {};
const now = new Date();
const month = now.getMonth();
const year = now.getFullYear();
let rosario = 0, caminar = 0, vitaminas = 0, agua = 0;
for (let key in data) {
const d = new Date(key);
if (d.getMonth() === month && d.getFullYear() === year) {
if(data[key].rosario) rosario++;
if(data[key].caminar) caminar++;
if(data[key].vitaminas) vitaminas++;
agua += Number(data[key].agua || 0);
}
}
document.getElementById("bj-rosario").textContent = rosario;
document.getElementById("bj-caminar").textContent = caminar;
document.getElementById("bj-vitaminas").textContent = vitaminas;
document.getElementById("bj-agua").textContent = agua;
}
/* ==============================================
INICIALIZACIÓN
============================================== */
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("year").innerText = new Date().getFullYear();
ponerlaFechaActual();
santoDelDia();
loadBulletSummary();
getMoonPhase();
// generateMiniMoonCalendar();
// Inicializar sol/luna + tiempo actual con una sola geolocalización
initApp();
});
async function initApp() {
try {
const { lat, lon } = await getLocationOnce();
updateSunCard(lat, lon);
getWeather({ coords: { latitude: lat, longitude: lon } });
} catch (err) {
document.getElementById("location").textContent = "Error: " + err;
}
}

282
frontend/js/estadisticas.js Normal file
View File

@ -0,0 +1,282 @@
// ====================
// Helper
// ====================
function $(id) { return document.getElementById(id); }
const monthNames = ["Enero","Febrero","Marzo","Abril","Mayo","Junio",
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
let selectedMonth = new Date().getMonth(); // mes actual
let ciudadActual = "Madrid"; // ciudad por defecto
// const BASE_API = "https://aplicacionesdevanguardia.es/eltiempo/servidor/api-weather-fechas.php";
const BASE_API = "https://tatvil.es/apis/api/weather/filter";
function buildApiUrl({ ciudad, desde, hasta }) {
const params = new URLSearchParams();
params.append("ciudad", ciudad);
params.append("desde", desde);
params.append("hasta", hasta);
console.log("Construyendo URL con parámetros:", { ciudad, desde, hasta });
console.log("URL API:", `${BASE_API}?${params.toString()}`);
return `${BASE_API}?${params.toString()}`;
}
// ====================
// Actualizar nombre de mes
// ====================
function updateMonthHeader() {
$("mes-nombre").textContent = monthNames[selectedMonth];
}
// ====================
// Cargar datos desde la API
// ====================
async function loadStats(options = {}) {
try {
const url = buildApiUrl({ ciudad: ciudadActual, ...options });
const response = await fetch(url);
if (!response.ok) throw new Error("Error cargando datos: " + response.status);
let data = await response.json(); // Cambia 'const' por 'let'
if (!data || !data.length) throw new Error("Datos vacíos");
// --- FILTRADO POR CIUDAD (Importante mientras Java no filtre) ---
data = data.filter(d => d.ciudad === ciudadActual);
// ----------------------------------------------------------------
renderLastData(data);
renderMonthStats(data);
renderTrend(data);
} catch (err) {
console.error(err);
$("stats-location").textContent = "Error cargando datos";
}
}
// ====================
// Último dato
// ====================
function renderLastData(data) {
const last = data[data.length - 1];
$("last-date").textContent = last.dia;
$("last-temp").textContent = last.temp_max + "°C / " + last.temp_min + "°C";
$("last-humidity").textContent = last.humedad + " %";
$("last-rain").textContent = last.lluvia + " mm";
$("last-wind").textContent = last.viento_velocidad + " km/h";
$("last-sunrise").textContent = last.amanecer;
$("last-sunset").textContent = last.anochecer;
$("stats-location").textContent = `Estadisticas de ${ciudadActual}`;
}
// ====================
// FASES LUNARES
// ====================
function getMoonPhase() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
const c = Math.floor(365.25 * year);
const e = Math.floor(30.6 * (month + 1));
const jd = c + e + day - 694039.09;
const phase = (jd / 29.53) % 1;
const age = phase * 29.53;
let phaseName = "", icon = "";
if (age < 1.84566) { phaseName = "Luna Nueva"; icon = "🌑"; }
else if (age < 5.53699) { phaseName = "Creciente Iluminante"; icon = "🌒"; }
else if (age < 9.22831) { phaseName = "Cuarto Creciente"; icon = "🌓"; }
else if (age < 12.91963) { phaseName = "Gibosa Creciente"; icon = "🌔"; }
else if (age < 16.61096) { phaseName = "Luna Llena"; icon = "🌕"; }
else if (age < 20.30228) { phaseName = "Gibosa Menguante"; icon = "🌖"; }
else if (age < 23.99361) { phaseName = "Cuarto Menguante"; icon = "🌗"; }
else if (age < 27.68493) { phaseName = "Creciente Menguante"; icon = "🌘"; }
else { phaseName = "Luna Nueva"; icon = "🌑"; }
$("moon-phase").textContent = phaseName;
$("moon-icon").textContent = icon;
const today = new Date();
$("stats-location").textContent += ` | Hoy ${today.getDate()} de ${monthNames[today.getMonth()]} de ${today.getFullYear()}`;
}
// ====================
// Estadísticas del mes seleccionado
// ====================
function renderMonthStats(data) {
// Esta es la clave: filtrar por el mes seleccionado antes de calcular
const monthData = data.filter(d => {
const fechaDato = new Date(d.dia);
return fechaDato.getMonth() === selectedMonth;
});
if (!monthData.length) {
// Si no hay datos, ponemos a cero para que no salgan cosas raras
$("month-days").textContent = 0;
return;
}
// Agrupar por día (el cron guarda varios registros por día)
const byDay = {};
monthData.forEach(d => {
const key = d.dia; // YYYY-MM-DD
if (!byDay[key]) byDay[key] = { maxTemps: [], minTemps: [], lluvia: [], humedad: [] };
byDay[key].maxTemps.push(d.temp_max);
byDay[key].minTemps.push(d.temp_min);
byDay[key].lluvia.push(parseFloat(d.lluvia));
byDay[key].humedad.push(parseFloat(d.humedad));
});
const days = Object.values(byDay);
const maxTemps = days.map(d => Math.max(...d.maxTemps));
const minTemps = days.map(d => Math.min(...d.minTemps));
// La lluvia del día es el máximo registrado ese día (acumulado), no la suma de todas las lecturas
const lluviaPorDia = days.map(d => Math.max(...d.lluvia));
const lluviaTotal = lluviaPorDia.reduce((sum, v) => sum + v, 0);
const lluviaMedia = lluviaTotal / days.length;
const humedad = (days.reduce((sum, d) => sum + d.humedad.reduce((a, b) => a + b, 0) / d.humedad.length, 0) / days.length).toFixed(1);
$("month-days").textContent = days.length;
$("month-max").textContent = Math.max(...maxTemps) + "°C";
$("month-min").textContent = Math.min(...minTemps) + "°C";
$("month-rain").textContent = lluviaTotal.toFixed(1) + " mm (total) / " + lluviaMedia.toFixed(1) + " mm (media diaria)";
$("month-humidity").textContent = humedad + " % (media diaria)";
}
// ====================
// Tendencia histórica
// ====================
function renderTrend(data) {
const byYear = {};
console.log("Datos recibidos para tendencia histórica:", data);
// Agrupar datos por año+día para el mes seleccionado (el cron guarda varios registros por día)
const byYearDay = {};
data.forEach(d => {
const date = new Date(d.dia);
if (date.getMonth() === selectedMonth) {
const year = date.getFullYear();
const dayKey = d.dia; // YYYY-MM-DD
if (!byYearDay[year]) byYearDay[year] = {};
if (!byYearDay[year][dayKey]) byYearDay[year][dayKey] = { max: [], min: [], rain: [] };
byYearDay[year][dayKey].max.push(d.temp_max);
byYearDay[year][dayKey].min.push(d.temp_min);
byYearDay[year][dayKey].rain.push(parseFloat(d.lluvia));
}
});
// Reducir por día antes de agrupar por año
Object.keys(byYearDay).forEach(year => {
byYear[year] = { max: [], min: [], rain: [] };
Object.values(byYearDay[year]).forEach(day => {
byYear[year].max.push(Math.max(...day.max));
byYear[year].min.push(Math.min(...day.min));
byYear[year].rain.push(Math.max(...day.rain));
});
});
const container = $("trend-container");
console.log("Datos agrupados por año para tendencia:", byYear);
if (Object.keys(byYear).length === 0) {
container.innerHTML = "<p>No hay datos históricos para este mes.</p>";
return;
}
let html = "<table class=\"trend-table\"><thead><tr><th>Año</th><th>Máx</th><th>Mín</th><th>Lluvia</th></tr></thead><tbody>";
Object.keys(byYear).sort((a, b) => b - a).forEach(year => {
const maxAvg = (byYear[year].max.reduce((a,b)=>a+b,0)/byYear[year].max.length).toFixed(1);
const minAvg = (byYear[year].min.reduce((a,b)=>a+b,0)/byYear[year].min.length).toFixed(1);
const rainTotal = byYear[year].rain.reduce((a,b)=>a+b,0).toFixed(1);
html += `
<tr>
<td><strong>${year}</strong></td>
<td>${maxAvg}°C</td>
<td>${minAvg}°C</td>
<td>${rainTotal} mm</td>
</tr>
`;
});
html += "</tbody></table>";
container.innerHTML = html;
}
// ====================
// Cargar mes actual según selectedMonth
// ====================
function loadCurrentMonth() {
const ahora = new Date();
const yearActual = ahora.getFullYear();
const mesActual = ahora.getMonth();
const diaActual = ahora.getDate();
// El mes que queremos consultar
const yearBusqueda = yearActual;
const monthBusqueda = selectedMonth + 1;
// 1. Primer día del mes (Siempre el 01)
const firstDay = `${yearBusqueda}-${String(monthBusqueda).padStart(2, "0")}-01`;
// 2. Calculamos el último día teórico del mes
let ultimoDiaObj = new Date(yearBusqueda, monthBusqueda, 0);
// 3. VALIDACIÓN CRUCIAL: Si el mes seleccionado es el actual,
// limitamos la búsqueda hasta HOY para evitar el Error 500 del servidor.
if (selectedMonth === mesActual && yearBusqueda === yearActual) {
ultimoDiaObj = ahora;
}
// 4. Formateo manual YYYY-MM-DD (Evita toISOString y sus desfases UTC)
const y = ultimoDiaObj.getFullYear();
const m = String(ultimoDiaObj.getMonth() + 1).padStart(2, "0");
const d = String(ultimoDiaObj.getDate()).padStart(2, "0");
const lastDay = `${y}-${m}-${d}`;
console.log(`Petición: ${firstDay} hasta ${lastDay}`);
loadStats({ desde: firstDay, hasta: lastDay });
}
// ====================
// Inicialización
// ====================
document.addEventListener("DOMContentLoaded", () => {
updateMonthHeader();
getMoonPhase();
loadCurrentMonth();
$("prev-month").addEventListener("click", () => {
selectedMonth = (selectedMonth + 11) % 12;
updateMonthHeader();
loadCurrentMonth();
});
$("next-month").addEventListener("click", () => {
selectedMonth = (selectedMonth + 1) % 12;
updateMonthHeader();
loadCurrentMonth();
});
$("year").textContent = new Date().getFullYear();
// ====================
// Selector de ciudad
// ====================
$("city-select").addEventListener("change", (e) => {
ciudadActual = e.target.value;
loadCurrentMonth();
});
});

View File

@ -0,0 +1,148 @@
const API_URL = "http://aplicacionesdevanguardia.es/eltiempo/apis/api-weather-reverse.php?ciudad=madrid";
/* ---------- UTILIDADES ---------- */
function $(id) {
return document.getElementById(id);
}
function monthName(monthIndex) {
return new Date(2026, monthIndex, 1)
.toLocaleDateString("es-ES", { month: "long" });
}
/* ---------- CARGA PRINCIPAL ---------- */
async function loadStats() {
try {
const res = await fetch(API_URL);
const data = await res.json();
if (!data || !data.length) throw "Sin datos";
renderLastDay(data[0]);
renderMonthStats(data);
renderTrend(data);
$("stats-location").textContent = "Madrid (datos históricos)";
} catch (e) {
$("stats-location").textContent = "Error cargando estadísticas";
console.error(e);
}
}
/* ---------- ÚLTIMO DÍA ---------- */
function renderLastDay(day) {
$("last-date").textContent = day.dia;
$("last-temp").textContent = `${day.temp_min}°C / ${day.temp_max}°C`;
$("last-humidity").textContent = `${Math.round(day.humedad)} %`;
$("last-rain").textContent = `${day.lluvia} mm`;
$("last-wind").textContent = `${Math.round(day.viento_velocidad)} km/h`;
$("last-sunrise").textContent = day.amanecer;
$("last-sunset").textContent = day.anochecer;
}
/* ---------- RESUMEN DEL MES ---------- */
function renderMonthStats(data) {
const now = new Date();
const month = now.getMonth();
const year = now.getFullYear();
const monthData = data.filter(d => {
const date = new Date(d.dia);
return date.getMonth() === month && date.getFullYear() === year;
});
if (!monthData.length) return;
const maxTemps = monthData.map(d => d.temp_max);
const minTemps = monthData.map(d => d.temp_min);
const lluvia = monthData.reduce(
(sum, d) => sum + parseFloat(d.lluvia), 0
);
const humedad = (
monthData.reduce((sum, d) => sum + parseFloat(d.humedad), 0)
/ monthData.length
).toFixed(1);
$("month-days").textContent = monthData.length;
$("month-max").textContent = Math.max(...maxTemps) + "°C";
$("month-min").textContent = Math.min(...minTemps) + "°C";
$("month-rain").textContent = lluvia.toFixed(1) + " mm";
$("month-humidity").textContent = humedad + " %";
}
/* ---------- TENDENCIA HISTÓRICA ---------- */
function renderTrend(data) {
const now = new Date();
const month = now.getMonth();
const byYear = {};
data.forEach(d => {
const date = new Date(d.dia);
if (date.getMonth() === month) {
const year = date.getFullYear();
if (!byYear[year]) {
byYear[year] = { max: [], min: [], rain: [] };
}
byYear[year].max.push(d.temp_max);
byYear[year].min.push(d.temp_min);
byYear[year].rain.push(parseFloat(d.lluvia));
}
});
const container = $("trend-container");
container.innerHTML = "";
Object.keys(byYear)
.sort()
.forEach(year => {
const maxAvg = (
byYear[year].max.reduce((a,b)=>a+b,0) /
byYear[year].max.length
).toFixed(1);
const minAvg = (
byYear[year].min.reduce((a,b)=>a+b,0) /
byYear[year].min.length
).toFixed(1);
const rainTotal = (
byYear[year].rain.reduce((a,b)=>a+b,0)
).toFixed(1);
container.innerHTML += `
<p>
<strong>${year}</strong>
Máx ${maxAvg}°C ·
Mín ${minAvg}°C ·
Lluvia ${rainTotal} mm
</p>
`;
});
}
/* ---------- INIT ---------- */
document.addEventListener("DOMContentLoaded", () => {
$("year").textContent = new Date().getFullYear();
loadStats();
});

View File

@ -0,0 +1,251 @@
// ====================
// Helper
// ====================
function $(id) { return document.getElementById(id); }
const monthNames = ["Enero","Febrero","Marzo","Abril","Mayo","Junio",
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
let selectedMonth = new Date().getMonth(); // mes actual
let ciudadActual = "Madrid"; // ciudad por defecto
const BASE_API = "https://aplicacionesdevanguardia.es/eltiempo/servidor/api-weather-fechas.php";
// ====================
// Construir URL de API según filtros
// ====================
function buildApiUrl({ ciudad, fecha = null, desde = null, hasta = null }) {
const params = new URLSearchParams();
params.append("ciudad", ciudad);
if (fecha) params.append("fecha", fecha);
if (desde) params.append("desde", desde);
if (hasta) params.append("hasta", hasta);
console.log("24 - Construyendo URL con parámetros:", { ciudad, fecha, desde, hasta });
console.log("URL API:", `${BASE_API}?${params.toString()}`);
return `${BASE_API}?${params.toString()}`;
}
// ====================
// Actualizar nombre de mes
// ====================
function updateMonthHeader() {
$("mes-nombre").textContent = monthNames[selectedMonth];
}
// ====================
// Cargar datos desde la API
// ====================
async function loadStats(options = {}) {
try {
const url = buildApiUrl({
ciudad: ciudadActual,
...options
});
const response = await fetch(url);
if (!response.ok) throw new Error("Error cargando datos: " + response.status);
const data = await response.json();
if (!data || !data.length) throw new Error("Datos vacíos");
renderLastData(data);
renderMonthStats(data);
renderTrend(data);
} catch (err) {
console.error(err);
$("stats-location").textContent = "Error cargando datos";
}
}
// ====================
// Último dato
// ====================
function renderLastData(data) {
const last = data[data.length - 1];
$("last-date").textContent = last.dia;
$("last-temp").textContent = last.temp_max + "°C / " + last.temp_min + "°C";
$("last-humidity").textContent = last.humedad + " %";
$("last-rain").textContent = last.lluvia + " mm";
$("last-wind").textContent = last.viento_velocidad + " km/h";
$("last-sunrise").textContent = last.amanecer;
$("last-sunset").textContent = last.anochecer;
$("stats-location").textContent = `Estadisticas de ${ciudadActual}`;
}
// ====================
// FASES LUNARES
// ====================
function getMoonPhase() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
const c = Math.floor(365.25 * year);
const e = Math.floor(30.6 * (month + 1));
const jd = c + e + day - 694039.09;
const phase = (jd / 29.53) % 1;
const age = phase * 29.53;
let phaseName = "", icon = "";
if (age < 1.84566) { phaseName = "Luna Nueva"; icon = "🌑"; }
else if (age < 5.53699) { phaseName = "Creciente Iluminante"; icon = "🌒"; }
else if (age < 9.22831) { phaseName = "Cuarto Creciente"; icon = "🌓"; }
else if (age < 12.91963) { phaseName = "Gibosa Creciente"; icon = "🌔"; }
else if (age < 16.61096) { phaseName = "Luna Llena"; icon = "🌕"; }
else if (age < 20.30228) { phaseName = "Gibosa Menguante"; icon = "🌖"; }
else if (age < 23.99361) { phaseName = "Cuarto Menguante"; icon = "🌗"; }
else if (age < 27.68493) { phaseName = "Creciente Menguante"; icon = "🌘"; }
else { phaseName = "Luna Nueva"; icon = "🌑"; }
$("moon-phase").textContent = phaseName;
$("moon-icon").textContent = icon;
const today = new Date();
$("stats-location").textContent += ` | Hoy ${today.getDate()} de ${monthNames[today.getMonth()]} de ${today.getFullYear()}`;
}
// ====================
// Estadísticas del mes seleccionado
// ====================
function renderMonthStats(data) {
const monthData = data.filter(d => new Date(d.dia).getMonth() === selectedMonth);
if (!monthData.length) return;
const maxTemps = monthData.map(d => d.temp_max);
const minTemps = monthData.map(d => d.temp_min);
const lluvia = monthData.reduce((sum, d) => sum + parseFloat(d.lluvia), 0);
const humedad = (monthData.reduce((sum, d) => sum + parseFloat(d.humedad), 0) / monthData.length).toFixed(1);
const lluviamedia = lluvia/monthData.length; // media de lluvia diaria
$("month-days").textContent = monthData.length;
$("month-max").textContent = Math.max(...maxTemps) + "°C";
$("month-min").textContent = Math.min(...minTemps) + "°C";
$("month-rain").textContent = lluvia.toFixed(1) + " mm (total) / " + lluviamedia.toFixed(1) + " mm (media diaria)";
$("month-humidity").textContent = humedad + " % (media diaria)";
}
// ====================
// Tendencia histórica
// ====================
function renderTrend(data) {
const byYear = {};
console.log("Datos recibidos para tendencia histórica:", data);
// Agrupar datos por año para el mes seleccionado
data.forEach(d => {
const date = new Date(d.dia);
if (date.getMonth() === selectedMonth) {
const year = date.getFullYear();
if (!byYear[year]) byYear[year] = { max: [], min: [], rain: [] };
byYear[year].max.push(d.temp_max);
byYear[year].min.push(d.temp_min);
byYear[year].rain.push(parseFloat(d.lluvia));
}
});
const container = $("trend-container");
console.log("Datos agrupados por año para tendencia:", byYear);
if (Object.keys(byYear).length === 0) {
container.innerHTML = "<p>No hay datos históricos para este mes.</p>";
return;
}
let html = "<table class=\"trend-table\"><thead><tr><th>Año</th><th>Máx</th><th>Mín</th><th>Lluvia</th></tr></thead><tbody>";
Object.keys(byYear).sort((a, b) => b - a).forEach(year => {
const maxAvg = (byYear[year].max.reduce((a,b)=>a+b,0)/byYear[year].max.length).toFixed(1);
const minAvg = (byYear[year].min.reduce((a,b)=>a+b,0)/byYear[year].min.length).toFixed(1);
const rainTotal = byYear[year].rain.reduce((a,b)=>a+b,0).toFixed(1);
html += `
<tr>
<td><strong>${year}</strong></td>
<td>${maxAvg}°C</td>
<td>${minAvg}°C</td>
<td>${rainTotal} mm</td>
</tr>
`;
});
html += "</tbody></table>";
container.innerHTML = html;
}
// ====================
// Cargar mes actual según selectedMonth
// ====================
function loadCurrentMonth() {
const ahora = new Date();
const yearActual = ahora.getFullYear();
const mesActual = ahora.getMonth();
const diaActual = ahora.getDate();
// El mes que queremos consultar
const yearBusqueda = yearActual;
const monthBusqueda = selectedMonth + 1;
// 1. Primer día del mes (Siempre el 01)
const firstDay = `${yearBusqueda}-${String(monthBusqueda).padStart(2, "0")}-01`;
// 2. Calculamos el último día teórico del mes
let ultimoDiaObj = new Date(yearBusqueda, monthBusqueda, 0);
// 3. VALIDACIÓN CRUCIAL: Si el mes seleccionado es el actual,
// limitamos la búsqueda hasta HOY para evitar el Error 500 del servidor.
if (selectedMonth === mesActual && yearBusqueda === yearActual) {
ultimoDiaObj = ahora;
}
// 4. Formateo manual YYYY-MM-DD (Evita toISOString y sus desfases UTC)
const y = ultimoDiaObj.getFullYear();
const m = String(ultimoDiaObj.getMonth() + 1).padStart(2, "0");
const d = String(ultimoDiaObj.getDate()).padStart(2, "0");
const lastDay = `${y}-${m}-${d}`;
console.log(`Petición: ${firstDay} hasta ${lastDay}`);
loadStats({ desde: firstDay, hasta: lastDay });
}
// ====================
// Inicialización
// ====================
document.addEventListener("DOMContentLoaded", () => {
updateMonthHeader();
getMoonPhase();
loadCurrentMonth();
$("prev-month").addEventListener("click", () => {
selectedMonth = (selectedMonth + 11) % 12;
updateMonthHeader();
loadCurrentMonth();
});
$("next-month").addEventListener("click", () => {
selectedMonth = (selectedMonth + 1) % 12;
updateMonthHeader();
loadCurrentMonth();
});
$("year").textContent = new Date().getFullYear();
// ====================
// Selector de ciudad
// ====================
$("city-select").addEventListener("change", (e) => {
ciudadActual = e.target.value;
loadCurrentMonth();
});
});

View File

@ -0,0 +1,255 @@
// ====================
// Helper
// ====================
function $(id) { return document.getElementById(id); }
const monthNames = ["Enero","Febrero","Marzo","Abril","Mayo","Junio",
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
let selectedMonth = new Date().getMonth(); // mes actual
let ciudadActual = "Madrid"; // ciudad por defecto
// const BASE_API = "https://aplicacionesdevanguardia.es/eltiempo/servidor/api-weather-fechas.php";
const BASE_API = "https://tatvil.es/apis/api/weather/filter";
function buildApiUrl({ ciudad, desde, hasta }) {
const params = new URLSearchParams();
params.append("ciudad", ciudad);
params.append("desde", desde);
params.append("hasta", hasta);
console.log("Construyendo URL con parámetros:", { ciudad, desde, hasta });
console.log("URL API:", `${BASE_API}?${params.toString()}`);
return `${BASE_API}?${params.toString()}`;
}
// ====================
// Actualizar nombre de mes
// ====================
function updateMonthHeader() {
$("mes-nombre").textContent = monthNames[selectedMonth];
}
// ====================
// Cargar datos desde la API
// ====================
async function loadStats(options = {}) {
try {
const url = buildApiUrl({ ciudad: ciudadActual, ...options });
const response = await fetch(url);
if (!response.ok) throw new Error("Error cargando datos: " + response.status);
let data = await response.json(); // Cambia 'const' por 'let'
if (!data || !data.length) throw new Error("Datos vacíos");
// --- FILTRADO POR CIUDAD (Importante mientras Java no filtre) ---
data = data.filter(d => d.ciudad === ciudadActual);
// ----------------------------------------------------------------
renderLastData(data);
renderMonthStats(data);
renderTrend(data);
} catch (err) {
console.error(err);
$("stats-location").textContent = "Error cargando datos";
}
}
// ====================
// Último dato
// ====================
function renderLastData(data) {
const last = data[data.length - 1];
$("last-date").textContent = last.dia;
$("last-temp").textContent = last.temp_max + "°C / " + last.temp_min + "°C";
$("last-humidity").textContent = last.humedad + " %";
$("last-rain").textContent = last.lluvia + " mm";
$("last-wind").textContent = last.viento_velocidad + " km/h";
$("last-sunrise").textContent = last.amanecer;
$("last-sunset").textContent = last.anochecer;
$("stats-location").textContent = `Estadisticas de ${ciudadActual}`;
}
// ====================
// FASES LUNARES
// ====================
function getMoonPhase() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
const c = Math.floor(365.25 * year);
const e = Math.floor(30.6 * (month + 1));
const jd = c + e + day - 694039.09;
const phase = (jd / 29.53) % 1;
const age = phase * 29.53;
let phaseName = "", icon = "";
if (age < 1.84566) { phaseName = "Luna Nueva"; icon = "🌑"; }
else if (age < 5.53699) { phaseName = "Creciente Iluminante"; icon = "🌒"; }
else if (age < 9.22831) { phaseName = "Cuarto Creciente"; icon = "🌓"; }
else if (age < 12.91963) { phaseName = "Gibosa Creciente"; icon = "🌔"; }
else if (age < 16.61096) { phaseName = "Luna Llena"; icon = "🌕"; }
else if (age < 20.30228) { phaseName = "Gibosa Menguante"; icon = "🌖"; }
else if (age < 23.99361) { phaseName = "Cuarto Menguante"; icon = "🌗"; }
else if (age < 27.68493) { phaseName = "Creciente Menguante"; icon = "🌘"; }
else { phaseName = "Luna Nueva"; icon = "🌑"; }
$("moon-phase").textContent = phaseName;
$("moon-icon").textContent = icon;
const today = new Date();
$("stats-location").textContent += ` | Hoy ${today.getDate()} de ${monthNames[today.getMonth()]} de ${today.getFullYear()}`;
}
// ====================
// Estadísticas del mes seleccionado
// ====================
function renderMonthStats(data) {
// Esta es la clave: filtrar por el mes seleccionado antes de calcular
const monthData = data.filter(d => {
const fechaDato = new Date(d.dia);
return fechaDato.getMonth() === selectedMonth;
});
if (!monthData.length) {
// Si no hay datos, ponemos a cero para que no salgan cosas raras
$("month-days").textContent = 0;
return;
}
const maxTemps = monthData.map(d => d.temp_max);
const minTemps = monthData.map(d => d.temp_min);
const lluvia = monthData.reduce((sum, d) => sum + parseFloat(d.lluvia), 0);
const humedad = (monthData.reduce((sum, d) => sum + parseFloat(d.humedad), 0) / monthData.length).toFixed(1);
const lluviamedia = lluvia/monthData.length; // media de lluvia diaria
$("month-days").textContent = monthData.length;
$("month-max").textContent = Math.max(...maxTemps) + "°C";
$("month-min").textContent = Math.min(...minTemps) + "°C";
$("month-rain").textContent = lluvia.toFixed(1) + " mm (total) / " + lluviamedia.toFixed(1) + " mm (media diaria)";
$("month-humidity").textContent = humedad + " % (media diaria)";
}
// ====================
// Tendencia histórica
// ====================
function renderTrend(data) {
const byYear = {};
console.log("Datos recibidos para tendencia histórica:", data);
// Agrupar datos por año para el mes seleccionado
data.forEach(d => {
const date = new Date(d.dia);
if (date.getMonth() === selectedMonth) {
const year = date.getFullYear();
if (!byYear[year]) byYear[year] = { max: [], min: [], rain: [] };
byYear[year].max.push(d.temp_max);
byYear[year].min.push(d.temp_min);
byYear[year].rain.push(parseFloat(d.lluvia));
}
});
const container = $("trend-container");
console.log("Datos agrupados por año para tendencia:", byYear);
if (Object.keys(byYear).length === 0) {
container.innerHTML = "<p>No hay datos históricos para este mes.</p>";
return;
}
let html = "<table class=\"trend-table\"><thead><tr><th>Año</th><th>Máx</th><th>Mín</th><th>Lluvia</th></tr></thead><tbody>";
Object.keys(byYear).sort((a, b) => b - a).forEach(year => {
const maxAvg = (byYear[year].max.reduce((a,b)=>a+b,0)/byYear[year].max.length).toFixed(1);
const minAvg = (byYear[year].min.reduce((a,b)=>a+b,0)/byYear[year].min.length).toFixed(1);
const rainTotal = byYear[year].rain.reduce((a,b)=>a+b,0).toFixed(1);
html += `
<tr>
<td><strong>${year}</strong></td>
<td>${maxAvg}°C</td>
<td>${minAvg}°C</td>
<td>${rainTotal} mm</td>
</tr>
`;
});
html += "</tbody></table>";
container.innerHTML = html;
}
// ====================
// Cargar mes actual según selectedMonth
// ====================
function loadCurrentMonth() {
const ahora = new Date();
const yearActual = ahora.getFullYear();
const mesActual = ahora.getMonth();
const diaActual = ahora.getDate();
// El mes que queremos consultar
const yearBusqueda = yearActual;
const monthBusqueda = selectedMonth + 1;
// 1. Primer día del mes (Siempre el 01)
const firstDay = `${yearBusqueda}-${String(monthBusqueda).padStart(2, "0")}-01`;
// 2. Calculamos el último día teórico del mes
let ultimoDiaObj = new Date(yearBusqueda, monthBusqueda, 0);
// 3. VALIDACIÓN CRUCIAL: Si el mes seleccionado es el actual,
// limitamos la búsqueda hasta HOY para evitar el Error 500 del servidor.
if (selectedMonth === mesActual && yearBusqueda === yearActual) {
ultimoDiaObj = ahora;
}
// 4. Formateo manual YYYY-MM-DD (Evita toISOString y sus desfases UTC)
const y = ultimoDiaObj.getFullYear();
const m = String(ultimoDiaObj.getMonth() + 1).padStart(2, "0");
const d = String(ultimoDiaObj.getDate()).padStart(2, "0");
const lastDay = `${y}-${m}-${d}`;
console.log(`Petición: ${firstDay} hasta ${lastDay}`);
loadStats({ desde: firstDay, hasta: lastDay });
}
// ====================
// Inicialización
// ====================
document.addEventListener("DOMContentLoaded", () => {
updateMonthHeader();
getMoonPhase();
loadCurrentMonth();
$("prev-month").addEventListener("click", () => {
selectedMonth = (selectedMonth + 11) % 12;
updateMonthHeader();
loadCurrentMonth();
});
$("next-month").addEventListener("click", () => {
selectedMonth = (selectedMonth + 1) % 12;
updateMonthHeader();
loadCurrentMonth();
});
$("year").textContent = new Date().getFullYear();
// ====================
// Selector de ciudad
// ====================
$("city-select").addEventListener("change", (e) => {
ciudadActual = e.target.value;
loadCurrentMonth();
});
});