generated from nathanwoodburn/python-webserver-template
feat: Add initial site
All checks were successful
Build Docker / BuildImage (push) Successful in 2m7s
All checks were successful
Build Docker / BuildImage (push) Successful in 2m7s
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ __pycache__/
|
||||
.env
|
||||
.vs/
|
||||
.venv/
|
||||
schedule_data.json
|
||||
29
server.py
29
server.py
@@ -18,6 +18,22 @@ import 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__)
|
||||
|
||||
|
||||
@@ -74,9 +90,7 @@ def wellknown(path):
|
||||
# region Main routes
|
||||
@app.route("/")
|
||||
def index():
|
||||
# Get current time in the format "dd MMM YYYY hh:mm AM/PM"
|
||||
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
||||
return render_template("index.html", datetime=current_datetime)
|
||||
return render_template("schedule.html", schedule=SCHEDULE_DATA)
|
||||
|
||||
|
||||
@app.route("/<path:path>")
|
||||
@@ -126,6 +140,15 @@ def api_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
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
body {
|
||||
background-color: #000000;
|
||||
background-color: #e300eb;
|
||||
color: #ffffff;
|
||||
}
|
||||
h1 {
|
||||
@@ -38,4 +38,25 @@ a:hover {
|
||||
.mike-section p {
|
||||
line-height: 1.6;
|
||||
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);
|
||||
}
|
||||
418
templates/assets/css/schedule.css
Normal file
418
templates/assets/css/schedule.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -12,47 +12,17 @@
|
||||
<body>
|
||||
<div class="spacer"></div>
|
||||
<div class="centre">
|
||||
<h1>Nathan.Woodburn/</h1>
|
||||
<span>The current date and time is {{datetime}}</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<h1>Weekly Schedule</h1>
|
||||
<span>Current date and time: {{datetime}}</span>
|
||||
<br><br>
|
||||
<a href="/schedule" class="schedule-link">View Weekly Schedule</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fetchData() {
|
||||
// Fetch the data from the server
|
||||
fetch('/api/v1/data')
|
||||
.then(response => response.json())
|
||||
.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);
|
||||
// Redirect to schedule after 2 seconds
|
||||
setTimeout(() => {
|
||||
window.location.href = '/schedule';
|
||||
}, 2000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
108
templates/schedule.html
Normal file
108
templates/schedule.html
Normal 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>
|
||||
Reference in New Issue
Block a user