Update weather-webapp/templates/index.html

This commit is contained in:
2025-07-23 06:50:43 +00:00
parent 5e786a8d76
commit 8d6e8fe351

View File

@ -1,139 +1,593 @@
<!DOCTYPE html>
<html lang="pl">
<html lang="pl" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Interaktywna mapa pogody kliknij lub wyszukaj miasto, by sprawdzić prognozę.">
<title>Pogoda Web</title>
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" loading="lazy" />
<script src="https://cdn.jsdelivr.net/npm/chart.js" loading="lazy"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* {
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
}
:root {
--bs-font-sans-serif: 'Inter', sans-serif;
--fs-heading-lg: 3.5rem;
--fs-heading-md: 2.8rem;
--fs-heading-sm: 2.2rem;
--fs-lead-lg: 1.5rem;
--fs-lead-md: 1.25rem;
--fs-quote-lg: 1.1rem;
--fs-quote-md: 0.95rem;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #e0f7fa, #e8eaf6);
color: #333;
background-color: #212529;
color: #e2e6ea;
font-family: var(--bs-font-sans-serif);
line-height: 1.6;
}
header {
padding: 20px;
background: #2196f3;
color: white;
text-align: center;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
h1 {
font-size: var(--fs-heading-lg);
font-weight: 700;
}
main {
max-width: 1000px;
margin: auto;
padding: 20px;
h2 {
font-size: 2.5rem;
font-weight: 600;
}
#controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
.lead {
font-size: var(--fs-lead-lg);
font-weight: 300;
}
input, button {
font-size: 1em;
padding: 10px;
border-radius: 6px;
border: 1px solid #ccc;
.bg-primary {
background-color: #0d6efd !important;
}
button {
background: #2196f3;
color: white;
.bg-secondary {
background-color: #343a40 !important;
}
.card {
border: none;
transition: background 0.3s;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
background-color: #2b3035 !important;
}
button:hover {
background: #1976d2;
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3) !important;
}
#map {
height: 400px;
border-radius: 8px;
border-radius: 12px;
overflow: hidden;
margin-bottom: 20px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
min-height: 250px;
}
#result {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
white-space: pre-wrap;
margin-bottom: 20px;
#map:hover {
transform: scale(1.005);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
}
canvas {
background: white;
padding: 10px;
background: #2b3035;
padding: 15px;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
margin-bottom: 30px;
transition: all 0.3s ease;
}
canvas:hover {
box-shadow: 0 6px 15px rgba(0,0,0,0.25);
}
.spinner-wrapper {
display: none;
justify-content: center;
align-items: center;
margin: 2em 0;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.form-control, .btn {
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
margin-bottom: 40px;
transition: all 0.3s ease;
}
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
background-color: #212529;
color: #e2e6ea;
}
.form-control::placeholder {
color: #9da3a8;
}
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-primary:hover {
background-color: #0b5ed7;
border-color: #0a58ca;
transform: translateY(-2px);
}
.toast {
border-radius: 8px;
background-color: rgba(220, 53, 69, 0.9);
}
.toast-body {
color: #fff;
}
.navbar {
padding-top: 1rem;
padding-bottom: 1rem;
background-color: var(--bs-dark) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.navbar-brand {
display: flex;
align-items: center;
font-weight: 700;
font-size: 1.75rem;
color: var(--bs-primary) !important;
margin-left: 1rem;
}
.navbar-brand .logo {
height: 40px;
margin-right: 10px;
}
.theme-toggle-btn, .menu-toggle-btn {
background: none;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
.theme-toggle-btn:hover, .menu-toggle-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.4);
}
.theme-toggle-btn i, .menu-toggle-btn i {
font-size: 1.2rem;
}
.hero-section {
background-color: var(--bs-primary) !important;
padding-top: 3rem;
padding-bottom: 3rem;
position: relative;
overflow: hidden;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('https://www.transparenttextures.com/patterns/clean-gray-paper.png');
opacity: 0.1;
z-index: 1;
}
.hero-section .container {
position: relative;
z-index: 2;
}
.hero-section h1 {
font-size: var(--fs-heading-lg);
font-weight: 700;
margin-bottom: 0.5rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.hero-section .lead {
font-size: var(--fs-lead-lg);
font-weight: 300;
margin-bottom: 2rem;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
}
#carouselQuotes {
max-width: 800px;
margin-left: auto;
margin-right: auto;
padding: 1.5rem;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
min-height: 120px;
display: flex;
align-items: center;
}
#carouselQuotes .carousel-inner {
width: 100%;
}
#carouselQuotes .carousel-item .d-block {
font-size: var(--fs-quote-lg);
font-style: italic;
color: rgba(255, 255, 255, 0.9);
padding: 0 40px;
}
#carouselQuotes .carousel-control-prev-icon,
#carouselQuotes .carousel-control-next-icon {
background-image: none;
width: 1.5rem;
height: 1.5rem;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
}
#carouselQuotes .carousel-control-prev-icon::before {
content: '\276E';
color: #fff;
}
#carouselQuotes .carousel-control-next-icon::before {
content: '\276F';
color: #fff;
}
.animate__fadeInDown {
animation-duration: 1s;
}
.animate__fadeInUp {
animation-duration: 1s;
}
.offcanvas {
background-color: #2b3035 !important;
color: #e2e6ea;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
.offcanvas-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.offcanvas-title {
color: var(--bs-primary);
font-weight: 700;
}
.offcanvas .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
.offcanvas-body .list-group-item {
background-color: transparent;
color: #e2e6ea;
border: none;
padding: 0.75rem 1rem;
font-size: 1.1rem;
transition: background-color 0.2s ease, color 0.2s ease;
}
.offcanvas-body .list-group-item:hover {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
}
.offcanvas-body .list-group-item.active {
background-color: var(--bs-primary) !important;
color: #fff !important;
font-weight: 600;
border-radius: 0.5rem;
}
.offcanvas-body .list-group-item i {
margin-right: 10px;
width: 20px;
text-align: center;
}
@media (max-width: 767.98px) {
#map {
height: 250px;
}
h1 {
font-size: var(--fs-heading-sm);
}
h2 {
font-size: 2rem;
}
.lead {
font-size: var(--fs-lead-md);
}
.carousel-item .d-block {
font-size: var(--fs-quote-md) !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
.py-5 {
padding-top: 3rem !important;
padding-bottom: 3rem !important;
}
.my-5 {
margin-top: 3rem !important;
margin-bottom: 3rem !important;
}
.row.g-3 {
gap: 1rem !important;
}
.mb-4 {
margin-bottom: 1.5rem !important;
}
.navbar-brand {
font-size: 1.4rem;
}
.navbar-brand .logo {
height: 30px;
}
.hero-section h1 {
font-size: var(--fs-heading-md);
}
.hero-section .lead {
font-size: var(--fs-lead-md);
}
#carouselQuotes {
padding: 1rem;
min-height: 100px;
}
#carouselQuotes .carousel-item .d-block {
font-size: var(--fs-quote-md) !important;
padding: 0 20px;
}
.navbar .ms-auto .btn {
margin-left: 0.25rem;
}
}
@media (max-width: 575.98px) {
.hero-section h1 {
font-size: var(--fs-heading-sm);
}
.hero-section .lead {
font-size: var(--fs-lead-md);
}
}
:root[data-bs-theme='dark'] {
--bs-body-bg: #121212;
--bs-body-color: #fff;
}
:root[data-bs-theme='light'] {
--bs-body-bg: #ffffff;
--bs-body-color: #000;
}
</style>
</head>
<body>
<header>
<h1>Pogoda Web</h1>
<p>Kliknij na mapie lub wyszukaj miasto, aby zobaczyć prognozę pogody</p>
<div id="quote" style="margin-top: 1em; font-style: italic; font-size: 1.1em;"></div>
</header>
<main>
<div id="controls">
<input type="text" id="cityInput" placeholder="Wpisz miasto" autocomplete="off" />
<button onclick="searchCity()">Szukaj</button>
<body class="bg-dark text-light">
<nav class="navbar navbar-expand-lg navbar-dark animate__animated animate__fadeInDown">
<div class="container">
<button class="menu-toggle-btn" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasMenu" aria-controls="offcanvasMenu" aria-label="Toggle navigation">
<i class="fas fa-bars"></i>
</button>
<a class="navbar-brand" href="#">
Pogoda Web
</a>
<div class="ms-auto">
<button id="themeToggle" class="theme-toggle-btn">
<i class="fas fa-moon" id="themeIcon"></i>
</button>
</div>
<div id="map" title="Mapa pogodowa" aria-label="Mapa pogodowa z możliwością kliknięcia"></div>
<div id="result">Kliknij na mapie lub wyszukaj miasto, aby zobaczyć pogodę.</div>
<canvas id="dailyChart"></canvas>
<canvas id="hourlyChart"></canvas>
</div>
</nav>
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasMenu" aria-labelledby="offcanvasMenuLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasMenuLabel"><i class="fas fa-cloud-sun me-2"></i>Pogoda Web Menu</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="list-group list-group-flush">
<a href="#" class="list-group-item list-group-item-action active" aria-current="page"><i class="fas fa-map-marked-alt"></i>Mapa Pogody</a>
<a href="#" class="list-group-item list-group-item-action"><i class="fas fa-chart-line"></i>Statystyki Pogodowe</a>
<a href="#" class="list-group-item list-group-item-action"><i class="fas fa-calendar-alt"></i>Prognoza Długoterminowa</a>
<a href="#" class="list-group-item list-group-item-action"><i class="fas fa-info-circle"></i>O Aplikacji</a>
<a href="#" class="list-group-item list-group-item-action"><i class="fas fa-envelope"></i>Kontakt</a>
</div>
<div class="mt-4 border-top pt-3">
<p class="text-muted small">Wersja 1.0.0</p>
</div>
</div>
</div>
<header class="hero-section text-white text-center py-5 shadow-lg animate__animated animate__fadeInDown">
<div class="container">
<h1 class="fw-bold">Interaktywna Mapa Pogody</h1>
<p class="lead">Kliknij na mapie lub wyszukaj miasto, aby zobaczyć aktualną prognozę i szczegółowe dane.</p>
<div id="carouselQuotes" class="carousel slide" data-bs-ride="carousel" data-bs-interval="5000">
<div class="carousel-inner" id="quoteCarouselInner">
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselQuotes" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Poprzedni cytat</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselQuotes" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Następny cytat</span>
</button>
</div>
</div>
</header>
<main class="container my-5 animate__animated animate__fadeInUp">
<div class="row g-3 mb-4 align-items-center">
<div class="col-md-8">
<input type="text" id="cityInput" placeholder="Wpisz miasto..." class="form-control form-control-lg shadow-sm" aria-label="Wpisz nazwę miasta" />
</div>
<div class="col-md-4">
<button onclick="searchCity()" class="btn btn-primary btn-lg w-100 shadow-sm"><i class="fas fa-search me-2"></i>Szukaj</button>
</div>
</div>
<div class="spinner-wrapper" id="spinner">
<div class="spinner-border text-info" style="width: 3rem; height: 3rem;" role="status">
<span class="visually-hidden">Ładowanie...</span>
</div>
</div>
<div class="mb-4">
<div id="map" class="shadow-lg rounded-4" title="Mapa pogodowa" aria-label="Mapa pogodowa z możliwością kliknięcia"></div>
</div>
<div id="result" class="card shadow-lg mb-5 p-4 rounded-4">
<p class="lead mb-0">Kliknij na mapie lub wyszukaj miasto, aby zobaczyć pogodę.</p>
</div>
<h2 class="text-center mb-4 text-primary">Prognoza dzienna</h2>
<canvas id="dailyChart" class="w-100 mb-5 shadow-lg"></canvas>
<h2 class="text-center mb-4 text-primary">Prognoza godzinowa</h2>
<canvas id="hourlyChart" class="w-100 shadow-lg"></canvas>
</main>
<script src="https://unpkg.com/leaflet/dist/leaflet.js" loading="lazy"></script>
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
<div id="toastError" class="toast align-items-center border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="toastBody">Błąd</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Zamknij"></button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
//if (window.location.pathname === "/ping") {
// window.location.href = "/ping.html";
//}
document.addEventListener('DOMContentLoaded', () => {
const themeToggleBtn = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const htmlElement = document.documentElement;
const currentTheme = htmlElement.getAttribute('data-bs-theme');
if (currentTheme === 'light') {
themeIcon.classList.remove('fa-moon');
themeIcon.classList.add('fa-sun');
} else {
themeIcon.classList.remove('fa-sun');
themeIcon.classList.add('fa-moon');
}
themeToggleBtn.addEventListener('click', () => {
let currentTheme = htmlElement.getAttribute('data-bs-theme');
if (currentTheme === 'dark') {
htmlElement.setAttribute('data-bs-theme', 'light');
themeIcon.classList.remove('fa-moon');
themeIcon.classList.add('fa-sun');
} else {
htmlElement.setAttribute('data-bs-theme', 'dark');
themeIcon.classList.remove('fa-sun');
themeIcon.classList.add('fa-moon');
}
});
});
const weatherQuotes = [
"Ale nie do nas należy panowanie nad wszystkimi erami tego świata; my mamy za zadanie zrobić, co w naszej mocy, dla epoki, w której żyjemy, wytrzebić zło ze znanego nam pola, aby przekazać następcom rolę czystą, gotową do uprawy. Jaka im będzie sprzyjała pogoda, to już nie nasza rzecz.",
"Czyli do czego to służy? - nadal nie rozumiałam. - Niszczy wszystko, całe życie - złowieszczy głos Zafkiela potoczył się po pomieszczeniu. - TO KATAKLIZM, KATAKLIZM, KATAKLIZM! I oto dowód na to, co się dzieje, kiedy ktoś przez kilka miliardów lat włącza i wyłącza deszcz. Zdecydowanie nie polecam tego zawodu.",
"Ale nie do nas należy panowanie nad wszystkimi erami tego świata...",
"TO KATAKLIZM, KATAKLIZM, KATAKLIZM!",
" Jesteś dobrym człowiekiem. ...na złą pogodę.",
"Nie ma złej pogody, jest tylko złe ubranie!",
"Pomyślmy. Dziś mamy środę, a że to mój dzień, nazywaj mnie Wednesday, Panem Wednesday. Choć, zważywszy na pogodę, równie dobrze mógłbym nazywać się Thursday.",
"Nie jestem ja rządcą pogody i nie jest nim żadna istota dwunożna.",
" (...) Proszę spojrzeć przez okno. I powiedzieć, co pan widzi. Mgłę — odparł najwyższy kapłan. Vetinari westchnął. Czasami pogoda zupełnie nie miała wyczucia konwencji narracyjnej.",
"Czasem kluczem do sukcesu było właśnie cierpliwe wyczekiwanie. Jeśli pogoda ci nie odpowiada, nie pakujesz się w sam środek burzy, tylko czekasz,burza minie. Po co niepotrzebnie moknąć?",
"Internetowe wyzwiska znikają jak pogoda. Do nienawiści w małżeństwie trzeba się przyłożyć. Jest szczera.",
"Zima nie umiera. Nie tak jak umierają ludzie. Trzyma się późnych przymrozków, zapachu jesieni w letnie wieczory, a w czasie upałów ucieka w góry.",
"Historia ludzkości to w siedemdziesięciu trzech procentach rozmowy o pogodzie wyrażające prawdziwy strach."
"Pomyślmy. Dziś mamy środę...",
"Nie jestem ja rządcą pogody...",
"Mgłę — odparł najwyższy kapłan.",
"Jeśli pogoda ci nie odpowiada, poczekajminie.",
"Internetowe wyzwiska znikają jak pogoda...",
"Zima nie umiera. Nie tak jak umierają ludzie.",
"Historia ludzkości to w 73% rozmowy o pogodzie."
];
function populateCarousel() {
const container = document.getElementById("quoteCarouselInner");
container.innerHTML = '';
weatherQuotes.forEach((quote, i) => {
const item = document.createElement("div");
item.className = `carousel-item${i === 0 ? " active" : ""}`;
item.innerHTML = `<div class="d-block w-100 px-3 pb-2 fs-6 fst-italic text-shadow-sm">${quote}</div>`;
container.appendChild(item);
});
}
function getRandomQuote() {
const index = Math.floor(Math.random() * weatherQuotes.length);
return weatherQuotes[index];
function showSpinner(show) {
const spinner = document.getElementById("spinner");
if (show) {
spinner.style.display = "flex";
} else {
spinner.style.display = "none";
}
}
function showToast(message) {
const toastEl = document.getElementById("toastError");
const toastBody = document.getElementById("toastBody");
toastBody.innerText = message;
const toast = new bootstrap.Toast(toastEl);
toast.show();
}
function updateResult(text, isError = false) {
const resultDiv = document.getElementById("result");
resultDiv.innerHTML = `<p class="lead mb-0">${text}</p>`;
if (isError) {
resultDiv.classList.remove('bg-secondary');
resultDiv.classList.add('bg-danger');
showToast(text);
} else {
resultDiv.classList.remove('bg-danger');
resultDiv.classList.add('bg-secondary');
}
}
const map = L.map('map').setView([52.237, 21.017], 6);
let marker = null;
let currentPlaceName = null;
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }).addTo(map);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).addTo(map);
const rainLayer = L.tileLayer('', {
opacity: 0.6,
@ -148,7 +602,7 @@ function getRandomQuote() {
const timestamps = data.radar.past;
if (!timestamps.length) return;
const latestTime = timestamps[timestamps.length - 1];
const latestTime = timestamps[timestamps.length - 1].path;
const tileUrl = `https://tilecache.rainviewer.com/v2/radar/${latestTime}/256/{z}/{x}/{y}/2/1_1.png`;
rainLayer.setUrl(tileUrl);
@ -158,7 +612,6 @@ function getRandomQuote() {
}
}
async function fetchWithTimeout(resource, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
@ -170,14 +623,17 @@ function getRandomQuote() {
return response;
} catch (error) {
clearTimeout(timeout);
if (error.name === 'AbortError') {
throw new Error('Żądanie zostało przerwane: przekroczono limit czasu.');
}
throw error;
}
}
async function fetchPlaceName(lat, lon) {
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`;
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&amp;lon=${lon}&zoom=18&addressdetails=1`;
const response = await fetchWithTimeout(url, {
headers: { 'User-Agent': 'PogodaApp/1.0' }
headers: { 'User-Agent': 'PogodaApp/1.0 (your-email@example.com)' }
});
if (!response.ok) throw new Error('Błąd podczas pobierania nazwy miejsca.');
const data = await response.json();
@ -195,6 +651,7 @@ function getRandomQuote() {
setMarker(lat, lng, "Ładowanie nazwy...");
map.setView([lat, lng], 10);
updateResult("Ładowanie nazwy lokalizacji...");
showSpinner(true);
try {
const placeName = await fetchPlaceName(lat, lng);
currentPlaceName = placeName;
@ -202,24 +659,30 @@ function getRandomQuote() {
updateResult("Ładowanie pogody...");
await fetchWeather(lat, lng);
} catch (err) {
updateResult("Błąd: " + err.message);
updateResult("Błąd: " + err.message, true);
}
showSpinner(false);
});
function updateResult(text) {
document.getElementById('result').innerText = text;
}
async function searchCity() {
const city = document.getElementById('cityInput').value.trim();
if (!city) return updateResult("Proszę wpisać nazwę miasta.");
if (!city) {
updateResult("Proszę wpisać nazwę miasta.", true);
return;
}
updateResult("Wyszukiwanie miasta...");
showSpinner(true);
try {
const res = await fetchWithTimeout(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(city)}`);
const res = await fetchWithTimeout(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(city)}&limit=1`, {
headers: { 'User-Agent': 'PogodaApp/1.0 (your-email@example.com)' }
});
if (!res.ok) throw new Error('Błąd podczas wyszukiwania miasta.');
const data = await res.json();
if (!data.length) return updateResult("Nie znaleziono miasta.");
if (!data.length) {
updateResult("Nie znaleziono miasta.", true);
return;
}
const { lat, lon, display_name } = data[0];
currentPlaceName = display_name;
@ -228,8 +691,9 @@ function getRandomQuote() {
updateResult("Ładowanie pogody...");
await fetchWeather(lat, lon);
} catch (err) {
updateResult("Błąd: " + err.message);
updateResult("Błąd: " + err.message, true);
}
showSpinner(false);
}
let dailyChart = null;
@ -243,41 +707,29 @@ function getRandomQuote() {
body: JSON.stringify({ lat, lon })
});
if (!res.ok) throw new Error(`Błąd serwera: ${res.status}`);
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Błąd serwera: ${res.status} - ${errorText}`);
}
const data = await res.json();
if (data.error) return updateResult("Błąd serwera: " + data.error);
if (data.error) return updateResult("Błąd serwera: " + data.error, true);
const current = data.current ?? {};
const daily = data.daily?.forecast ?? {};
const hourly = data.hourly?.hourly_forecast ?? {};
let resultText = `Lokalizacja: ${currentPlaceName ?? 'Nieznana lokalizacja'}
Temperatura: ${current.temperature ?? 'brak danych'} °C
Wiatr: ${current.windspeed ?? 'brak danych'} km/h
Czas pomiaru: ${current.time ? new Date(current.time).toLocaleString() : 'brak danych'}`;
if (Array.isArray(daily.time) && daily.time.length > 0) {
resultText += "\n\nPrognoza 7-dniowa:\n";
for (let i = 0; i < daily.time.length; i++) {
resultText += `${daily.time[i]}: ${daily.temperature_2m_min?.[i]}${daily.temperature_2m_max?.[i]} °C, Opady: ${daily.precipitation_sum?.[i]} mm\n`;
}
}
if (Array.isArray(hourly.time) && hourly.time.length > 0) {
resultText += "\n\nNajbliższe godziny:\n";
for (let i = 0; i < Math.min(24, hourly.time.length); i++) {
resultText += `${hourly.time[i]}: ${hourly.temperature_2m?.[i]} °C, Wiatr: ${hourly.windspeed_10m?.[i]} km/h, Opady: ${hourly.precipitation?.[i]} mm\n`;
}
}
let resultText = `
<h4 class="card-title text-primary">${currentPlaceName ?? 'Nieznana lokalizacja'}</h4>
<p class="card-text mb-1"><i class="fas fa-thermometer-half me-2"></i><strong>Temperatura:</strong> ${current.temperature ?? 'brak danych'} °C</p>
<p class="card-text mb-1"><i class="fas fa-wind me-2"></i><strong>Wiatr:</strong> ${current.windspeed ?? 'brak danych'} km/h</p>
<p class="card-text mb-0"><i class="fas fa-clock me-2"></i><strong>Czas pomiaru:</strong> ${current.time ? new Date(current.time).toLocaleString() : 'brak danych'}</p>
`;
updateResult(resultText.trim());
const quote = getRandomQuote();
document.getElementById('quote').innerText = `Cytat dnia:\n"${quote}"`;
drawDailyChart(daily);
drawHourlyChart(hourly);
} catch (err) {
updateResult("Błąd podczas pobierania pogody: " + err.message);
updateResult("Błąd podczas pobierania pogody: " + err.message, true);
}
}
@ -288,121 +740,66 @@ Czas pomiaru: ${current.time ? new Date(current.time).toLocaleString() : 'brak d
dailyChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.time,
labels: data.time.map(t => new Date(t).toLocaleDateString('pl-PL', { weekday: 'short', day: 'numeric', month: 'short' })),
datasets: [
{
label: 'Temp. max (°C)',
data: data.temperature_2m_max,
backgroundColor: 'rgba(255, 99, 132, 0.6)'
backgroundColor: 'rgba(255, 99, 132, 0.8)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
},
{
label: 'Temp. min (°C)',
data: data.temperature_2m_min,
backgroundColor: 'rgba(54, 162, 235, 0.6)'
backgroundColor: 'rgba(54, 162, 235, 0.8)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
},
{
label: 'Opady (mm)',
data: data.precipitation_sum,
type: 'line',
borderColor: 'rgba(0, 128, 0, 0.8)',
fill: false,
yAxisID: 'y1'
borderColor: 'rgba(0, 200, 0, 0.8)',
backgroundColor: 'rgba(0, 200, 0, 0.2)',
fill: true,
yAxisID: 'y1',
tension: 0.3
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
interaction: { mode: 'index', intersect: false },
stacked: false,
scales: {
y: {
title: { display: true, text: 'Temperatura (°C)' }
plugins: {
title: {
display: true,
text: 'Prognoza dzienna',
color: '#e2e6ea',
font: {
size: 18
}
},
y1: {
position: 'right',
title: { display: true, text: 'Opady (mm)' },
grid: { drawOnChartArea: false }
legend: {
labels: {
color: '#e2e6ea',
font: {
size: 12
}
}
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.7)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#0d6efd',
}
}
}
});
}
function drawHourlyChart(data) {
const ctx = document.getElementById('hourlyChart').getContext('2d');
if (hourlyChart) hourlyChart.destroy();
hourlyChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.time,
datasets: [
{
label: 'Temperatura (°C)',
data: data.temperature_2m,
borderColor: 'orange',
fill: false
},
{
label: 'Wiatr (km/h)',
data: data.windspeed_10m,
borderColor: 'blue',
fill: false
},
{
label: 'Opady (mm)',
data: data.precipitation,
borderColor: 'green',
fill: false
}
]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: 'Wartość' }
}
}
}
});
}
function getUserLocation() {
if (navigator.geolocation) {
updateResult("Pobieranie Twojej lokalizacji...");
navigator.geolocation.getCurrentPosition(
async (position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
setMarker(lat, lon, "Ładowanie nazwy...");
map.setView([lat, lon], 10);
try {
const placeName = await fetchPlaceName(lat, lon);
currentPlaceName = placeName;
setMarker(lat, lon, placeName);
updateResult("Ładowanie pogody...");
await fetchWeather(lat, lon);
} catch (err) {
updateResult("Błąd lokalizacji: " + err.message);
}
},
(error) => {
updateResult("Nie udało się uzyskać lokalizacji: " + error.message);
}
);
} else {
updateResult("Geolokalizacja nie jest obsługiwana w tej przeglądarce.");
}
}
window.addEventListener('load', () => {
getUserLocation();
});
updateRainLayer();
</script>
</body>
</html>