feat: Add initial site
All checks were successful
Build Docker / BuildImage (push) Successful in 2m7s

This commit is contained in:
2025-09-29 21:10:09 +10:00
parent 04772d7156
commit 50331bd7a3
6 changed files with 583 additions and 42 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ __pycache__/
.env .env
.vs/ .vs/
.venv/ .venv/
schedule_data.json

View File

@@ -18,6 +18,22 @@ import dotenv
dotenv.load_dotenv() dotenv.load_dotenv()
def load_schedule_data():
"""Load schedule data from JSON file"""
try:
with open('schedule_data.json', 'r') as f:
data = json.load(f)
return data.get('schedule', [])
except FileNotFoundError:
print("Warning: schedule_data.json not found. Using empty schedule.")
return []
except json.JSONDecodeError:
print("Warning: Invalid JSON in schedule_data.json. Using empty schedule.")
return []
# Load schedule data from JSON file
SCHEDULE_DATA = load_schedule_data()
app = Flask(__name__) app = Flask(__name__)
@@ -74,9 +90,7 @@ def wellknown(path):
# region Main routes # region Main routes
@app.route("/") @app.route("/")
def index(): def index():
# Get current time in the format "dd MMM YYYY hh:mm AM/PM" return render_template("schedule.html", schedule=SCHEDULE_DATA)
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
return render_template("index.html", datetime=current_datetime)
@app.route("/<path:path>") @app.route("/<path:path>")
@@ -126,6 +140,15 @@ def api_data():
return jsonify(data) return jsonify(data)
@app.route("/api/v1/schedule", methods=["GET"])
def api_schedule():
"""
API endpoint that returns the weekly schedule data.
"""
# Reload data in case file has been updated
current_schedule = load_schedule_data()
return jsonify({"schedule": current_schedule})
# endregion # endregion

View File

@@ -1,5 +1,5 @@
body { body {
background-color: #000000; background-color: #e300eb;
color: #ffffff; color: #ffffff;
} }
h1 { h1 {
@@ -39,3 +39,24 @@ a:hover {
line-height: 1.6; line-height: 1.6;
margin-bottom: 15px; margin-bottom: 15px;
} }
/* Schedule link styling */
.schedule-link {
display: inline-block;
padding: 12px 24px;
background-color: rgba(70, 70, 70, 0.8);
border: 2px solid #888;
border-radius: 8px;
color: #ffffff;
text-decoration: none;
font-size: 18px;
font-weight: 600;
transition: all 0.3s ease;
}
.schedule-link:hover {
background-color: rgba(100, 100, 100, 0.9);
border-color: #aaa;
text-decoration: none;
transform: translateY(-2px);
}

View File

@@ -0,0 +1,418 @@
/* Theme Variables */
:root {
/* Dark Theme (Default) */
--bg-color: #000000;
--text-color: #ffffff;
--container-bg: rgba(30, 30, 30, 0.8);
--table-header-bg: rgba(70, 70, 70, 0.8);
--table-border: #444;
--table-header-border: #555;
--row-hover-bg: rgba(50, 50, 50, 0.5);
--special-event-bg: rgba(100, 50, 150, 0.2);
--special-event-hover: rgba(100, 50, 150, 0.3);
--date-color: #e0e0e0;
--leader-color: #b0b0b0;
--topic-color: #d0d0d0;
--empty-leader-color: #666;
--select-bg: rgba(50, 50, 50, 0.8);
--select-border: #666;
}
[data-theme="pink"] {
--bg-color: #1a0d14;
--text-color: #f8e8f0;
--container-bg: rgba(60, 30, 45, 0.8);
--table-header-bg: rgba(120, 60, 90, 0.8);
--table-border: #8b4a6b;
--table-header-border: #a55577;
--row-hover-bg: rgba(80, 40, 60, 0.5);
--special-event-bg: rgba(180, 100, 150, 0.3);
--special-event-hover: rgba(180, 100, 150, 0.4);
--date-color: #f0c8d8;
--leader-color: #d8a8c0;
--topic-color: #e8c8d8;
--empty-leader-color: #996677;
--select-bg: rgba(80, 40, 60, 0.8);
--select-border: #a55577;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
margin: 0;
padding: 0;
line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
font-size: 48px;
margin: 0;
padding: 20px 0;
color: var(--text-color);
font-weight: 300;
}
.theme-switcher {
display: flex;
align-items: center;
gap: 15px;
font-size: 14px;
}
.theme-label {
color: var(--text-color);
font-weight: 500;
margin-right: 5px;
}
.theme-buttons {
display: flex;
gap: 8px;
background-color: var(--container-bg);
padding: 4px;
border-radius: 12px;
border: 1px solid var(--table-border);
}
.theme-btn {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: none;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
color: var(--leader-color);
font-size: 13px;
font-weight: 500;
position: relative;
overflow: hidden;
}
.theme-btn:hover {
background-color: var(--row-hover-bg);
transform: translateY(-1px);
}
.theme-btn.active {
background-color: var(--special-event-bg);
color: var(--text-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.theme-btn.active[data-theme="dark"] {
background: linear-gradient(135deg, rgba(100, 50, 150, 0.3), rgba(50, 50, 100, 0.3));
}
.theme-btn.active[data-theme="pink"] {
background: linear-gradient(135deg, rgba(255, 192, 203, 0.3), rgba(219, 112, 147, 0.3));
}
.theme-icon {
font-size: 16px;
transition: transform 0.3s ease;
}
.theme-btn:hover .theme-icon {
transform: scale(1.1);
}
.theme-btn.active .theme-icon {
transform: scale(1.05);
}
.theme-name {
font-family: inherit;
letter-spacing: 0.5px;
}
.schedule-container {
background-color: var(--container-bg);
border-radius: 12px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
overflow-x: auto;
transition: background-color 0.3s ease;
}
.schedule-table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
font-size: 16px;
}
.schedule-table th {
background-color: var(--table-header-bg);
color: var(--text-color);
padding: 16px 12px;
text-align: left;
font-weight: 600;
border-bottom: 3px solid var(--table-header-border);
position: sticky;
top: 0;
transition: background-color 0.3s ease;
}
.schedule-table td {
padding: 14px 12px;
border-bottom: 1px solid var(--table-border);
vertical-align: top;
transition: border-color 0.3s ease;
}
.schedule-table tr:hover {
background-color: var(--row-hover-bg);
transition: background-color 0.2s ease;
}
.special-event {
background-color: var(--special-event-bg);
transition: background-color 0.3s ease;
}
.special-event:hover {
background-color: var(--special-event-hover);
}
.date-cell {
font-weight: 600;
color: var(--date-color);
min-width: 120px;
transition: color 0.3s ease;
}
.leader-cell {
color: var(--leader-color);
min-width: 140px;
transition: color 0.3s ease;
}
.topic-cell {
color: var(--topic-color);
max-width: 400px;
word-wrap: break-word;
transition: color 0.3s ease;
}
/* Special styling for empty leader cells */
.leader-cell:contains("-") {
color: #666;
font-style: italic;
}
/* Remove old select styles */
.theme-switcher select {
display: none;
}
.desktop-only {
display: flex;
}
.mobile-only {
display: none;
}
/* Responsive design */
@media (max-width: 768px) {
.container {
padding: 10px 5px;
}
.header-content {
flex-direction: column;
text-align: center;
gap: 15px;
}
.desktop-only {
display: none;
}
.mobile-only {
display: flex;
justify-content: center;
margin-top: 20px;
padding: 15px 0;
border-top: 1px solid var(--table-border);
}
h1 {
font-size: 28px;
padding: 10px 0;
}
.theme-switcher {
justify-content: center;
}
.theme-buttons {
background-color: var(--container-bg);
}
.schedule-container {
padding: 15px 10px;
margin: 0;
border-radius: 8px;
}
.schedule-table {
font-size: 13px;
width: 100%;
min-width: 100%;
}
.schedule-table th,
.schedule-table td {
padding: 8px 6px;
word-wrap: break-word;
overflow-wrap: break-word;
}
.date-cell,
.leader-cell {
min-width: auto;
font-size: 12px;
}
.date-cell {
width: 20%;
font-size: 11px;
font-weight: 700;
}
.leader-cell {
width: 18%;
font-size: 10px;
}
.topic-cell {
width: 44%;
font-size: 12px;
line-height: 1.3;
}
}
@media (max-width: 600px) {
.container {
padding: 8px 3px;
}
h1 {
font-size: 24px;
}
.schedule-container {
padding: 12px 5px;
border-radius: 6px;
}
.schedule-table {
font-size: 11px;
}
.schedule-table th,
.schedule-table td {
padding: 6px 4px;
}
/* Stack layout for very small screens */
.schedule-table th:nth-child(2),
.schedule-table td:nth-child(2),
.schedule-table th:nth-child(3),
.schedule-table td:nth-child(3) {
display: none;
}
.date-cell {
width: 30%;
font-size: 12px;
}
.topic-cell {
width: 70%;
font-size: 11px;
position: relative;
}
.topic-cell::before {
content: attr(data-leaders);
display: block;
font-size: 9px;
color: var(--empty-leader-color);
margin-bottom: 3px;
font-style: italic;
}
}
@media (max-width: 480px) {
.mobile-only .theme-switcher {
flex-direction: row;
gap: 8px;
}
.mobile-only .theme-label {
margin-right: 0;
font-size: 12px;
}
.mobile-only .theme-btn .theme-name {
display: none;
}
.mobile-only .theme-btn {
padding: 8px;
min-width: 40px;
justify-content: center;
}
.mobile-only .theme-icon {
font-size: 16px;
}
.schedule-container {
padding: 10px 3px;
}
.schedule-table {
font-size: 10px;
}
.schedule-table th,
.schedule-table td {
padding: 5px 3px;
}
.date-cell {
font-size: 11px;
}
.topic-cell {
font-size: 10px;
line-height: 1.2;
}
}

View File

@@ -12,47 +12,17 @@
<body> <body>
<div class="spacer"></div> <div class="spacer"></div>
<div class="centre"> <div class="centre">
<h1>Nathan.Woodburn/</h1> <h1>Weekly Schedule</h1>
<span>The current date and time is {{datetime}}</span> <span>Current date and time: {{datetime}}</span>
</div> <br><br>
<a href="/schedule" class="schedule-link">View Weekly Schedule</a>
<div class="spacer"></div>
<div class="centre">
<h2 id="test-content-header">Pulling data</h2>
<span class="test-content">This is a test content area that will be updated with data from the server.</span>
<br>
<br>
<span class="test-content-timestamp">Timestamp: Waiting to pull data</span>
</div> </div>
<script> <script>
function fetchData() { // Redirect to schedule after 2 seconds
// Fetch the data from the server setTimeout(() => {
fetch('/api/v1/data') window.location.href = '/schedule';
.then(response => response.json()) }, 2000);
.then(data => {
// Get the data header element
const dataHeader = document.getElementById('test-content-header');
// Update the header with the fetched data
dataHeader.textContent = data.header;
// Get the test content element
const testContent = document.querySelector('.test-content');
// Update the content with the fetched data
testContent.textContent = data.content;
// Get the timestamp element
const timestampElement = document.querySelector('.test-content-timestamp');
// Update the timestamp with the fetched data
timestampElement.textContent = `Timestamp: ${data.timestamp}`;
})
.catch(error => console.error('Error fetching data:', error));
}
// Initial fetch after 2 seconds
setTimeout(fetchData, 2000);
// Then fetch every 2 seconds
setInterval(fetchData, 2000);
</script> </script>
</body> </body>

108
templates/schedule.html Normal file
View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weekly Schedule</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="stylesheet" href="/assets/css/schedule.css">
</head>
<body data-theme="dark">
<div class="container">
<header>
<div class="header-content">
<h1>Weekly Schedule</h1>
<div class="theme-switcher desktop-only">
<span class="theme-label">Theme:</span>
<div class="theme-buttons">
<button class="theme-btn active" data-theme="dark" title="Dark Theme">
<span class="theme-icon">🌙</span>
<span class="theme-name">Dark</span>
</button>
<button class="theme-btn" data-theme="pink" title="Pink Theme">
<span class="theme-icon">🌸</span>
<span class="theme-name">Pink</span>
</button>
</div>
</div>
</div>
</header>
<main>
<div class="schedule-container">
<table class="schedule-table">
<thead>
<tr>
<th>Date</th>
<th>Primary Leader</th>
<th>Secondary Leader</th>
<th>Topic</th>
</tr>
</thead>
<tbody>
{% for item in schedule %}
<tr class="{% if not item.primary_leader and not item.secondary_leader %}special-event{% endif %}">
<td class="date-cell">{{ item.date }}</td>
<td class="leader-cell">{{ item.primary_leader if item.primary_leader else "" }}</td>
<td class="leader-cell">{{ item.secondary_leader if item.secondary_leader else "" }}</td>
<td class="topic-cell" data-leaders="{% if item.primary_leader or item.secondary_leader %}Leaders: {{ item.primary_leader or '-' }}, {{ item.secondary_leader or '-' }}{% endif %}">{{ item.topic }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</main>
<div class="theme-switcher mobile-only">
<span class="theme-label">Theme:</span>
<div class="theme-buttons">
<button class="theme-btn active" data-theme="dark" title="Dark Theme">
<span class="theme-icon">🌙</span>
<span class="theme-name">Dark</span>
</button>
<button class="theme-btn" data-theme="pink" title="Pink Theme">
<span class="theme-icon">🌸</span>
<span class="theme-name">Pink</span>
</button>
</div>
</div>
</div>
<script>
// Theme switcher functionality
const themeButtons = document.querySelectorAll('.theme-btn');
const body = document.body;
// Load saved theme or default to dark
const savedTheme = localStorage.getItem('theme') || 'dark';
body.setAttribute('data-theme', savedTheme);
// Update active button for both switchers
themeButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.theme === savedTheme);
});
// Handle theme changes
themeButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
const selectedTheme = btn.dataset.theme;
// Update body theme
body.setAttribute('data-theme', selectedTheme);
// Update active states for both switchers
themeButtons.forEach(b => b.classList.remove('active'));
document.querySelectorAll(`[data-theme="${selectedTheme}"]`).forEach(b => {
b.classList.add('active');
});
// Save to localStorage
localStorage.setItem('theme', selectedTheme);
});
});
</script>
</body>
</html>