805 lines
29 KiB
HTML
805 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="pl" data-bs-theme="dark">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Pogoda Web</title>
|
||
<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 {
|
||
background-color: #212529;
|
||
color: #e2e6ea;
|
||
font-family: var(--bs-font-sans-serif);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
h1 {
|
||
font-size: var(--fs-heading-lg);
|
||
font-weight: 700;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 2.5rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.lead {
|
||
font-size: var(--fs-lead-lg);
|
||
font-weight: 300;
|
||
}
|
||
|
||
.bg-primary {
|
||
background-color: #0d6efd !important;
|
||
}
|
||
|
||
.bg-secondary {
|
||
background-color: #343a40 !important;
|
||
}
|
||
|
||
.card {
|
||
border: none;
|
||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||
background-color: #2b3035 !important;
|
||
}
|
||
|
||
.card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3) !important;
|
||
}
|
||
|
||
#map {
|
||
height: 400px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||
transition: all 0.3s ease;
|
||
min-height: 250px;
|
||
}
|
||
|
||
#map:hover {
|
||
transform: scale(1.005);
|
||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
canvas {
|
||
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;
|
||
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 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>
|
||
</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>
|
||
|
||
<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>
|
||
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...",
|
||
"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ę...",
|
||
"Nie jestem ja rządcą pogody...",
|
||
"Mgłę — odparł najwyższy kapłan.",
|
||
"Jeśli pogoda ci nie odpowiada, poczekaj aż minie.",
|
||
"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 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, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).addTo(map);
|
||
|
||
const rainLayer = L.tileLayer('', {
|
||
opacity: 0.6,
|
||
zIndex: 100,
|
||
attribution: 'Dane radarowe: RainViewer.com'
|
||
});
|
||
|
||
async function updateRainLayer() {
|
||
try {
|
||
const res = await fetch('https://api.rainviewer.com/public/weather-maps.json');
|
||
const data = await res.json();
|
||
const timestamps = data.radar.past;
|
||
if (!timestamps.length) return;
|
||
|
||
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);
|
||
map.addLayer(rainLayer);
|
||
} catch (err) {
|
||
console.error("Błąd pobierania warstwy deszczu:", err);
|
||
}
|
||
}
|
||
|
||
async function fetchWithTimeout(resource, options = {}) {
|
||
const controller = new AbortController();
|
||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||
options.signal = controller.signal;
|
||
|
||
try {
|
||
const response = await fetch(resource, options);
|
||
clearTimeout(timeout);
|
||
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}&zoom=18&addressdetails=1`;
|
||
const response = await fetchWithTimeout(url, {
|
||
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();
|
||
return data.display_name || 'Nieznana lokalizacja';
|
||
}
|
||
|
||
function setMarker(lat, lon, name) {
|
||
if (marker) map.removeLayer(marker);
|
||
marker = L.marker([lat, lon]).addTo(map);
|
||
if (name) marker.bindPopup(name).openPopup();
|
||
}
|
||
|
||
map.on('click', async (e) => {
|
||
const { lat, lng } = e.latlng;
|
||
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;
|
||
setMarker(lat, lng, placeName);
|
||
updateResult("Ładowanie pogody...");
|
||
await fetchWeather(lat, lng);
|
||
} catch (err) {
|
||
updateResult("Błąd: " + err.message, true);
|
||
}
|
||
showSpinner(false);
|
||
});
|
||
|
||
async function searchCity() {
|
||
const city = document.getElementById('cityInput').value.trim();
|
||
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)}&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) {
|
||
updateResult("Nie znaleziono miasta.", true);
|
||
return;
|
||
}
|
||
|
||
const { lat, lon, display_name } = data[0];
|
||
currentPlaceName = display_name;
|
||
map.setView([lat, lon], 10);
|
||
setMarker(lat, lon, display_name);
|
||
updateResult("Ładowanie pogody...");
|
||
await fetchWeather(lat, lon);
|
||
} catch (err) {
|
||
updateResult("Błąd: " + err.message, true);
|
||
}
|
||
showSpinner(false);
|
||
}
|
||
|
||
let dailyChart = null;
|
||
let hourlyChart = null;
|
||
|
||
async function fetchWeather(lat, lon) {
|
||
try {
|
||
const res = await fetchWithTimeout('/app/get_weather', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ lat, lon })
|
||
});
|
||
|
||
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, true);
|
||
|
||
const current = data.current ?? {};
|
||
const daily = data.daily?.forecast ?? {};
|
||
const hourly = data.hourly?.hourly_forecast ?? {};
|
||
|
||
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());
|
||
drawDailyChart(daily);
|
||
drawHourlyChart(hourly);
|
||
} catch (err) {
|
||
updateResult("Błąd podczas pobierania pogody: " + err.message, true);
|
||
}
|
||
}
|
||
|
||
function drawDailyChart(data) {
|
||
const ctx = document.getElementById('dailyChart').getContext('2d');
|
||
if (dailyChart) dailyChart.destroy();
|
||
|
||
dailyChart = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
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.8)',
|
||
borderColor: 'rgba(255, 99, 132, 1)',
|
||
borderWidth: 1
|
||
},
|
||
{
|
||
label: 'Temp. min (°C)',
|
||
data: data.temperature_2m_min,
|
||
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, 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,
|
||
plugins: {
|
||
title: {
|
||
display: true,
|
||
text: 'Prognoza dzienna',
|
||
color: '#e2e6ea',
|
||
font: {
|
||
size: 18
|
||
}
|
||
},
|
||
legend: {
|
||
labels: {
|
||
color: '#e2e6ea',
|
||
font: {
|
||
size: 12
|
||
}
|
||
}
|
||
},
|
||
tooltip: {
|
||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||
titleColor: '#fff',
|
||
bodyColor: '#fff',
|
||
borderColor: '#0d6efd',
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |