diff --git a/angular.json b/angular.json index 8a9470f..b54541e 100644 --- a/angular.json +++ b/angular.json @@ -48,8 +48,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "8kB", + "maximumError": "12kB" } ], "outputHashing": "all" diff --git a/src/app/components/home/home.html b/src/app/components/home/home.html index 4d2d450..43fe116 100644 --- a/src/app/components/home/home.html +++ b/src/app/components/home/home.html @@ -1,4 +1,94 @@
+ + + + + + + + @if (showGpsModal()) { + + } + @if (gettingLocation()) {
@@ -6,7 +96,7 @@
} - @if (locationDenied() && !loading()) { + @if (locationDenied() && !loading() && !gpsPermissionAsked()) {

⚠️ Permisos d'ubicació denegats. Mostrant el temps de Lliçà d'Amunt.

} - - -
- -
} diff --git a/src/app/components/home/home.scss b/src/app/components/home/home.scss index 08fb11c..3441598 100644 --- a/src/app/components/home/home.scss +++ b/src/app/components/home/home.scss @@ -1,9 +1,13 @@ .weather-app { min-height: 100vh; background: linear-gradient(180deg, #072d59 0%, #56556a 100%); - padding: 20px; + padding: 12px; color: white; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + + @media (min-width: 768px) { + padding: 20px; + } } .loading, .error { @@ -22,15 +26,19 @@ right: 0; background: rgba(255, 152, 0, 0.95); backdrop-filter: blur(10px); - padding: 16px; + padding: 10px; text-align: center; z-index: 1000; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); p { - margin: 0 0 12px 0; - font-size: 14px; + margin: 0 0 8px 0; + font-size: 12px; font-weight: 500; + + @media (min-width: 768px) { + font-size: 14px; + } } .location-button { @@ -55,6 +63,143 @@ } } +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + padding: 20px; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal-content { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 20px; + padding: 30px 25px; + max-width: 400px; + width: 100%; + text-align: center; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + animation: slideUp 0.3s ease; + + @media (min-width: 768px) { + padding: 40px 35px; + border-radius: 24px; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal-icon { + font-size: 48px; + margin-bottom: 15px; + + @media (min-width: 768px) { + font-size: 60px; + margin-bottom: 20px; + } +} + +.modal-content h2 { + margin: 0 0 12px 0; + font-size: 22px; + font-weight: 700; + color: white; + + @media (min-width: 768px) { + font-size: 26px; + margin-bottom: 15px; + } +} + +.modal-content p { + margin: 0 0 25px 0; + font-size: 15px; + line-height: 1.6; + color: rgba(255, 255, 255, 0.95); + + @media (min-width: 768px) { + font-size: 16px; + margin-bottom: 30px; + } +} + +.modal-actions { + display: flex; + gap: 10px; + flex-direction: column; + + @media (min-width: 400px) { + flex-direction: row; + gap: 12px; + } +} + +.modal-button { + flex: 1; + padding: 12px 20px; + border: none; + border-radius: 12px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + + @media (min-width: 768px) { + padding: 14px 24px; + font-size: 16px; + } + + &.primary { + background: white; + color: #667eea; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(255, 255, 255, 0.3); + } + + &:active { + transform: translateY(0); + } + } + + &.secondary { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + + &:hover { + background: rgba(255, 255, 255, 0.3); + } + + &:active { + transform: scale(0.98); + } + } +} + .spinner { width: 50px; height: 50px; @@ -93,21 +238,35 @@ .header { text-align: center; - margin-bottom: 40px; - padding-top: 20px; + margin-bottom: 20px; + padding-top: 50px; + + @media (min-width: 768px) { + margin-bottom: 40px; + padding-top: 20px; + } .location { h1 { - font-size: 32px; + font-size: 24px; font-weight: 300; - margin: 0 0 8px 0; + margin: 0 0 6px 0; + + @media (min-width: 768px) { + font-size: 32px; + margin: 0 0 8px 0; + } } .station-info { - font-size: 14px; + font-size: 11px; opacity: 0.9; margin: 0; + @media (min-width: 768px) { + font-size: 14px; + } + .distance { opacity: 0.7; } @@ -115,22 +274,35 @@ } .current-temp { - margin: 30px 0; + margin: 20px 0; + + @media (min-width: 768px) { + margin: 30px 0; + } .temperature { - font-size: 96px; + font-size: 64px; font-weight: 200; line-height: 1; + + @media (min-width: 768px) { + font-size: 96px; + } } } .min-max { display: flex; justify-content: center; - gap: 20px; - font-size: 18px; + gap: 15px; + font-size: 15px; opacity: 0.9; + @media (min-width: 768px) { + gap: 20px; + font-size: 18px; + } + span { font-weight: 500; } @@ -140,43 +312,394 @@ .section { background: rgba(255, 255, 255, 0.15); backdrop-filter: blur(10px); - border-radius: 20px; - padding: 20px; - margin-bottom: 20px; + border-radius: 16px; + padding: 12px; + margin-bottom: 12px; + + @media (min-width: 768px) { + border-radius: 20px; + padding: 20px; + margin-bottom: 20px; + } h2 { - font-size: 14px; + font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; - margin: 0 0 15px 0; + margin: 0 0 10px 0; opacity: 0.8; + + @media (min-width: 768px) { + font-size: 14px; + margin: 0 0 15px 0; + } } } -.refresh-container { - text-align: center; - margin-top: 30px; +.menu-button { + position: fixed; + top: 15px; + right: 15px; + width: 36px; + height: 36px; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + border-radius: 10px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 5px; + cursor: pointer; + z-index: 1001; + transition: all 0.3s ease; - .refresh-button { - padding: 12px 24px; - background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); - color: white; - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - font-size: 16px; - font-weight: 600; - cursor: pointer; + @media (min-width: 768px) { + top: 20px; + right: 20px; + width: 40px; + height: 40px; + gap: 6px; + } + + &:hover { + background: rgba(255, 255, 255, 0.3); + transform: scale(1.05); + } + + span { + display: block; + width: 20px; + height: 2px; + background: white; + border-radius: 2px; transition: all 0.3s ease; - &:hover { - background: rgba(255, 255, 255, 0.3); - transform: translateY(-2px); - } - - &:active { - transform: translateY(0); + @media (min-width: 768px) { + width: 24px; } } } + +.menu-panel { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 100%; + max-width: 400px; + z-index: 1000; + pointer-events: none; + + &.open { + pointer-events: all; + + .menu-overlay { + opacity: 1; + } + + .menu-content { + transform: translateX(0); + } + } +} + +.menu-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + opacity: 0; + transition: opacity 0.3s ease; +} + +.menu-content { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 100%; + background: linear-gradient(180deg, #1a3a5f 0%, #2d2d3a 100%); + backdrop-filter: blur(20px); + box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3); + transform: translateX(100%); + transition: transform 0.3s ease; + overflow-y: auto; + padding: 15px; + + @media (min-width: 768px) { + padding: 20px; + } +} + +.menu-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + @media (min-width: 768px) { + margin-bottom: 30px; + padding-bottom: 20px; + } + + h2 { + font-size: 20px; + font-weight: 600; + margin: 0; + color: white; + + @media (min-width: 768px) { + font-size: 24px; + } + } + + .close-button { + width: 32px; + height: 32px; + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 50%; + color: white; + font-size: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + + @media (min-width: 768px) { + width: 36px; + height: 36px; + font-size: 24px; + } + + &:hover { + background: rgba(255, 255, 255, 0.2); + transform: rotate(90deg); + } + } +} + +.menu-item { + margin-bottom: 18px; + + @media (min-width: 768px) { + margin-bottom: 25px; + } + + &.last-update { + background: rgba(255, 255, 255, 0.1); + padding: 12px; + border-radius: 10px; + display: flex; + flex-direction: column; + gap: 4px; + + @media (min-width: 768px) { + padding: 15px; + border-radius: 12px; + gap: 5px; + } + + .label { + font-size: 10px; + opacity: 0.7; + text-transform: uppercase; + letter-spacing: 0.5px; + + @media (min-width: 768px) { + font-size: 12px; + } + } + + .value { + font-size: 14px; + font-weight: 600; + + @media (min-width: 768px) { + font-size: 16px; + } + } + } +} + +.menu-action { + width: 100%; + padding: 12px; + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(10px); + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-bottom: 12px; + + @media (min-width: 768px) { + padding: 16px; + border-radius: 12px; + font-size: 16px; + margin-bottom: 15px; + } + + &:hover { + background: rgba(255, 255, 255, 0.25); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + } + + &:active { + transform: translateY(0); + } + + &.secondary { + background: rgba(255, 255, 255, 0.05); + + &:hover { + background: rgba(255, 255, 255, 0.15); + } + } +} + +.search-section { + label { + display: block; + font-size: 12px; + font-weight: 600; + margin-bottom: 8px; + opacity: 0.9; + + @media (min-width: 768px) { + font-size: 14px; + margin-bottom: 10px; + } + } + + .search-input { + width: 100%; + padding: 10px 14px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + color: white; + font-size: 16px; /* 16px para evitar zoom automático en Safari iOS */ + transition: all 0.3s ease; + + @media (min-width: 768px) { + padding: 12px 16px; + border-radius: 10px; + font-size: 16px; + } + + &::placeholder { + color: rgba(255, 255, 255, 0.5); + } + + &:focus { + outline: none; + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.4); + } + } + + .search-loading { + margin-top: 8px; + padding: 8px; + text-align: center; + font-size: 12px; + opacity: 0.7; + + @media (min-width: 768px) { + margin-top: 10px; + padding: 10px; + font-size: 14px; + } + } + + .search-results { + margin-top: 8px; + max-height: 250px; + overflow-y: auto; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; + + @media (min-width: 768px) { + margin-top: 10px; + max-height: 300px; + border-radius: 10px; + } + + .search-result { + padding: 10px 12px; + cursor: pointer; + transition: all 0.2s ease; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + + @media (min-width: 768px) { + padding: 12px 16px; + } + + &:last-child { + border-bottom: none; + } + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + .result-name { + display: block; + font-weight: 500; + font-size: 13px; + margin-bottom: 3px; + + @media (min-width: 768px) { + font-size: 15px; + margin-bottom: 4px; + } + } + + .result-coords { + display: block; + font-size: 10px; + opacity: 0.6; + + @media (min-width: 768px) { + font-size: 12px; + } + } + } + } + + .no-results { + margin-top: 8px; + padding: 12px; + text-align: center; + font-size: 12px; + opacity: 0.6; + background: rgba(0, 0, 0, 0.2); + border-radius: 8px; + + @media (min-width: 768px) { + margin-top: 10px; + padding: 15px; + font-size: 14px; + border-radius: 10px; + } + } +} + +// Responsive +@media (max-width: 768px) { + .menu-panel { + max-width: 100%; + } +} diff --git a/src/app/components/home/home.ts b/src/app/components/home/home.ts index 4d4494e..d047165 100644 --- a/src/app/components/home/home.ts +++ b/src/app/components/home/home.ts @@ -27,11 +27,37 @@ export class Home implements OnInit { error = signal(null); gettingLocation = signal(false); locationDenied = signal(false); + + // Menú hamburguesa + menuOpen = signal(false); + searchQuery = signal(''); + searchResults = signal([]); + searching = signal(false); + + // Modal GPS + showGpsModal = signal(false); + gpsPermissionAsked = signal(false); constructor(private meteocatService: MeteocatService) {} ngOnInit(): void { - this.getCurrentLocation(); + // Verificar si ya se pidió permiso GPS antes + const gpsRejected = localStorage.getItem('gpsRejected'); + const gpsAsked = localStorage.getItem('gpsAsked'); + + if (gpsRejected === 'true') { + // Usuario rechazó GPS previamente, cargar datos por defecto + this.gpsPermissionAsked.set(true); + this.loadWeatherData(this.municipiCodi()); + } else if (gpsAsked === 'true') { + // Ya se pidió permiso antes, intentar obtener ubicación automáticamente + this.gpsPermissionAsked.set(true); + this.getCurrentLocation(); + } else { + // Primera vez, mostrar modal + this.showGpsModal.set(true); + this.loadWeatherData(this.municipiCodi()); // Cargar datos por defecto mientras + } } getCurrentLocation(): void { @@ -51,6 +77,11 @@ export class Home implements OnInit { console.log(`Ubicación GPS obtenida: ${lat}, ${lon}`); + // Marcar que el usuario aceptó GPS + localStorage.setItem('gpsAsked', 'true'); + localStorage.removeItem('gpsRejected'); + this.locationDenied.set(false); + // Buscar municipio más cercano this.meteocatService.getMunicipiCercano(lat, lon).subscribe({ next: (municipi) => { @@ -70,7 +101,12 @@ export class Home implements OnInit { (error) => { console.warn('Error obteniendo ubicación:', error.message); this.gettingLocation.set(false); - this.locationDenied.set(true); + + // Solo mostrar banner si el usuario lo solicitó manualmente + if (!this.gpsPermissionAsked()) { + this.locationDenied.set(true); + } + // Usar municipio por defecto si se deniega el permiso this.loadWeatherData(this.municipiCodi()); }, @@ -170,4 +206,90 @@ export class Home implements OnInit { this.locationDenied.set(false); this.getCurrentLocation(); } + + acceptGpsModal(): void { + this.showGpsModal.set(false); + this.gpsPermissionAsked.set(true); + localStorage.setItem('gpsAsked', 'true'); + this.getCurrentLocation(); + } + + rejectGpsModal(): void { + this.showGpsModal.set(false); + this.gpsPermissionAsked.set(true); + localStorage.setItem('gpsRejected', 'true'); + localStorage.setItem('gpsAsked', 'true'); + // Ya tenemos los datos cargados por defecto + } + + toggleMenu(): void { + this.menuOpen.set(!this.menuOpen()); + } + + closeMenu(): void { + this.menuOpen.set(false); + this.searchQuery.set(''); + this.searchResults.set([]); + } + + onSearchInput(event: Event): void { + const input = event.target as HTMLInputElement; + const query = input.value.trim(); + this.searchQuery.set(query); + + if (query.length < 2) { + this.searchResults.set([]); + return; + } + + this.searching.set(true); + this.meteocatService.searchMunicipis(query).subscribe({ + next: (results) => { + this.searchResults.set(results); + this.searching.set(false); + }, + error: (err) => { + console.error('Error searching municipis:', err); + this.searching.set(false); + } + }); + } + + selectMunicipi(municipi: Municipi): void { + console.log(`Seleccionado municipio: ${municipi.nom} (${municipi.codi})`); + this.municipiCodi.set(municipi.codi); + this.closeMenu(); + this.loadWeatherData(municipi.codi); + } + + getLastUpdateDate(): string | null { + const horaria = this.prediccioHoraria(); + if (!horaria || horaria.length === 0) { + return null; + } + + try { + const lastDate = new Date(horaria[0].dataHora); + const now = new Date(); + const diffMinutes = Math.floor((now.getTime() - lastDate.getTime()) / 60000); + + if (diffMinutes < 60) { + return `fa ${diffMinutes} minuts`; + } else if (diffMinutes < 1440) { + const hours = Math.floor(diffMinutes / 60); + return `fa ${hours} ${hours === 1 ? 'hora' : 'hores'}`; + } else { + const options: Intl.DateTimeFormatOptions = { + day: 'numeric', + month: 'short', + hour: '2-digit', + minute: '2-digit' + }; + return lastDate.toLocaleDateString('ca-ES', options); + } + } catch (error) { + console.error('Error calculating last update date:', error); + return null; + } + } } diff --git a/src/app/components/prediccio-diaria/prediccio-diaria.scss b/src/app/components/prediccio-diaria/prediccio-diaria.scss index 7fa859a..4cb7119 100644 --- a/src/app/components/prediccio-diaria/prediccio-diaria.scss +++ b/src/app/components/prediccio-diaria/prediccio-diaria.scss @@ -3,69 +3,113 @@ flex-direction: column; gap: 1px; background: rgba(255, 255, 255, 0.1); - border-radius: 15px; + border-radius: 12px; overflow: hidden; + @media (min-width: 768px) { + border-radius: 15px; + } + .day-item { display: grid; grid-template-columns: 1fr auto auto 1.2fr; align-items: center; - gap: 15px; - padding: 15px; + gap: 8px; + padding: 10px; background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); + @media (min-width: 768px) { + gap: 15px; + padding: 15px; + } + &:hover { background: rgba(255, 255, 255, 0.1); } .day-info { .day-name { - font-size: 16px; + font-size: 13px; font-weight: 500; margin-bottom: 2px; + + @media (min-width: 768px) { + font-size: 16px; + } } .date { - font-size: 13px; + font-size: 11px; opacity: 0.7; + + @media (min-width: 768px) { + font-size: 13px; + } } } .weather-icon { - font-size: 32px; + font-size: 26px; line-height: 1; text-align: center; + @media (min-width: 768px) { + font-size: 32px; + } + .weather-icon-img { - width: 36px; - height: 36px; + width: 28px; + height: 28px; object-fit: contain; + + @media (min-width: 768px) { + width: 36px; + height: 36px; + } } } .precipitation { - font-size: 13px; + font-size: 11px; opacity: 0.8; - min-width: 50px; + min-width: 40px; text-align: center; + + @media (min-width: 768px) { + font-size: 13px; + min-width: 50px; + } } .precipitation-placeholder { - min-width: 50px; + min-width: 40px; + + @media (min-width: 768px) { + min-width: 50px; + } } .temps { .temp-bar-container { display: flex; align-items: center; - gap: 10px; + gap: 6px; + + @media (min-width: 768px) { + gap: 10px; + } .temp-min, .temp-max { - font-size: 16px; + font-size: 13px; font-weight: 500; - min-width: 35px; + min-width: 30px; text-align: center; + + @media (min-width: 768px) { + font-size: 16px; + min-width: 35px; + } } .temp-min { @@ -74,11 +118,15 @@ .temp-bar { flex: 1; - height: 6px; + height: 5px; background: rgba(255, 255, 255, 0.2); border-radius: 3px; position: relative; + @media (min-width: 768px) { + height: 6px; + } + .temp-range { position: absolute; height: 100%; diff --git a/src/app/components/prediccio-horaria/prediccio-horaria.scss b/src/app/components/prediccio-horaria/prediccio-horaria.scss index 2f69f02..bc6b0ae 100644 --- a/src/app/components/prediccio-horaria/prediccio-horaria.scss +++ b/src/app/components/prediccio-horaria/prediccio-horaria.scss @@ -1,13 +1,18 @@ .hourly-forecast { .hourly-scroll { display: flex; - gap: 15px; + gap: 10px; overflow-x: auto; overflow-y: hidden; - padding: 10px 0; + padding: 8px 0; scroll-behavior: smooth; -webkit-overflow-scrolling: touch; + @media (min-width: 768px) { + gap: 15px; + padding: 10px 0; + } + // Ocultar scrollbar pero mantener funcionalidad &::-webkit-scrollbar { height: 6px; @@ -32,45 +37,77 @@ display: flex; flex-direction: column; align-items: center; - min-width: 65px; - padding: 10px; + min-width: 55px; + padding: 8px 6px; background: rgba(255, 255, 255, 0.1); - border-radius: 15px; - gap: 8px; + border-radius: 12px; + gap: 6px; flex-shrink: 0; + @media (min-width: 768px) { + min-width: 65px; + padding: 10px; + border-radius: 15px; + gap: 8px; + } + .day-label { - font-size: 11px; + font-size: 9px; font-weight: 600; opacity: 0.8; text-transform: uppercase; + + @media (min-width: 768px) { + font-size: 11px; + } } .hour { - font-size: 13px; + font-size: 11px; font-weight: 500; opacity: 0.9; + + @media (min-width: 768px) { + font-size: 13px; + } } .icon { - font-size: 28px; + font-size: 24px; line-height: 1; + @media (min-width: 768px) { + font-size: 28px; + } + .weather-icon { - width: 32px; - height: 32px; + width: 26px; + height: 26px; object-fit: contain; + + @media (min-width: 768px) { + width: 32px; + height: 32px; + } } } .temp { - font-size: 18px; + font-size: 15px; font-weight: 600; + + @media (min-width: 768px) { + font-size: 18px; + } } .rain { - font-size: 11px; + font-size: 9px; opacity: 0.8; + + @media (min-width: 768px) { + font-size: 11px; + } } } }