first commit

This commit is contained in:
2025-07-22 09:18:35 +02:00
commit 82d8e52e0b
13 changed files with 1927 additions and 0 deletions

17
docker-compose.yml Normal file
View File

@ -0,0 +1,17 @@
services:
weather-web:
build:
context: ./weather-webapp
dockerfile: Dockerfile
restart: always
ports:
- "8000:8000"
networks:
- web-net
networks:
web-net:
external: true
name: shared-net

21
weather-webapp/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
RUN update-ca-certificates
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "main:app"]
#CMD ["flask", "run", "--host=0.0.0.0", "--port=6000"]
#CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "6000"]

186
weather-webapp/main.py Normal file
View File

@ -0,0 +1,186 @@
from flask import Flask, render_template, request, jsonify
import requests
app = Flask(__name__)
BASE_URL = "https://meteo.cbpio.pl"
#app.route("/gui/weather")
#def simple_weather():
# return render_template("weather.html")
@app.route("/gui/weather")
def simple_weather():
city = request.args.get("city", "Warszawa")
# Sprawdź, czy klient akceptuje JSON (np. curl)
if "application/json" in request.headers.get("Accept", "") or "curl" in request.headers.get("User-Agent", ""):
try:
response = requests.get(f"https://meteo.cbpio.pl/weather", params={"city": city})
return jsonify(response.json())
except Exception as e:
return jsonify({"error": str(e)}), 500
# Dla przeglądarki - HTML
return render_template("weather.html")
@app.route("/gui/weather/geo")
def geo_weather_page():
lat = request.args.get("lat")
lon = request.args.get("lon")
if not lat or not lon:
msg = {"error": "Podaj współrzędne, np. ?lat=52.4&lon=16.9"}
if "application/json" in request.headers.get("Accept", "") or "curl" in request.headers.get("User-Agent", ""):
return jsonify(msg), 400
return msg["error"], 400
try:
response = requests.get("https://meteo.cbpio.pl/weather/geo", params={"latitude": lat, "longitude": lon})
data = response.json()
except Exception as e:
error_msg = {"error": str(e)}
if "application/json" in request.headers.get("Accept", "") or "curl" in request.headers.get("User-Agent", ""):
return jsonify(error_msg), 500
return f"Błąd: {e}", 500
if "application/json" in request.headers.get("Accept", "") or "curl" in request.headers.get("User-Agent", ""):
return jsonify(data)
return render_template("geo_weather.html", weather=data)
@app.route("/gui/forecast")
def forecast():
city = request.args.get("city", "").strip()
if not city:
return render_template("forecast.html", city="", error="Nie podano miasta")
try:
forecast_data = requests.get(f"{BASE_URL}/forecast", params={"city": city}).json()
except Exception as e:
return render_template("forecast.html", city=city, error="Błąd pobierania danych")
accept = request.headers.get("Accept", "")
if "application/json" in accept:
return jsonify(forecast_data)
return render_template("forecast.html", city=forecast_data["city"], forecast=forecast_data["forecast"])
@app.route("/gui/forecast/hourly")
def hourly_forecast():
city = request.args.get("city", "").lower()
hours = request.args.get("hours", 24, type=int)
if not city:
return "Brak parametru 'city'", 400
try:
resp = requests.get("https://meteo.cbpio.pl/forecast/hourly", params={"city": city, "hours": hours})
resp.raise_for_status()
data = resp.json()
except Exception as e:
error_msg = f"Błąd pobierania danych: {str(e)}"
if "text/html" in request.headers.get("Accept", ""):
return render_template("hourly_forecast.html", city=city.capitalize(), hourly_forecast=None, error=error_msg)
return jsonify({"error": error_msg}), 500
hourly = data.get("hourly_forecast", {})
if "text/html" in request.headers.get("Accept", ""):
return render_template(
"hourly_forecast.html",
city=city.capitalize(),
hourly_forecast=hourly,
error=None
)
else:
return jsonify(data)
@app.route("/gui/history/range")
def history_range():
city = request.args.get("city", "").lower()
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
accept = request.headers.get("Accept", "")
if not city or not start_date or not end_date:
error_msg = "Brak wymaganych parametrów"
if "text/html" in accept:
return render_template("history_range.html", city=city.capitalize(),
start_date=start_date, end_date=end_date,
history=None, error=error_msg)
else:
return jsonify({"error": error_msg}), 400
url = (
f"https://meteo.cbpio.pl/history-range"
f"?city={city}&start_date={start_date}&end_date={end_date}"
)
try:
res = requests.get(url)
res.raise_for_status()
data = res.json()
except Exception as e:
if "text/html" in accept:
return render_template("history_range.html", city=city.capitalize(),
start_date=start_date, end_date=end_date,
history=None, error=str(e))
else:
return jsonify({"error": str(e)}), 500
history = data.get("history") # ✅ WYCIĄGAMY KLUCZ "history" z JSON-a
if "text/html" in accept:
return render_template("history_range.html", city=city.capitalize(),
start_date=start_date, end_date=end_date,
history=history, error=None) # ✅ przekazujemy history, nie cały JSON
else:
return jsonify({
"city": city.capitalize(),
"start_date": start_date,
"end_date": end_date,
"history": history
})
@app.route("/app/")
def index():
return render_template("index.html")
@app.route("/app/get_weather", methods=["POST"])
def get_weather():
data = request.get_json()
lat = data.get("lat")
lon = data.get("lon")
if lat is None or lon is None:
return jsonify({"error": "Missing coordinates"}), 400
try:
current = requests.get(f"{BASE_URL}/weather/geo", params={"latitude": lat, "longitude": lon}).json()
forecast = requests.get(f"{BASE_URL}/forecast/geo", params={
"latitude": lat,
"longitude": lon,
"days": 7
}).json()
hourly = requests.get(f"{BASE_URL}/forecast/hourly/geo", params={
"latitude": lat,
"longitude": lon,
"hours": 24
}).json()
return jsonify({
"current": current,
"daily": forecast,
"hourly": hourly
})
except Exception as e:
print("❌ Error fetching data:", e)
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
#app.run(debug=True)
app.run(debug=True,host='0.0.0.0', port=8000)

View File

@ -0,0 +1,4 @@
flask
requests
gunicorn
uvicorn

View File

@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<title>Prognoza pogody - {{ city }}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
/* Reset i podstawy */
* {
box-sizing: border-box;
}
body {
font-family: 'Roboto', Arial, sans-serif;
background: linear-gradient(135deg, #74ebd5 0%, #ACB6E5 100%);
color: #222;
margin: 0;
padding: 40px 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
color: #0d3b66;
font-weight: 700;
margin-bottom: 30px;
text-shadow: 1px 1px 4px rgba(0,0,0,0.1);
}
.error {
color: #ff4c4c;
font-weight: 700;
background: #ffe6e6;
padding: 15px 25px;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(255,0,0,0.15);
max-width: 600px;
text-align: center;
margin-bottom: 40px;
}
table {
width: 90%;
max-width: 800px;
border-collapse: separate;
border-spacing: 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
background-color: #fff;
margin-bottom: 40px;
font-size: 1rem;
}
thead tr {
background: linear-gradient(90deg, #0d3b66, #3f72af);
color: #fff;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
th, td {
padding: 14px 20px;
text-align: center;
border-right: 1px solid rgba(255,255,255,0.2);
}
th:last-child, td:last-child {
border-right: none;
}
tbody tr {
border-bottom: 1px solid #e2e8f0;
transition: background-color 0.3s ease;
}
tbody tr:nth-child(even) {
background-color: #f7faff;
}
tbody tr:hover {
background-color: #dbe9ff;
}
/* Canvas kontener */
#chartContainer {
width: 90%;
max-width: 900px;
background: #fff;
padding: 25px 30px;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
}
/* Responsive */
@media (max-width: 768px) {
body {
padding: 20px 10px;
}
table {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
<h1>Prognoza pogody dla {{ city }}</h1>
{% if error %}
<p class="error">{{ error }}</p>
{% elif forecast and forecast.time %}
<table>
<thead>
<tr>
<th>Data</th>
<th>Temperatura maks. (°C)</th>
<th>Temperatura min. (°C)</th>
<th>Opady (mm)</th>
</tr>
</thead>
<tbody>
{% for i in range(forecast.time|length) %}
<tr>
<td>{{ forecast.time[i] }}</td>
<td>{{ forecast.temperature_2m_max[i] }}</td>
<td>{{ forecast.temperature_2m_min[i] }}</td>
<td>{{ forecast.precipitation_sum[i] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div id="chartContainer">
<canvas id="weatherChart" width="800" height="400"></canvas>
</div>
<script>
const labels = {{ forecast.time|tojson }};
const tempMax = {{ forecast.temperature_2m_max|tojson }};
const tempMin = {{ forecast.temperature_2m_min|tojson }};
const precipitation = {{ forecast.precipitation_sum|tojson }};
const ctx = document.getElementById('weatherChart').getContext('2d');
const weatherChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Temperatura maksymalna (°C)',
data: tempMax,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.3)',
fill: false,
tension: 0.3,
pointRadius: 5,
pointHoverRadius: 7,
},
{
label: 'Temperatura minimalna (°C)',
data: tempMin,
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.3)',
fill: false,
tension: 0.3,
pointRadius: 5,
pointHoverRadius: 7,
},
{
label: 'Opady (mm)',
data: precipitation,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.3)',
fill: true,
yAxisID: 'y1',
tension: 0.3,
pointRadius: 5,
pointHoverRadius: 7,
}
]
},
options: {
responsive: true,
interaction: {
mode: 'nearest',
intersect: false,
},
scales: {
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: 'Temperatura (°C)',
font: { weight: 'bold' }
},
grid: {
color: 'rgba(0,0,0,0.05)'
}
},
y1: {
type: 'linear',
position: 'right',
title: {
display: true,
text: 'Opady (mm)',
font: { weight: 'bold' }
},
grid: {
drawOnChartArea: false
}
},
x: {
ticks: {
maxRotation: 45,
minRotation: 30
}
}
},
plugins: {
legend: {
position: 'top',
labels: {
font: { size: 14, weight: '600' }
}
},
title: {
display: true,
text: 'Prognoza pogody',
font: { size: 20, weight: '700' }
},
tooltip: {
enabled: true,
mode: 'index',
intersect: false,
backgroundColor: 'rgba(0,0,0,0.8)',
titleFont: { size: 16, weight: 'bold' },
bodyFont: { size: 14 }
}
},
animation: {
duration: 1000,
easing: 'easeOutQuart'
}
}
});
</script>
{% else %}
<p>Brak danych pogodowych.</p>
{% endif %}
</body>
</html>

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Pogoda z koordynatów</title>
<style>
body { font-family: sans-serif; background: #f0f0f0; padding: 2rem; }
.card {
background: white; padding: 2rem; border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 500px; margin: auto;
}
.label { font-weight: bold; }
</style>
</head>
<body>
<div class="card">
<h2>Pogoda z Open-Meteo</h2>
<p><span class="label">Miasto:</span> {{ weather.city }}</p>
<p><span class="label">Szerokość:</span> {{ weather.latitude }}</p>
<p><span class="label">Długość:</span> {{ weather.longitude }}</p>
<p><span class="label">Temperatura:</span> {{ weather.temperature }} °C</p>
<p><span class="label">Wiatr:</span> {{ weather.windspeed }} m/s</p>
<p><span class="label">Czas:</span> {{ weather.time }}</p>
</div>
</body>
</html>

View File

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<title>Historia pogody - {{ city }}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
body {
font-family: 'Roboto', Arial, sans-serif;
background: linear-gradient(135deg, #FDFCFB 0%, #E2D1C3 100%);
color: #222;
margin: 0;
padding: 40px 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
color: #2a2a72;
font-size: 1.8rem;
text-align: center;
margin-bottom: 30px;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
.error {
color: #ff4c4c;
font-weight: 700;
background: #ffe6e6;
padding: 15px 25px;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(255,0,0,0.15);
max-width: 600px;
text-align: center;
margin-bottom: 40px;
}
table {
width: 90%;
max-width: 900px;
border-collapse: separate;
border-spacing: 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
background-color: #fff;
margin-bottom: 40px;
font-size: 1rem;
}
thead tr {
background: linear-gradient(90deg, #2a2a72, #009ffd);
color: #fff;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
th, td {
padding: 14px 20px;
text-align: center;
border-right: 1px solid rgba(255,255,255,0.2);
}
th:last-child, td:last-child {
border-right: none;
}
tbody tr {
border-bottom: 1px solid #e2e8f0;
transition: background-color 0.3s ease;
}
tbody tr:nth-child(even) {
background-color: #f7faff;
}
tbody tr:hover {
background-color: #dbe9ff;
}
canvas {
max-width: 95%;
margin-bottom: 60px;
}
@media (max-width: 768px) {
body {
padding: 20px 10px;
}
table {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
<h1>Historia pogody dla {{ city }}<br>od {{ start_date }} do {{ end_date }}</h1>
{% if error %}
<p class="error">{{ error }}</p>
{% elif history %}
<table>
<thead>
<tr>
<th>Data</th>
<th>Temp. maks. (°C)</th>
<th>Temp. min. (°C)</th>
<th>Opady (mm)</th>
</tr>
</thead>
<tbody>
{% for i in range(history["time"]|length) %}
<tr>
<td>{{ history["time"][i] }}</td>
<td>{{ history["temperature_2m_max"][i] }}</td>
<td>{{ history["temperature_2m_min"][i] }}</td>
<td>{{ history["precipitation_sum"][i] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Wykres -->
<canvas id="historyChart" width="800" height="400"></canvas>
<script>
const labels = {{ history["time"]|tojson }};
const tempMax = {{ history["temperature_2m_max"]|tojson }};
const tempMin = {{ history["temperature_2m_min"]|tojson }};
const precipitation = {{ history["precipitation_sum"]|tojson }};
const ctx = document.getElementById('historyChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Temperatura maksymalna (°C)',
data: tempMax,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: false,
tension: 0.3
},
{
label: 'Temperatura minimalna (°C)',
data: tempMin,
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
fill: false,
tension: 0.3
},
{
label: 'Opady (mm)',
data: precipitation,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
fill: true,
yAxisID: 'y1',
tension: 0.3
}
]
},
options: {
responsive: true,
scales: {
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: 'Temperatura (°C)'
}
},
y1: {
type: 'linear',
position: 'right',
title: {
display: true,
text: 'Opady (mm)'
},
grid: {
drawOnChartArea: false
}
}
},
plugins: {
legend: {
position: 'top'
},
title: {
display: true,
text: 'Historia pogody'
}
}
}
});
</script>
{% endif %}
</body>
</html>

View File

@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Prognoza godzinowa - {{ city }}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
<style>
body {
font-family: 'Roboto', sans-serif;
background-color: #eef2f3;
color: #222;
padding: 30px;
transition: background 0.3s, color 0.3s;
}
body.dark {
background-color: #121212;
color: #f0f0f0;
}
h1 {
color: #2a2a72;
text-align: center;
}
.dark h1 {
color: #90caf9;
}
.controls {
text-align: center;
margin-bottom: 20px;
}
.controls input, .controls button {
margin: 0 5px;
padding: 5px 10px;
}
.error {
color: red;
font-weight: bold;
text-align: center;
}
table {
margin: 20px auto;
border-collapse: collapse;
width: 95%;
}
th, td {
border: 1px solid #aaa;
padding: 8px;
text-align: center;
}
th {
background-color: #2a6ebb;
color: #fff;
}
.dark table {
background-color: #1e1e1e;
color: #ddd;
}
.dark th {
background-color: #37474f;
}
canvas {
display: block;
margin: 40px auto;
max-width: 95%;
}
</style>
</head>
<body>
<h1>Prognoza godzinowa dla {{ city }}</h1>
<div class="controls">
<label>Od godziny: <input type="time" id="startHour"></label>
<label>Do godziny: <input type="time" id="endHour"></label>
<button onclick="filterByHour()">Filtruj</button>
<button onclick="toggleDarkMode()">🌜 Przełącz tryb nocny</button>
</div>
{% if error %}
<p class="error">{{ error }}</p>
{% elif hourly_forecast %}
<table id="hourlyTable">
<thead>
<tr>
<th>Godzina</th><th>Temp. (°C)</th><th>Wiatr (km/h)</th><th>Opady (mm)</th>
</tr>
</thead>
<tbody>
{% for i in range(hourly_forecast["time"]|length) %}
<tr>
<td>{{ hourly_forecast["time"][i] }}</td>
<td>{{ hourly_forecast["temperature_2m"][i] }}</td>
<td>{{ hourly_forecast["windspeed_10m"][i] }}</td>
<td>{{ hourly_forecast["precipitation"][i] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<canvas id="hourlyChart" width="800" height="400"></canvas>
<script>
const originalData = {
labels: {{ hourly_forecast["time"]|tojson }},
temperature: {{ hourly_forecast["temperature_2m"]|tojson }},
wind: {{ hourly_forecast["windspeed_10m"]|tojson }},
precipitation: {{ hourly_forecast["precipitation"]|tojson }}
};
const ctx = document.getElementById('hourlyChart').getContext('2d');
let chart;
function renderChart(data) {
if (chart) chart.destroy();
chart = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: [
{
label: 'Temperatura (°C)',
data: data.temperature,
borderColor: 'rgba(255,99,132,1)',
backgroundColor: 'rgba(255,99,132,0.2)',
tension: 0.3,
yAxisID: 'y'
},
{
label: 'Wiatr (km/h)',
data: data.wind,
borderColor: 'rgba(255,159,64,1)',
backgroundColor: 'rgba(255,159,64,0.2)',
tension: 0.3,
yAxisID: 'y'
},
{
label: 'Opady (mm)',
data: data.precipitation,
borderColor: 'rgba(75,192,192,1)',
backgroundColor: 'rgba(75,192,192,0.2)',
fill: true,
yAxisID: 'y1',
tension: 0.3
}
]
},
options: {
responsive: true,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Prognoza godzinowa' },
zoom: {
pan: {
enabled: true,
mode: 'x',
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x'
}
}
},
scales: {
y: {
position: 'left',
title: { display: true, text: 'Temperatura / Wiatr' }
},
y1: {
position: 'right',
title: { display: true, text: 'Opady (mm)' },
grid: { drawOnChartArea: false }
}
}
}
});
}
function filterByHour() {
const start = document.getElementById('startHour').value;
const end = document.getElementById('endHour').value;
if (!start || !end) return;
const filtered = {
labels: [],
temperature: [],
wind: [],
precipitation: []
};
originalData.labels.forEach((time, i) => {
const hour = time.slice(11, 16);
if (hour >= start && hour <= end) {
filtered.labels.push(time);
filtered.temperature.push(originalData.temperature[i]);
filtered.wind.push(originalData.wind[i]);
filtered.precipitation.push(originalData.precipitation[i]);
}
});
renderChart(filtered);
}
function toggleDarkMode() {
document.body.classList.toggle('dark');
renderChart(chart.data);
}
renderChart(originalData);
</script>
{% endif %}
</body>
</html>

View File

@ -0,0 +1,408 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<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>
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #e0f7fa, #e8eaf6);
color: #333;
}
header {
padding: 20px;
background: #2196f3;
color: white;
text-align: center;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
main {
max-width: 1000px;
margin: auto;
padding: 20px;
}
#controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
}
input, button {
font-size: 1em;
padding: 10px;
border-radius: 6px;
border: 1px solid #ccc;
}
button {
background: #2196f3;
color: white;
border: none;
transition: background 0.3s;
cursor: pointer;
}
button:hover {
background: #1976d2;
}
#map {
height: 400px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
#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;
}
canvas {
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
margin-bottom: 40px;
}
</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>
</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>
</main>
<script src="https://unpkg.com/leaflet/dist/leaflet.js" loading="lazy"></script>
<script>
//if (window.location.pathname === "/ping") {
// window.location.href = "/ping.html";
//}
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.",
" 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."
];
function getRandomQuote() {
const index = Math.floor(Math.random() * weatherQuotes.length);
return weatherQuotes[index];
}
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);
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];
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);
throw error;
}
}
async function fetchPlaceName(lat, lon) {
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`;
const response = await fetchWithTimeout(url, {
headers: { 'User-Agent': 'PogodaApp/1.0' }
});
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...");
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);
}
});
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.");
updateResult("Wyszukiwanie miasta...");
try {
const res = await fetchWithTimeout(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(city)}`);
if (!res.ok) throw new Error('Błąd podczas wyszukiwania miasta.');
const data = await res.json();
if (!data.length) return updateResult("Nie znaleziono miasta.");
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);
}
}
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) throw new Error(`Błąd serwera: ${res.status}`);
const data = await res.json();
if (data.error) return updateResult("Błąd serwera: " + data.error);
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`;
}
}
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);
}
}
function drawDailyChart(data) {
const ctx = document.getElementById('dailyChart').getContext('2d');
if (dailyChart) dailyChart.destroy();
dailyChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.time,
datasets: [
{
label: 'Temp. max (°C)',
data: data.temperature_2m_max,
backgroundColor: 'rgba(255, 99, 132, 0.6)'
},
{
label: 'Temp. min (°C)',
data: data.temperature_2m_min,
backgroundColor: 'rgba(54, 162, 235, 0.6)'
},
{
label: 'Opady (mm)',
data: data.precipitation_sum,
type: 'line',
borderColor: 'rgba(0, 128, 0, 0.8)',
fill: false,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
stacked: false,
scales: {
y: {
title: { display: true, text: 'Temperatura (°C)' }
},
y1: {
position: 'right',
title: { display: true, text: 'Opady (mm)' },
grid: { drawOnChartArea: false }
}
}
}
});
}
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>

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<title>Status serwera</title>
<style>
body {
font-family: sans-serif;
background: #e0f2f1;
color: #004d40;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.box {
background: white;
padding: 2em;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
text-align: center;
}
.error {
color: red;
}
</style>
</head>
<body>
<div class="box" id="statusBox">
<h1>🟡 Ładowanie...</h1>
<p>Trwa sprawdzanie statusu serwera...</p>
</div>
<script>
fetch("/ping", { headers: { Accept: "application/json" } })
.then(res => res.json())
.then(data => {
const box = document.getElementById("statusBox");
box.innerHTML = `
<h1>✅ Serwer działa</h1>
<p>Status: <strong>${data.status}</strong></p>
`;
})
.catch(err => {
const box = document.getElementById("statusBox");
box.innerHTML = `
<h1 class="error">❌ Błąd</h1>
<p class="error">Nie udało się połączyć z serwerem:<br>${err.message}</p>
`;
});
</script>
</body>
</html>

View File

@ -0,0 +1,460 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Interaktywna mapa pogody kliknij lub wyszukaj miasto, by sprawdzić prognozę.">
<title>Pogoda Web</title>
<link rel="icon" href="/favicon.ico" />
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<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>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #e0f7fa, #e8eaf6);
color: #333;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: linear-gradient(45deg, #2196f3, #0d47a1);
color: white;
padding: 2rem 1rem;
text-align: center;
box-shadow: 0 4px 10px rgba(33, 150, 243, 0.5);
border-radius: 0 0 1rem 1rem;
margin-bottom: 2rem;
}
header h1 {
font-weight: 900;
font-size: 2.8rem;
letter-spacing: 0.05em;
text-shadow: 1px 1px 4px rgba(0,0,0,0.3);
margin-bottom: 0.3rem;
}
header p {
font-size: 1.25rem;
font-weight: 500;
opacity: 0.85;
margin-bottom: 1rem;
text-shadow: 1px 1px 3px rgba(0,0,0,0.2);
}
#quote {
font-style: italic;
font-size: 1.15rem;
margin-top: 0.5rem;
color: #e3f2fd;
text-shadow: 1px 1px 2px #0d47a1;
}
main.container {
flex-grow: 1;
max-width: 1100px;
}
#map {
height: 450px;
border-radius: 1rem;
box-shadow: 0 6px 15px rgba(33, 150, 243, 0.3);
margin-bottom: 2rem;
}
#result {
background: white;
padding: 1.5rem;
border-radius: 1rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
white-space: pre-wrap;
font-size: 1rem;
min-height: 150px;
margin-bottom: 2rem;
color: #1a237e;
font-weight: 600;
}
canvas {
background: white;
padding: 1rem;
border-radius: 1rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
margin-bottom: 3rem;
max-height: 350px;
width: 100% !important;
}
/* Dostosowanie input i button w kontrolkach */
#controls .form-control {
font-size: 1.1rem;
min-width: 200px;
border-radius: 0.75rem;
box-shadow: inset 0 2px 6px rgba(33, 150, 243, 0.15);
border: 1.5px solid #2196f3;
transition: border-color 0.3s ease;
}
#controls .form-control:focus {
border-color: #0d47a1;
box-shadow: 0 0 8px #0d47a1;
}
#controls .btn-primary {
font-size: 1.1rem;
padding: 0.65rem 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 6px 15px rgba(33, 150, 243, 0.4);
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
#controls .btn-primary:hover,
#controls .btn-primary:focus {
background-color: #0d47a1;
box-shadow: 0 8px 20px rgba(13, 71, 161, 0.7);
}
</style>
</head>
<body>
<header>
<h1>🌦️ Pogoda Web</h1>
<p>Kliknij na mapie lub wyszukaj miasto, aby zobaczyć prognozę pogody</p>
<div id="quote"></div>
</header>
<main class="container">
<div id="controls" class="d-flex flex-wrap gap-3 mb-4">
<input
type="text"
id="cityInput"
class="form-control flex-grow-1"
placeholder="Wpisz miasto"
autocomplete="off"
aria-label="Wpisz nazwę miasta do wyszukania"
/>
<button
type="button"
onclick="searchCity()"
class="btn btn-primary"
aria-label="Szukaj miasta"
>
Szukaj
</button>
</div>
<div id="map" title="Mapa pogodowa" aria-label="Mapa pogodowa z możliwością kliknięcia"></div>
<div id="result" aria-live="polite" aria-atomic="true">Kliknij na mapie lub wyszukaj miasto, aby zobaczyć pogodę.</div>
<canvas id="dailyChart" aria-label="Wykres prognozy dziennej"></canvas>
<canvas id="hourlyChart" aria-label="Wykres prognozy godzinowej"></canvas>
</main>
<script src="https://unpkg.com/leaflet/dist/leaflet.js" loading="lazy"></script>
<!-- Bootstrap Bundle JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
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.",
" 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."
];
function getRandomQuote() {
const index = Math.floor(Math.random() * weatherQuotes.length);
return weatherQuotes[index];
}
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);
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];
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);
throw error;
}
}
async function fetchPlaceName(lat, lon) {
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`;
const response = await fetchWithTimeout(url, {
headers: { 'User-Agent': 'PogodaApp/1.0' }
});
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...");
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);
}
});
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.");
updateResult("Wyszukiwanie miasta...");
try {
const res = await fetchWithTimeout(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(city)}`);
if (!res.ok) throw new Error('Błąd podczas wyszukiwania miasta.');
const data = await res.json();
if (!data.length) return updateResult("Nie znaleziono miasta.");
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);
}
}
let dailyChart = null;
let hourlyChart = null;
async function fetchWeather(lat, lon) {
try {
const res = await fetchWithTimeout('/get_weather', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ lat, lon })
});
if (!res.ok) throw new Error(`Błąd serwera: ${res.status}`);
const data = await res.json();
if (data.error) return updateResult("Błąd serwera: " + data.error);
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`;
}
}
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);
}
}
function drawDailyChart(data) {
const ctx = document.getElementById('dailyChart').getContext('2d');
if (dailyChart) dailyChart.destroy();
dailyChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.time,
datasets: [
{
label: 'Temp. max (°C)',
data: data.temperature_2m_max,
backgroundColor: 'rgba(255, 99, 132, 0.6)'
},
{
label: 'Temp. min (°C)',
data: data.temperature_2m_min,
backgroundColor: 'rgba(54, 162, 235, 0.6)'
},
{
label: 'Opady (mm)',
data: data.precipitation_sum,
type: 'line',
borderColor: 'rgba(0, 128, 0, 0.8)',
fill: false,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
stacked: false,
scales: {
y: {
title: { display: true, text: 'Temperatura (°C)' }
},
y1: {
position: 'right',
title: { display: true, text: 'Opady (mm)' },
grid: { drawOnChartArea: false }
}
}
}
});
}
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>

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pogoda</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: #e8f0fe;
margin: 0;
padding: 2rem;
display: flex;
justify-content: center;
}
.card {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
max-width: 400px;
width: 100%;
}
h1 {
margin-top: 0;
font-size: 1.8rem;
color: #333;
}
.info {
margin: 1rem 0;
font-size: 1.2rem;
color: #555;
}
.label {
font-weight: bold;
}
</style>
</head>
<body>
<div class="card">
<h1>Pogoda w <span id="city">...</span></h1>
<div class="info"><span class="label">Temperatura:</span> <span id="temp">...</span> °C</div>
<div class="info"><span class="label">Wiatr:</span> <span id="wind">...</span> m/s</div>
<div class="info"><span class="label">Czas pomiaru:</span> <span id="time">...</span></div>
</div>
<script>
// Pobierz ?city=poznan z adresu URL
const urlParams = new URLSearchParams(window.location.search);
const city = urlParams.get("city") || "Warszawa";
fetch(`https://meteo.cbpio.pl/weather?city=${encodeURIComponent(city)}`)
.then(res => res.json())
.then(data => {
document.getElementById("city").textContent = data.city;
document.getElementById("temp").textContent = data.temperature.toFixed(1);
document.getElementById("wind").textContent = data.windspeed.toFixed(1);
document.getElementById("time").textContent = new Date(data.time).toLocaleString("pl-PL");
})
.catch(err => {
document.querySelector(".card").innerHTML = "<p style='color: red;'>Błąd ładowania danych pogodowych.</p>";
console.error(err);
});
</script>
</body>
</html>

View File

@ -0,0 +1 @@
/home/jakub/services/meteo/server/templates/