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
|
.env
|
||||||
.vs/
|
.vs/
|
||||||
.venv/
|
.venv/
|
||||||
|
schedule_data.json
|
||||||
29
server.py
29
server.py
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
body {
|
body {
|
||||||
background-color: #000000;
|
background-color: #e300eb;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
@@ -38,4 +38,25 @@ a:hover {
|
|||||||
.mike-section p {
|
.mike-section p {
|
||||||
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);
|
||||||
}
|
}
|
||||||
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>
|
<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
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