Update weather-webapp/templates/index.html
This commit is contained in:
@ -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, aż 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, 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 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: '© <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}&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>
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user