From 6d7019bba516aec447655317138e9229288ed031 Mon Sep 17 00:00:00 2001
From: Nathan Woodburn <github@nathan.woodburn.au>
Date: Thu, 6 Mar 2025 15:15:24 +1100
Subject: [PATCH] feat: Add intial site

---
 README.md                      |  24 ++++++-
 requirements.txt               |   3 +-
 server.py                      | 118 ++++++++++++++++++++++++++++++++-
 templates/assets/css/index.css |  46 +++++++++++++
 templates/assets/js/index.js   |  80 ++++++++++++++++++++++
 templates/index.html           |  18 ++++-
 6 files changed, 284 insertions(+), 5 deletions(-)
 create mode 100644 templates/assets/js/index.js

diff --git a/README.md b/README.md
index 2f77bf4..cc42c69 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,25 @@
 # python-webserver-template
 
-Python3 website template including git actions
\ No newline at end of file
+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"]
+}'
+```
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 8cf7fcb..83d7719 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
 flask
 gunicorn
 requests
-python-dotenv
\ No newline at end of file
+python-dotenv
+pytz
\ No newline at end of file
diff --git a/server.py b/server.py
index 240a1a8..0e3148e 100644
--- a/server.py
+++ b/server.py
@@ -13,8 +13,12 @@ from flask import (
 import os
 import json
 import requests
-from datetime import datetime
+from datetime import datetime, timedelta
 import dotenv
+from pytz import timezone
+import pytz
+import re
+
 
 dotenv.load_dotenv()
 
@@ -76,6 +80,118 @@ def wellknown(path):
 def index():
     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>")
 def catch_all(path: str):
diff --git a/templates/assets/css/index.css b/templates/assets/css/index.css
index 9635f9c..23a89c3 100644
--- a/templates/assets/css/index.css
+++ b/templates/assets/css/index.css
@@ -11,6 +11,52 @@ h1 {
     margin-top: 10%;
     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 {
     color: #ffffff;
     text-decoration: none;
diff --git a/templates/assets/js/index.js b/templates/assets/js/index.js
new file mode 100644
index 0000000..14aae47
--- /dev/null
+++ b/templates/assets/js/index.js
@@ -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'));
+    }
+});
diff --git a/templates/index.html b/templates/index.html
index bb349f8..f24bc94 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -4,7 +4,7 @@
 <head>
     <meta charset="UTF-8">
     <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="stylesheet" href="/assets/css/index.css">
 </head>
@@ -12,8 +12,22 @@
 <body>
     <div class="spacer"></div>
     <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>
+    <script src="/assets/js/index.js"></script>
 </body>
 
 </html>
\ No newline at end of file