741 lines
36 KiB
PHP
741 lines
36 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>@yield('title', 'OnAPB')</title>
|
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('favicon-32.png') }}">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('favicon-16.png') }}">
|
|
{{-- PWA --}}
|
|
<link rel="manifest" href="/manifest.json?v=4">
|
|
<meta name="theme-color" content="#b00000">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="apple-mobile-web-app-title" content="OnAPB">
|
|
<link rel="apple-touch-icon" href="/icons/icon-192.png?v=4">
|
|
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152.png?v=4">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192.png?v=4">
|
|
<link rel="apple-touch-icon" sizes="167x167" href="/icons/icon-192.png?v=4">
|
|
|
|
{{-- Preload del logo del navbar (LCP-critical) --}}
|
|
<link rel="preload" as="image" href="{{ asset('logo.webp') }}" type="image/webp" fetchpriority="high">
|
|
|
|
{{-- CSS critico (bloqueante, requerido para el layout/navbar) --}}
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
{{-- Tipografía: Antonio (display) + DM Sans (body) — cargadas en kinetic-arena.css --}}
|
|
<link rel="stylesheet" href="{{ asset('static/base.css?v=5') }}">
|
|
<link rel="stylesheet" href="{{ asset('static/kinetic-arena.css?v=5') }}">
|
|
<link rel="stylesheet" href="{{ asset('static/kinetic-arena-v3.min.css') }}?v={{ @filemtime(public_path('static/kinetic-arena-v3.min.css')) ?: '3' }}">
|
|
|
|
{{-- CSS no critico: cargar async via preload-swap (no bloquea el primer paint) --}}
|
|
<link rel="preload" as="style" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" onload="this.onload=null;this.rel='stylesheet'">
|
|
<link rel="preload" as="style" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css" onload="this.onload=null;this.rel='stylesheet'">
|
|
<noscript>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
|
</noscript>
|
|
|
|
@yield('styles')
|
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
|
<style>
|
|
.password-toggle {
|
|
cursor: pointer;
|
|
z-index: 10;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Splash Screen (X Style) -->
|
|
<div id="splash-screen">
|
|
<img src="/icons/icon-192.png?v=4" alt="OnAPB" id="splash-logo">
|
|
</div>
|
|
<script>
|
|
// Ocultar el splash INMEDIATAMENTE si ya se mostró en esta sesión.
|
|
// Este script es síncrono: corre antes de que el navegador pinte nada.
|
|
if (sessionStorage.getItem('splashShown')) {
|
|
document.getElementById('splash-screen').style.display = 'none';
|
|
}
|
|
</script>
|
|
|
|
<!-- Navbar -->
|
|
<nav class="navbar navbar-expand-lg custom-navbar no-line">
|
|
<div class="container">
|
|
<a class="navbar-brand" href="{{ route('home') }}" data-magnetic data-magnetic-strength="0.3">
|
|
<picture>
|
|
<source srcset="{{ asset('logo.webp') }}" type="image/webp">
|
|
<img src="{{ asset('logo.png') }}" alt="OnAPB" class="navbar-logo" width="80" height="80" fetchpriority="high" decoding="async">
|
|
</picture>
|
|
</a>
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="navbarNav">
|
|
<ul class="navbar-nav ms-auto">
|
|
<li class="nav-item"><a class="nav-link {{ request()->routeIs('home') ? 'active' : '' }}" href="{{ route('home') }}">Inicio</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ request()->routeIs('eventos.*') ? 'active' : '' }}" href="{{ route('eventos.index') }}">Partidos</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ request()->routeIs('asociate') ? 'active' : '' }}" href="{{ route('asociate') }}">Unite</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ request()->routeIs('promos.*') ? 'active' : '' }}" href="{{ route('promos.index') }}">Lugares</a></li>
|
|
<li class="nav-item"><a class="nav-link {{ request()->routeIs('noticias.*') ? 'active' : '' }}" href="{{ route('noticias.index') }}">Noticias</a></li>
|
|
<li class="nav-item"><a class="nav-link fw-bold text-primary {{ request()->routeIs('documentacion.*') ? 'active' : '' }}" href="{{ route('documentacion.index') }}"><i class="bi bi-question-circle-fill me-1"></i> Ayuda</a></li>
|
|
|
|
@if(isset($navTorneos) && $navTorneos->count() > 0)
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
|
Torneos
|
|
</a>
|
|
<ul class="dropdown-menu">
|
|
@foreach($navTorneos as $nt)
|
|
<li class="dropdown-header text-uppercase small fw-bold">{{ $nt->nombre }}</li>
|
|
<li><a class="dropdown-item" href="{{ route('torneos.standings', $nt->id) }}">Posiciones</a></li>
|
|
<li><a class="dropdown-item" href="{{ route('torneos.topScorers', $nt->id) }}">Goleadores</a></li>
|
|
@if(!$loop->last)<li><hr class="dropdown-divider"></li>@endif
|
|
@endforeach
|
|
</ul>
|
|
</li>
|
|
@endif
|
|
|
|
@php
|
|
$isAdmin = session()->has('admin_logged_in') && session('admin_logged_in');
|
|
$isUser = session()->has('user_logged_in') && session('user_logged_in');
|
|
$userName = session('user_name', session('admin_username', ''));
|
|
$avatarClass = $isAdmin ? 'text-danger fw-bold' : ($isUser ? 'text-primary fw-bold' : '');
|
|
@endphp
|
|
|
|
@if($isUser || $isAdmin)
|
|
{{-- 🔔 Badge de notificaciones (solo para usuarios, no admins puros) --}}
|
|
@if($isUser)
|
|
<li class="nav-item me-1">
|
|
<a class="nav-link position-relative" href="{{ route('notificaciones.index') }}" id="notif-bell" title="Mis notificaciones">
|
|
<i class="bi bi-bell-fill fs-5"></i>
|
|
@if(isset($notifCount) && $notifCount > 0)
|
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id="notif-badge" style="font-size:0.65rem;">
|
|
{{ $notifCount > 99 ? '99+' : $notifCount }}
|
|
</span>
|
|
@else
|
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger d-none" id="notif-badge" style="font-size:0.65rem;"></span>
|
|
@endif
|
|
</a>
|
|
</li>
|
|
@endif
|
|
<li class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle {{ $avatarClass }}" href="#" role="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-person-circle"></i> {{ $userName }}
|
|
</a>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
@if($isUser)
|
|
<li class="dropdown-header">Usuario</li>
|
|
<li><span class="dropdown-item-text">{{ session('user_tipo') === 'jugador' ? '🏀' : '👤' }} {{ $userName }} ({{ session('user_tipo') }})</span></li>
|
|
<li><a class="dropdown-item" href="{{ route('panel.usuario') }}">Mi Panel</a></li>
|
|
<li>
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<button type="submit" class="dropdown-item text-danger">Salir</button>
|
|
</form>
|
|
</li>
|
|
@endif
|
|
@if($isAdmin)
|
|
@if($isUser)<li><hr class="dropdown-divider"></li>@endif
|
|
<li class="dropdown-header">Admin</li>
|
|
@if(session('admin_role') == 1 || session('admin_role') == 2)
|
|
<li><a class="dropdown-item" href="{{ route('admin.dashboard') }}">Gestionar</a></li>
|
|
@endif
|
|
<li>
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<button type="submit" class="dropdown-item text-danger">Salir Admin</button>
|
|
</form>
|
|
</li>
|
|
@endif
|
|
</ul>
|
|
</li>
|
|
@else
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#loginModal">Iniciar sesión</a>
|
|
</li>
|
|
@endif
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Contenido -->
|
|
<div class="content">
|
|
@yield('content')
|
|
</div>
|
|
|
|
<!-- Banner PWA: Instalar la app -->
|
|
<div id="pwa-install-banner" style="display:none; position:fixed; bottom:0; left:0; right:0; z-index:9999;
|
|
background: white; color: #111;
|
|
padding:15px 20px; align-items:center; justify-content:space-between;
|
|
gap:12px; box-shadow:0 -4px 30px rgba(0,0,0,0.15); flex-wrap:wrap; border-top: 1px solid #eee;">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div>
|
|
<img src="/icons/icon-96.png?v=2" alt="OnAPB" style="width:45px; height:45px; object-fit: contain;" onerror="this.style.display='none'">
|
|
</div>
|
|
<div id="pwa-text-container">
|
|
<div class="fw-bold text-dark" style="font-size:1rem; letter-spacing: -0.01em;">Instalá OnAPB en tu celular</div>
|
|
<div class="text-muted" style="font-size:0.82rem;">Accedé rápido a partidos, tu QR y notificaciones</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<button id="pwa-install-btn" class="btn btn-light btn-sm fw-bold px-4 hover-scale" style="border-radius:20px; height: 38px;">
|
|
<i class="bi bi-download me-1"></i> Instalar
|
|
</button>
|
|
<button id="pwa-install-dismiss" class="btn btn-link text-white p-2" style="text-decoration:none;" title="Cerrar">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer class="footer" style="background-color: var(--surface-container-low); border-top: 1px solid var(--outline-variant);">
|
|
|
|
<div class="sponsor-carousel-container" style="background-color: var(--surface-container-lowest); border-bottom: 1px solid var(--outline-variant);">
|
|
<div class="sponsor-carousel-track" id="sponsorTrack">
|
|
{{-- Logo OnAPB siempre va primero --}}
|
|
<div class="sponsor-item">
|
|
<img src="{{ asset('logo.png') }}" alt="OnAPB">
|
|
</div>
|
|
{{-- Sponsors si los hay --}}
|
|
@if(isset($footerSponsors) && $footerSponsors->count() > 0)
|
|
@foreach($footerSponsors as $s)
|
|
<div class="sponsor-item">
|
|
@if($s->url)
|
|
<a href="{{ $s->url }}" target="_blank">
|
|
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}">
|
|
</a>
|
|
@else
|
|
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}">
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
|
|
{{-- Duplicamos para loop infinito --}}
|
|
<div class="sponsor-item">
|
|
<img src="{{ asset('logo.png') }}" alt="OnAPB">
|
|
</div>
|
|
@foreach($footerSponsors as $s)
|
|
<div class="sponsor-item">
|
|
@if($s->url)
|
|
<a href="{{ $s->url }}" target="_blank">
|
|
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}">
|
|
</a>
|
|
@else
|
|
<img src="{{ asset($s->imagen) }}" alt="{{ $s->nombre }}">
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
@else
|
|
{{-- Sin sponsors: solo el logo centrado, sin animación --}}
|
|
<div class="sponsor-item">
|
|
<img src="{{ asset('logo.png') }}" alt="OnAPB">
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div class="container mt-4">
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 py-3">
|
|
<p class="mb-0 text-kinetic-muted small">
|
|
<span class="kfx-dot me-2"></span>
|
|
© {{ date('Y') }} OnAPB · <span class="kfx-text-gradient fw-bold">Kinetic Arena Experience</span>
|
|
</p>
|
|
<div class="d-flex gap-3 small">
|
|
<a href="{{ route('home') }}" class="kfx-link text-kinetic-muted">Inicio</a>
|
|
<a href="{{ route('eventos.index') }}" class="kfx-link text-kinetic-muted">Partidos</a>
|
|
<a href="{{ route('promos.index') }}" class="kfx-link text-kinetic-muted">Lugares</a>
|
|
<a href="{{ route('documentacion.index') }}" class="kfx-link text-kinetic-muted">Ayuda</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Modal de Login -->
|
|
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<ul class="nav nav-tabs" id="loginTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link @if(session('login_tab') != 'admin') active @endif" id="player-tab" data-bs-toggle="tab" data-bs-target="#playerLogin" type="button" role="tab">Jugador / Aficionado</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link @if(session('login_tab') == 'admin') active @endif" id="admin-tab" data-bs-toggle="tab" data-bs-target="#adminLogin" type="button" role="tab">Admin</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content p-3">
|
|
<!-- Error handled via Toast -->
|
|
|
|
<!-- Login Jugador/Aficionado -->
|
|
<div class="tab-pane fade @if(session('login_tab') != 'admin') show active @endif" id="playerLogin" role="tabpanel">
|
|
<form method="POST" action="{{ route('login') }}">
|
|
@csrf
|
|
<input type="hidden" name="tipo" value="player">
|
|
<div class="mb-3">
|
|
<input type="text" name="dni" class="form-control" placeholder="DNI" required>
|
|
</div>
|
|
<div class="mb-3 input-group">
|
|
<input type="password" name="password" class="form-control" placeholder="Contraseña" required>
|
|
<button class="btn btn-outline-secondary toggle-password" type="button">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="cf-turnstile" data-sitekey="{{ config('services.turnstile.site_key') }}"></div>
|
|
</div>
|
|
<button type="submit" class="btn btn-success w-100">Ingresar</button>
|
|
<div class="text-center mt-2">
|
|
<a href="{{ route('recuperar') }}" class="small text-secondary">¿Olvidaste tu contraseña?</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- Login Admin -->
|
|
<div class="tab-pane fade @if(session('login_tab') == 'admin') show active @endif" id="adminLogin" role="tabpanel">
|
|
<form method="POST" action="{{ route('login') }}">
|
|
@csrf
|
|
<input type="hidden" name="tipo" value="admin">
|
|
<div class="mb-3">
|
|
<input type="text" name="username" class="form-control" placeholder="Usuario" required>
|
|
</div>
|
|
<div class="mb-3 input-group">
|
|
<input type="password" name="password" class="form-control" placeholder="Contraseña" required>
|
|
<button class="btn btn-outline-secondary toggle-password" type="button">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="cf-turnstile" data-sitekey="{{ config('services.turnstile.site_key') }}"></div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary w-100">Ingresar como Admin</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
|
|
<script>
|
|
// Toggle visibilidad de contraseña
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.closest('.toggle-password')) {
|
|
const btn = e.target.closest('.toggle-password');
|
|
const input = btn.parentElement.querySelector('input');
|
|
const icon = btn.querySelector('i');
|
|
|
|
if (input.type === 'password') {
|
|
input.type = 'text';
|
|
icon.classList.replace('bi-eye', 'bi-eye-slash');
|
|
} else {
|
|
input.type = 'password';
|
|
icon.classList.replace('bi-eye-slash', 'bi-eye');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Notificaciones con SweetAlert
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Inicializar tooltips de Bootstrap
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
});
|
|
|
|
// Configuración Global SweetAlert2
|
|
const brandColors = {
|
|
primary: '#b00000',
|
|
success: '#2e7d32',
|
|
info: '#0277bd',
|
|
danger: '#b00000',
|
|
cancel: '#6c757d'
|
|
};
|
|
|
|
// Función centralizada para notificaciones Toast con estilo de marca
|
|
const showToast = (icon, title, text) => {
|
|
let iconColor = brandColors.info;
|
|
if (icon === 'success') iconColor = brandColors.success;
|
|
if (icon === 'error') iconColor = brandColors.danger;
|
|
if (icon === 'warning') iconColor = '#fbc02d';
|
|
|
|
Swal.fire({
|
|
icon: icon,
|
|
title: title,
|
|
text: text,
|
|
timer: 5000,
|
|
timerProgressBar: true,
|
|
showConfirmButton: false,
|
|
position: 'top-end',
|
|
toast: true,
|
|
iconColor: iconColor
|
|
});
|
|
};
|
|
|
|
// ── Intersection Observer: animaciones al scroll ──
|
|
const io = new IntersectionObserver((entries) => {
|
|
entries.forEach(e => {
|
|
if (e.isIntersecting) {
|
|
e.target.classList.add('visible');
|
|
io.unobserve(e.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
|
|
|
|
document.querySelectorAll('.animate-on-scroll, .animate-slide-left').forEach(el => io.observe(el));
|
|
|
|
// Mensaje de logout o cualquier mensaje por URL
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
['logout_msg', 'registro_msg', 'panel_msg', 'error_msg', 'admin_msg'].forEach(param => {
|
|
const val = urlParams.get(param);
|
|
if (val) {
|
|
const decoded = decodeURIComponent(val);
|
|
const isError = decoded.includes('⚠️') || decoded.toLowerCase().includes('error');
|
|
showToast(isError ? 'error' : 'success', isError ? '¡Ups!' : '¡Listo!', decoded);
|
|
}
|
|
});
|
|
|
|
// Capturar mensajes de sesión de Laravel
|
|
@if(session('mensaje'))
|
|
showToast('info', 'Información', `{!! addslashes(session("mensaje")) !!}`);
|
|
@endif
|
|
|
|
@if(session('registro_msg'))
|
|
@php $isErr = strpos(session("registro_msg"), "⚠️") !== false || strpos(session("registro_msg"), "Error") !== false; @endphp
|
|
showToast('{{ $isErr ? "error" : "success" }}', '{{ $isErr ? "¡Ups!" : "¡Listo!" }}', `{!! addslashes(session("registro_msg")) !!}`);
|
|
@endif
|
|
|
|
@if(session('panel_msg'))
|
|
showToast('success', '¡Listo!', `{!! addslashes(session("panel_msg")) !!}`);
|
|
@endif
|
|
|
|
@if(session('panel_error'))
|
|
showToast('error', '¡Ups!', `{!! addslashes(session("panel_error")) !!}`);
|
|
@endif
|
|
|
|
@if(session('admin_msg'))
|
|
showToast('success', '¡Listo!', `{!! addslashes(session("admin_msg")) !!}`);
|
|
@endif
|
|
|
|
@if(session('admin_error'))
|
|
showToast('error', '¡Ups!', `{!! addslashes(session("admin_error")) !!}`);
|
|
@endif
|
|
|
|
@if(session('login_error'))
|
|
showToast('error', 'Error de acceso', `{!! addslashes(session("login_error")) !!}`);
|
|
// Abrir el modal automáticamente si hay error de login para permitir re-intento
|
|
try {
|
|
var myModal = new bootstrap.Modal(document.getElementById('loginModal'));
|
|
myModal.show();
|
|
} catch (e) {
|
|
console.error("Error opening login modal:", e);
|
|
}
|
|
@endif
|
|
|
|
// Interceptor Global para Confirmaciones Estilizadas
|
|
document.addEventListener('submit', function(e) {
|
|
const form = e.target;
|
|
if (form.classList.contains('confirm-submit')) {
|
|
e.preventDefault();
|
|
const text = form.getAttribute('data-confirm-text') || '¿Estás seguro de realizar esta acción?';
|
|
const confirmButtonText = form.getAttribute('data-confirm-button') || 'Sí, confirmar';
|
|
const icon = form.getAttribute('data-confirm-icon') || 'warning';
|
|
|
|
// Determinar color del botón según el ícono o la clase
|
|
let confirmColor = brandColors.primary;
|
|
if (icon === 'success') confirmColor = brandColors.success;
|
|
if (icon === 'question') confirmColor = brandColors.info;
|
|
if (form.classList.contains('delete-form')) confirmColor = brandColors.danger;
|
|
|
|
Swal.fire({
|
|
title: '¿Confirmar acción?',
|
|
text: text,
|
|
icon: icon,
|
|
iconColor: confirmColor,
|
|
showCancelButton: true,
|
|
confirmButtonColor: confirmColor,
|
|
cancelButtonColor: brandColors.cancel,
|
|
confirmButtonText: confirmButtonText,
|
|
cancelButtonText: 'Cancelar',
|
|
reverseButtons: true
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
form.classList.remove('confirm-submit');
|
|
form.submit();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- SISTEMA DE NOTIFICACIONES PROACTIVAS ---
|
|
@if(session()->has('user_logged_in'))
|
|
(function() {
|
|
let lastNotifId = localStorage.getItem('last_notif_id') || 0;
|
|
|
|
function actualizarBadgeGlobal() {
|
|
fetch('/notificaciones/count')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const badge = document.getElementById('notif-badge');
|
|
if (!badge) return;
|
|
if (data.count > 0) {
|
|
badge.textContent = data.count > 99 ? '99+' : data.count;
|
|
badge.classList.remove('d-none');
|
|
} else {
|
|
badge.classList.add('d-none');
|
|
}
|
|
});
|
|
}
|
|
|
|
async function checkNewNotifications() {
|
|
if (!("Notification" in window)) return;
|
|
|
|
try {
|
|
const response = await fetch('{{ route("notificaciones.latest") }}');
|
|
const data = await response.json();
|
|
|
|
if (data.id > 0 && data.id > lastNotifId) {
|
|
if (Notification.permission === "granted" && document.visibilityState !== 'visible') {
|
|
const n = new Notification(data.titulo || "OnAPB", {
|
|
body: data.mensaje || "Tenés una nueva notificación",
|
|
icon: "/icons/icon-192.png"
|
|
});
|
|
n.onclick = () => {
|
|
window.focus();
|
|
location.href = "{{ route('notificaciones.index') }}";
|
|
};
|
|
}
|
|
|
|
actualizarBadgeGlobal();
|
|
lastNotifId = data.id;
|
|
localStorage.setItem('last_notif_id', lastNotifId);
|
|
} else if (data.id > 0) {
|
|
lastNotifId = Math.max(lastNotifId, data.id);
|
|
localStorage.setItem('last_notif_id', lastNotifId);
|
|
}
|
|
} catch (e) {
|
|
console.error("Error polling notifications:", e);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if ("Notification" in window && Notification.permission === "default") {
|
|
setTimeout(() => {
|
|
Notification.requestPermission();
|
|
}, 5000);
|
|
}
|
|
checkNewNotifications();
|
|
setInterval(checkNewNotifications, 300000); // 5 min
|
|
});
|
|
})();
|
|
@endif
|
|
</script>
|
|
|
|
@yield('scripts')
|
|
|
|
{{-- PWA: Registro del Service Worker y banner de instalación --}}
|
|
<script>
|
|
// Registrar Service Worker
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('/sw.js').catch(err => {
|
|
console.warn('SW registration failed:', err);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Banner de instalación a la app (PWA)
|
|
let deferredPrompt = null;
|
|
const installBanner = document.getElementById('pwa-install-banner');
|
|
const installBtn = document.getElementById('pwa-install-btn');
|
|
const installDismiss= document.getElementById('pwa-install-dismiss');
|
|
const pwaTextContainer = document.getElementById('pwa-text-container');
|
|
|
|
// Detección de iOS y Standalone
|
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone;
|
|
|
|
// Lógica para iOS (Safari no dispara beforeinstallprompt)
|
|
if (isIOS && !isStandalone && !localStorage.getItem('pwa-dismissed')) {
|
|
if (installBanner) {
|
|
installBanner.style.display = 'flex';
|
|
if (installBtn) installBtn.style.display = 'none';
|
|
if (pwaTextContainer) {
|
|
pwaTextContainer.innerHTML = `
|
|
<div class="fw-bold" style="font-size:0.95rem;">Instalá OnAPB en tu iPhone</div>
|
|
<div style="font-size:0.8rem; opacity:0.9;">Tocá <i class="bi bi-share"></i> y luego "Añadir a pantalla de inicio" <i class="bi bi-plus-square"></i></div>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
window.addEventListener('beforeinstallprompt', (e) => {
|
|
e.preventDefault();
|
|
deferredPrompt = e;
|
|
// Solo mostrar si no es iOS (donde ya manejamos el banner manual) y no está descartado
|
|
if (!isIOS && installBanner && !localStorage.getItem('pwa-dismissed')) {
|
|
installBanner.style.display = 'flex';
|
|
}
|
|
});
|
|
|
|
if (installBtn) {
|
|
installBtn.addEventListener('click', async () => {
|
|
if (!deferredPrompt) return;
|
|
deferredPrompt.prompt();
|
|
const { outcome } = await deferredPrompt.userChoice;
|
|
if (outcome === 'accepted') {
|
|
installBanner.style.display = 'none';
|
|
localStorage.setItem('pwa-dismissed', '1');
|
|
}
|
|
deferredPrompt = null;
|
|
});
|
|
}
|
|
|
|
if (installDismiss) {
|
|
installDismiss.addEventListener('click', () => {
|
|
if (installBanner) installBanner.style.display = 'none';
|
|
localStorage.setItem('pwa-dismissed', '1');
|
|
});
|
|
}
|
|
|
|
// --- Lógica de Web Push Notifications ---
|
|
@if(session('user_logged_in'))
|
|
async function subscribeUserToPush() {
|
|
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
|
console.warn('Push messaging is not supported');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
|
|
// Suscribirse
|
|
const subscription = await registration.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: urlBase64ToUint8Array("{{ env('VITE_VAPID_PUBLIC_KEY') }}")
|
|
});
|
|
|
|
console.log('User is subscribed:', subscription);
|
|
|
|
// Enviar suscripción al backend
|
|
await fetch("{{ route('notificaciones.subscribe') }}", {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify(subscription)
|
|
});
|
|
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: '¡Activado!',
|
|
text: 'Ya vas con recibir notificaciones en tu equipo.',
|
|
toast: true,
|
|
position: 'top-end',
|
|
timer: 3000,
|
|
showConfirmButton: false
|
|
});
|
|
|
|
// Ocultar botón si existe
|
|
const pushBtn = document.getElementById('pwa-push-btn');
|
|
if (pushBtn) pushBtn.classList.add('d-none');
|
|
|
|
} catch (err) {
|
|
console.error('Failed to subscribe the user: ', err);
|
|
if (Notification.permission === 'denied') {
|
|
Swal.fire('Atención', 'Bloqueaste las notificaciones. Por favor, habilitalas en la configuración de tu navegador.', 'warning');
|
|
}
|
|
}
|
|
}
|
|
|
|
function urlBase64ToUint8Array(base64String) {
|
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
const base64 = (base64String + padding)
|
|
.replace(/\-/g, '+')
|
|
.replace(/_/g, '/');
|
|
|
|
const rawData = window.atob(base64);
|
|
const outputArray = new Uint8Array(rawData.length);
|
|
|
|
for (let i = 0; i < rawData.length; ++i) {
|
|
outputArray[i] = rawData.charCodeAt(i);
|
|
}
|
|
return outputArray;
|
|
}
|
|
|
|
// Verificar permisos al cargar
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
// Ya tiene permiso, intentar renovar suscripción silenciosamente
|
|
navigator.serviceWorker.ready.then(reg => {
|
|
reg.pushManager.getSubscription().then(sub => {
|
|
if (sub) {
|
|
// Opcional: Re-enviar al backend por si cambió
|
|
}
|
|
});
|
|
});
|
|
} else if (isStandalone && 'Notification' in window && Notification.permission !== 'denied') {
|
|
// Si es PWA instalada, mostrar botón de habilitar notis
|
|
const installBanner = document.getElementById('pwa-install-banner');
|
|
const pwaTextContainer = document.getElementById('pwa-text-container');
|
|
const installBtn = document.getElementById('pwa-install-btn');
|
|
|
|
if (installBanner) {
|
|
installBanner.style.display = 'flex';
|
|
if (installBtn) installBtn.style.display = 'none';
|
|
if (pwaTextContainer) {
|
|
pwaTextContainer.innerHTML = `
|
|
<div class="fw-bold text-dark" style="font-size:0.95rem;">Habilitá las notificaciones</div>
|
|
<div class="text-muted" style="font-size:0.8rem;">Recibí avisos de partidos y novedades al instante</div>
|
|
`;
|
|
}
|
|
|
|
// Agregar botón de habilitar
|
|
const actionContainer = installBanner.querySelector('.d-flex.gap-2');
|
|
const pushBtn = document.createElement('button');
|
|
pushBtn.id = 'pwa-push-btn';
|
|
pushBtn.className = 'btn btn-primary btn-sm fw-bold px-4 hover-scale';
|
|
pushBtn.style.borderRadius = '20px';
|
|
pushBtn.style.height = '38px';
|
|
pushBtn.innerHTML = '<i class="bi bi-bell-fill me-1"></i> Habilitar';
|
|
pushBtn.onclick = subscribeUserToPush;
|
|
actionContainer.insertBefore(pushBtn, actionContainer.firstChild);
|
|
}
|
|
}
|
|
});
|
|
@endif
|
|
|
|
// --- Control del Splash Screen ---
|
|
// Solo se muestra una vez por sesión (no en cada navegación)
|
|
window.addEventListener('load', () => {
|
|
const splash = document.getElementById('splash-screen');
|
|
if (!splash) return;
|
|
|
|
if (sessionStorage.getItem('splashShown')) {
|
|
// Ya se mostró en esta sesión: ocultar inmediatamente sin animación
|
|
splash.remove();
|
|
return;
|
|
}
|
|
|
|
sessionStorage.setItem('splashShown', '1');
|
|
setTimeout(() => {
|
|
splash.classList.add('splash-hidden');
|
|
setTimeout(() => splash.remove(), 600);
|
|
}, 800);
|
|
});
|
|
|
|
// Fallback por si algo falla al cargar
|
|
setTimeout(() => {
|
|
const splash = document.getElementById('splash-screen');
|
|
if (splash) splash.classList.add('splash-hidden');
|
|
}, 5000);
|
|
</script>
|
|
|
|
{{-- Kinetic FX: capa de innovación visual (reveal, magnetic, ripple, parallax, cursor, contadores) --}}
|
|
<script src="{{ asset('static/kinetic-fx.js') }}?v={{ @filemtime(public_path('static/kinetic-fx.js')) ?: '3' }}" defer></script>
|
|
@include('components.genius-chat')
|
|
</body>
|
|
</html>
|
|
|