feat: Add intial site

This commit is contained in:
Nathan Woodburn 2025-03-06 15:15:24 +11:00
parent 66620204ac
commit 6d7019bba5
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
6 changed files with 284 additions and 5 deletions

View File

@ -1,3 +1,25 @@
# python-webserver-template # python-webserver-template
Python3 website template including git actions Python3 website template including git actions
## Example `curl` Commands
### Convert Specific Time Between Two Timezones
```sh
curl -X POST http://localhost:5000/api/v1/convert -H "Content-Type: application/json" -d '{
"from_tz": "Australia/Sydney",
"to_tz": "UTC",
"time": "2023-10-10T15:00:00"
}'
```
### Convert Specific Time Between Multiple Timezones
```sh
curl -X POST http://localhost:5000/api/v1/convert_multiple -H "Content-Type: application/json" -d '{
"from_tz": "Australia/Sydney",
"time": "2023-10-10T15:00:00",
"to_tzs": ["UTC", "America/New_York", "Europe/London"]
}'
```

View File

@ -1,4 +1,5 @@
flask flask
gunicorn gunicorn
requests requests
python-dotenv python-dotenv
pytz

118
server.py
View File

@ -13,8 +13,12 @@ from flask import (
import os import os
import json import json
import requests import requests
from datetime import datetime from datetime import datetime, timedelta
import dotenv import dotenv
from pytz import timezone
import pytz
import re
dotenv.load_dotenv() dotenv.load_dotenv()
@ -76,6 +80,118 @@ def wellknown(path):
def index(): def index():
return render_template("index.html") return render_template("index.html")
# Mapping of short timezone names to full names
SHORT_TZ_MAP = {
"GMT": "Etc/GMT",
"PST": "America/Los_Angeles",
"PDT": "America/Los_Angeles",
"MST": "America/Denver",
"MDT": "America/Denver",
"CST": "America/Chicago",
"CDT": "America/Chicago",
"EST": "America/New_York",
"EDT": "America/New_York",
"AKST": "America/Anchorage",
"AKDT": "America/Anchorage",
"HST": "Pacific/Honolulu",
"HDT": "Pacific/Honolulu",
"AST": "America/Puerto_Rico",
"NST": "America/St_Johns",
"BST": "Europe/London",
"CET": "Europe/Paris",
"CEST": "Europe/Paris",
"EET": "Europe/Athens",
"EEST": "Europe/Athens",
"MSK": "Europe/Moscow",
"MSD": "Europe/Moscow",
"IST": "Asia/Kolkata",
"PKT": "Asia/Karachi",
"WIB": "Asia/Jakarta",
"WITA": "Asia/Makassar",
"WIT": "Asia/Jayapura",
"CST": "Asia/Shanghai",
"JST": "Asia/Tokyo",
"KST": "Asia/Seoul",
"AEDT": "Australia/Sydney",
"AEST": "Australia/Sydney",
"ACST": "Australia/Adelaide",
"ACDT": "Australia/Adelaide",
"AWST": "Australia/Perth",
}
def get_full_timezone(tz):
match = re.match(r"^UTC([+-]\d{1,2}):?(\d{2})?$", tz)
if match:
hours = int(match.group(1))
minutes = int(match.group(2)) if match.group(2) else 0
return pytz.FixedOffset(hours * 60 + minutes)
full_tz = SHORT_TZ_MAP.get(tz, tz)
return pytz.timezone(full_tz) if isinstance(full_tz, str) else None
@app.route("/api/v1/convert", methods=["POST"])
def convert():
data = request.json
from_tz = get_full_timezone(data.get("from_tz").replace("_", "/"))
to_tz = get_full_timezone(data.get("to_tz").replace("_", "/"))
time = data.get("time")
if from_tz is None or to_tz is None:
return jsonify({"error": "Invalid timezone"}), 400
# Parse the input time
try:
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S")
except ValueError:
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M")
try:
# Get the timezone
from_tz_time = from_tz.localize(input_time)
# Convert the time
to_tz_time = from_tz_time.astimezone(to_tz)
except Exception as e:
return jsonify({"error": str(e)}), 400
return jsonify(
{
"from": from_tz_time.strftime("%Y-%m-%d %H:%M:%S %Z"),
"to": to_tz_time.strftime("%Y-%m-%d %H:%M:%S %Z"),
}
)
@app.route("/api/v1/convert_multiple", methods=["POST"])
def convert_multiple():
data = request.json
from_tz = get_full_timezone(data.get("from_tz").replace("_", "/"))
time = data.get("time")
to_tz_list = [(tz, get_full_timezone(tz.replace("_", "/"))) for tz in data.get("to_tzs") if len(tz) > 0]
if from_tz is None or any(tz[1] is None for tz in to_tz_list):
return jsonify({"error": "Invalid timezone"}), 400
# Parse the input time
try:
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S")
except ValueError:
input_time = datetime.strptime(time, "%Y-%m-%dT%H:%M")
try:
# Get the timezone
from_tz_time = from_tz.localize(input_time)
# Convert the time to each timezone in the list
results = {}
for original_tz, tz in to_tz_list:
to_tz_time = from_tz_time.astimezone(tz)
offset = to_tz_time.strftime('%z')
offset_formatted = f"UTC{offset[:3]}:{offset[3:]}" if offset else ""
tz_name = to_tz_time.tzname() if to_tz_time.tzname() else offset_formatted
results[original_tz] = f"{to_tz_time.strftime('%Y-%m-%d %H:%M:%S')} {tz_name}"
except Exception as e:
return jsonify({"error": str(e)}), 400
return jsonify(results)
@app.route("/<path:path>") @app.route("/<path:path>")
def catch_all(path: str): def catch_all(path: str):

View File

@ -11,6 +11,52 @@ h1 {
margin-top: 10%; margin-top: 10%;
text-align: center; text-align: center;
} }
form {
margin: 20px 0;
}
label {
display: block;
margin: 10px 0 5px;
}
input {
padding: 5px;
width: 80%;
max-width: 300px;
}
button {
margin-top: 10px;
padding: 10px 20px;
background-color: #ffffff;
color: #000000;
border: none;
cursor: pointer;
}
button:hover {
background-color: #dddddd;
}
#results {
margin-top: 20px;
text-align: left;
}
.results-container {
margin-top: 20px;
text-align: center;
}
.results-table {
width: 80%;
margin: 0 auto;
border-collapse: collapse;
}
.results-table th, .results-table td {
border: 1px solid #ffffff;
padding: 10px;
}
.results-table th {
background-color: #333333;
}
.results-table td {
background-color: #444444;
}
a { a {
color: #ffffff; color: #ffffff;
text-decoration: none; text-decoration: none;

View File

@ -0,0 +1,80 @@
document.getElementById('time-converter-form').addEventListener('submit', function(event) {
event.preventDefault();
const fromTz = document.getElementById('from-tz').value;
const time = document.getElementById('time').value;
const toTzs = document.getElementById('to-tzs').value.split(',');
// Update URL with query parameters
const params = new URLSearchParams({
from_tz: fromTz,
time: time,
to_tzs: toTzs.join(',')
});
window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);
fetch('/api/v1/convert_multiple', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
from_tz: fromTz,
time: time,
to_tzs: toTzs
})
})
.then(response => response.json())
.then(data => {
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = '<h2>Converted Times:</h2>';
const table = document.createElement('table');
table.classList.add('results-table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
const headerRow = document.createElement('tr');
const th1 = document.createElement('th');
th1.textContent = 'Timezone';
const th2 = document.createElement('th');
th2.textContent = 'Converted Time';
headerRow.appendChild(th1);
headerRow.appendChild(th2);
thead.appendChild(headerRow);
for (const [tz, convertedTime] of Object.entries(data)) {
const row = document.createElement('tr');
const cell1 = document.createElement('td');
cell1.textContent = tz;
const cell2 = document.createElement('td');
cell2.textContent = convertedTime;
row.appendChild(cell1);
row.appendChild(cell2);
tbody.appendChild(row);
}
table.appendChild(thead);
table.appendChild(tbody);
resultsDiv.appendChild(table);
})
.catch(error => {
console.error('Error:', error);
});
});
// Load parameters from URL if available
window.addEventListener('load', function() {
const params = new URLSearchParams(window.location.search);
const fromTz = params.get('from_tz');
const time = params.get('time');
const toTzs = params.get('to_tzs');
if (fromTz && time && toTzs) {
document.getElementById('from-tz').value = fromTz;
document.getElementById('time').value = time;
document.getElementById('to-tzs').value = toTzs;
// Trigger form submission to display results
document.getElementById('time-converter-form').dispatchEvent(new Event('submit'));
}
});

View File

@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nathan.Woodburn/</title> <title>Time Converter</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png"> <link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="stylesheet" href="/assets/css/index.css"> <link rel="stylesheet" href="/assets/css/index.css">
</head> </head>
@ -12,8 +12,22 @@
<body> <body>
<div class="spacer"></div> <div class="spacer"></div>
<div class="centre"> <div class="centre">
<h1>Nathan.Woodburn/</h1> <h1>Time Converter</h1>
<form id="time-converter-form">
<label for="from-tz">From Timezone:</label>
<input type="text" id="from-tz" name="from-tz" required>
<br>
<label for="time">Time:</label>
<input type="datetime-local" id="time" name="time" required>
<br>
<label for="to-tzs">To Timezones (comma separated):</label>
<input type="text" id="to-tzs" name="to-tzs" required>
<br>
<button type="submit">Convert</button>
</form>
<div id="results" class="results-container"></div>
</div> </div>
<script src="/assets/js/index.js"></script>
</body> </body>
</html> </html>