Initial commit

This commit is contained in:
2025-12-10 22:47:38 +01:00
parent e98d8c5c1b
commit f78c4d389d
2870 changed files with 641720 additions and 0 deletions

27
templates/add_board.html Normal file
View File

@@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block title %}Nieuw Liturgiebord Toevoegen - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h2 class="text-3xl font-semibold mb-6">Nieuw Liturgiebord Toevoegen</h2>
<form method="post" class="space-y-6 max-w-xl">
<div>
<label for="name" class="block mb-3 font-semibold text-gray-700">Naam van het bord</label>
<input type="text" id="name" name="name" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div class="flex gap-4">
<button type="submit" class="bg-[#f7d91a] text-black px-8 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Toevoegen</button>
<a href="{{ url_for('portal') }}" class="bg-white text-black border border-black px-8 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition">Annuleren</a>
</div>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,107 @@
{% extends 'base.html' %}
{% block title %}Admin Dashboard - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<section>
<h2 class="text-3xl font-semibold mb-6">Admin Dashboard</h2>
<form method="POST" class="space-y-6">
<div>
<h3 class="text-2xl font-semibold mb-4">Kerken</h3>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Kerknaam</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Actief</th>
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">URL</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for church in churches %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ church.name }}</td>
<td class="text-center px-6 py-4">
<input type="checkbox" name="church_active_{{ church.id }}" {% if church.is_active %}checked{% endif %} class="h-5 w-5 text-blue-600">
</td>
<td class="px-6 py-4 text-sm text-blue-600">
{% if church.is_active %}
{% for board in church.boards %}
<div class="mb-1 break-all"><a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" target="_blank" class="underline hover:text-blue-800 transition">{{ url_for('display_board_unique', unique_id=board.unique_id, _external=True) }}</a></div>
{% endfor %}
{% endif %}
</td>
<td class="text-center px-6 py-4">
<a href="#" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Weergeven</a>
<form action="{{ url_for('admin_delete_church', church_id=church.id) }}" method="post" style="display:inline;margin-left:8px;" onsubmit="return confirm('Weet je zeker dat je deze kerk en alle bijbehorende data wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div>
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
</div>
</form>
</section>
<section>
<h3 class="text-2xl font-semibold mb-4">Gebruikers</h3>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Kerk</th>
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Admin</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Impersonalisatie</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for user in users %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ user.username }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ user.church.name }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{% if user.is_admin %}Ja{% else %}Nee{% endif %}</td>
<td class="text-center px-6 py-4">
{% if not user.is_admin %}
<a href="{{ url_for('admin_impersonate', user_id=user.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Impersonate</a>
{% else %}-{% endif %}
</td>
<td class="text-center px-6 py-4">
{% if not user.is_admin %}
<form action="{{ url_for('admin_delete_user', user_id=user.id) }}" method="post" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze gebruiker en alle bijbehorende kerkdata wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
</form>
{% else %}-{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% block title %}Admin Login - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-md mx-auto">
<h2 class="text-3xl font-semibold tracking-tight">Admin Login</h2>
<form method="post" class="space-y-6">
<div>
<label for="username" class="block mb-2 font-semibold text-gray-700">Gebruikersnaam</label>
<input type="text" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="password" class="block mb-2 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-blue-600 text-white w-full rounded-md py-3 font-semibold shadow hover:bg-blue-700 transition">Login</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,50 @@
{% extends 'base.html' %}
{% block title %}Gebruikersbeheer - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h2 class="text-3xl font-semibold mb-6">Gebruikersbeheer</h2>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th scope="col" class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Kerk</th>
<th scope="col" class="px-6 py-3 text-sm font-semibold text-gray-700">Admin</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Impersonalisatie</th>
<th scope="col" class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for user in users %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ user.username }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ user.church.name }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{% if user.is_admin %}Ja{% else %}Nee{% endif %}</td>
<td class="text-center px-6 py-4">
{% if not user.is_admin %}
<a href="{{ url_for('admin_impersonate', user_id=user.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Impersonate</a>
{% else %}-{% endif %}
</td>
<td class="text-center px-6 py-4">
{% if not user.is_admin %}
<form action="{{ url_for('admin_delete_user', user_id=user.id) }}" method="post" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze gebruiker en alle bijbehorende kerkdata wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
</form>
{% else %}-{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('admin_dashboard') }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block mt-6">Terug naar dashboard</a>
</div>
{% endblock %}

151
templates/base.html Normal file
View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}PsalmbordOnline{% endblock %}</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Lora&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Quicksand:wght@300..700&display=swap" rel="stylesheet">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
h1, h2, h3, h4, h5, h6 {
font-family: 'Lora', serif;
font-weight: 600;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: 'Quicksand';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Floating scrollbar style for WebKit browsers */
overflow-y: scroll;
}
/* Webkit Floating Scrollbar */
body::-webkit-scrollbar {
width: 8px;
background: transparent;
position: fixed;
}
body::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.3);
border-radius: 4px;
border: 2px solid transparent;
background-clip: content-box;
}
/* Firefox Floating scrollbar */
@-moz-document url-prefix() {
html {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
}
}
</style>
</head>
<body class="bg-white text-gray-900 font-sans min-h-screen flex flex-col">
<nav class="bg-white border-b border-gray-200 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-20">
<!-- Logo -->
<div class="flex-shrink-0 flex items-center w-24">
{% if current_user.is_authenticated and current_user.church and current_user.church.logo_filename %}
<a href="{{ url_for('index') }}"><img src="{{ url_for('uploaded_file', filename=current_user.church.logo_filename) }}" alt="Logo" class="h-10 w-auto object-contain"></a>
{% else %}
<a href="{{ url_for('index') }}"><img src="{{ url_for('static', filename='default_logo.png') }}" alt="Default Logo" class="h-10 w-auto object-contain"></a>
{% endif %}
</div>
<!-- Desktop Nav -->
<div class="hidden sm:flex flex-1 justify-center">
<div id="nav-links" class="flex flex-row gap-2 items-center">
{% if current_user.is_authenticated %}
<a href="/portal" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Mijn schermen</a>
<a href="{{ url_for('global_schedules') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Geplande diensten</a>
<a href="{{ url_for('church_settings') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Instellingen</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin_dashboard') }}" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Admin</a>
{% endif %}
<a href="/logout" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Uitloggen</a>
{% else %}
<a href="/login" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Inloggen</a>
<a href="/register" class="text-black px-6 py-3 rounded font-semibold hover:bg-[#f7d91a] hover:text-black transition">Registreren</a>
{% endif %}
</div>
</div>
<!-- Current Time -->
<div id="current-time" class="text-gray-700 font-semibold text-right text-sm w-24" title="Huidige datum en tijd">
<div id="current-date"></div>
<div id="current-clock" class="mt-0"></div>
</div>
<!-- Mobile hamburger -->
<div class="flex sm:hidden">
<button id="menu-button" class="p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-600" aria-label="Open menu">
<svg class="h-7 w-7 text-gray-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Mobile nav menu -->
<div id="nav-mobile" class="sm:hidden hidden">
<div class="flex flex-col gap-2 py-3 items-center border-t border-gray-100">
{% if current_user.is_authenticated %}
<a href="/portal" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Mijn schermen</a>
<a href="{{ url_for('global_schedules') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Geplande diensten</a>
<a href="{{ url_for('church_settings') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Kerk instellingen</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin_dashboard') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Admin instellingen</a>
{% endif %}
<a href="/logout" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Logout</a>
{% else %}
<a href="/login" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Login</a>
<a href="/register" class="bg-[#f7d91a] text-black px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition w-full text-center">Register</a>
{% endif %}
</div>
</div>
</div>
</nav>
<script>
const menuButton = document.getElementById('menu-button');
const navMobile = document.getElementById('nav-mobile');
menuButton.addEventListener('click', () => {
navMobile.classList.toggle('hidden');
});
function updateTime() {
const now = new Date();
const dateOptions = { year: 'numeric', month: 'short', day: 'numeric' };
const timeOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit' };
const dateStr = now.toLocaleDateString('nl-NL', dateOptions);
const timeStr = now.toLocaleTimeString('nl-NL', timeOptions);
document.getElementById('current-date').textContent = dateStr;
document.getElementById('current-clock').textContent = timeStr;
}
setInterval(updateTime, 1000);
updateTime();
</script>
<main class="flex-grow max-w-7xl mx-auto w-full px-6 py-8 sm:py-12">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">
{% for message in messages %}
<div>{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="bg-gray-100 text-gray-600 text-center py-4 text-sm">
&copy; {{ current_year }} PsalmbordOnline &nbsp;|&nbsp; Contact: <a href="mailto:info@psalmbord.online" class="text-blue-600 hover:underline">info@psalmbord.online</a>
</footer>
</body>
</html>

View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h1 class="text-3xl font-semibold mb-6">Details voor {{ board.name }}</h1>
<!-- You can add more board settings above here as needed -->
<h2 class="text-2xl font-semibold mb-4">Schedules for this display</h2>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Days</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Start Time</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">End Time</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Content</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for s in schedules %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ s.days_of_week }}</td>
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ s.content[:50] }}{% if s.content|length > 50 %}...{% endif %}</td>
<td class="text-center px-6 py-4">
<a href="{{ url_for('edit_schedule', board_id=board.id, schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Edit</a>
<form method="post" action="{{ url_for('delete_schedule', board_id=board.id, schedule_id=s.id) }}" style="display:inline;">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('add_schedule', board_id=board.id) }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition inline-block">Add Schedule</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h1 class="text-3xl font-semibold mb-6">Manage Schedules for {{ board.name }}</h1>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700">Datum</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Starttijd</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Eindtijd</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for s in schedules %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">{{ s.name }}</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ s.date.strftime('%Y-%m-%d') }}</td>
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
<td class="text-center px-6 py-4 text-sm text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
<td class="text-center px-6 py-4">
<a href="{{ url_for('edit_schedule', board_id=board.id, schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Edit</a>
<form action="{{ url_for('delete_schedule', board_id=board.id, schedule_id=s.id) }}" method="POST" style="display:inline;">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('add_schedule', board_id=board.id) }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition inline-block">Add Schedule</a>
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block ml-4">Terug naar bord bewerken</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% extends 'base.html' %}
{% block title %}Wachtwoord wijzigen{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-semibold mb-6">Wijzig Wachtwoord</h2>
{% if msg %}
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">{{ msg }}</div>
{% endif %}
<form method="post" class="space-y-6">
<div>
<label for="current_password" class="block mb-3 font-semibold text-gray-700">Huidig wachtwoord</label>
<input type="password" id="current_password" name="current_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="new_password" class="block mb-3 font-semibold text-gray-700">Nieuw wachtwoord</label>
<input type="password" id="new_password" name="new_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="confirm_password" class="block mb-3 font-semibold text-gray-700">Bevestig nieuw wachtwoord</label>
<input type="password" id="confirm_password" name="confirm_password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,104 @@
{% extends 'base.html' %}
{% block title %}Kerk Instellingen{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
{% if msg %}
<div class="bg-blue-100 border border-blue-400 text-blue-700 px-6 py-4 rounded mb-6">{{ msg }}</div>
{% endif %}
<div class="flex flex-wrap gap-10">
<form method="post" class="flex-1 min-w-[300px] max-w-lg space-y-6" enctype="multipart/form-data">
<input type="hidden" name="form_type" value="contact">
<h3 class="text-2xl font-semibold">Kerkgegevens</h3>
<div>
<label for="name" class="block mb-3 font-semibold text-gray-700">Kerknaam</label>
<input type="text" id="name" name="name" value="{{ church.name }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="contact_email" class="block mb-3 font-semibold text-gray-700">Contact Email</label>
<input type="email" id="contact_email" name="contact_email" value="{{ church.contact_email }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="contact_phone" class="block mb-3 font-semibold text-gray-700">Telefoon</label>
<input type="text" id="contact_phone" name="contact_phone" value="{{ church.contact_phone }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="contact_address" class="block mb-3 font-semibold text-gray-700">Adres</label>
<input type="text" id="contact_address" name="contact_address" value="{{ church.contact_address }}" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="logo" class="block mb-3 font-semibold text-gray-700">Logo uploaden</label>
<input type="file" id="logo" name="logo" accept="image/png, image/jpeg, image/jpg, image/gif" class="w-full border border-gray-300 rounded-md p-4">
</div>
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Opslaan</button>
</form>
<div class="flex-1 min-w-[300px] max-w-md">
<h3 class="text-2xl font-semibold mb-4">Gebruikers van deze kerk</h3>
<div class="overflow-x-auto rounded-lg border border-gray-300 mb-6">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Gebruikersnaam</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for user in users %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900 flex items-center justify-between">
{{ user.username }}
{% if user.id != current_user.id %}
<form method="post" action="{{ url_for('church_delete_user', user_id=user.id) }}" onsubmit="return confirm('Weet je zeker dat je deze gebruiker wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white px-6 py-3 rounded font-semibold hover:bg-red-700 ml-4 transition">Verwijderen</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<h3 class="text-2xl font-semibold mb-4">Nieuwe gebruiker toevoegen aan deze kerk</h3>
<form method="post" class="space-y-6" >
<input type="hidden" name="form_type" value="user">
<div>
<label for="username" class="block mb-3 font-semibold text-gray-700">Emailadres</label>
<input type="email" id="username" name="username" placeholder="email@voorbeeld.com" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" id="password" name="password" class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 transition">Toevoegen</button>
</form>
<a href="{{ url_for('change_password') }}" class="bg-white text-black border border-black px-4 py-2 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block mt-6 inline-block text-base">Wachtwoord wijzigen</a>
</div>
</div>
<a href="{{ url_for('portal') }}" class="bg-white text-black border border-black px-6 py-3 rounded-md font-semibold shadow hover:bg-gray-100 transition inline-block">Terug naar portal</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Liturgiebord</title>
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
background: #222;
color: #fff;
font-family: 'Lora', serif;
height: 1920px;
width: 1080px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.liturgie-board {
width: 1080px;
height: 1920px;
display: flex;
flex-direction: column;
justify-content: flex-start; /* Align all rows from the top */
align-items: flex-start;
gap: 0vh;
padding-left: 0;
position: relative;
}
.liturgie-line {
margin: auto;
width: 1080px;
justify-items: center;
text-align: left;
font-size: 130px;
line-height: 1.1;
white-space: nowrap;
height: 190px; /* Fixed height ensures every row occupies space, even when empty */
box-sizing: border-box;
margin: 0;
padding: 20px;
z-index: 2;
text-shadow: 2px 2px 8px #000, 0 0 4px #000;
/* Optional: Uncomment for visible empty row borders */
/* border-bottom: 1px solid rgba(255,255,255,0.15); */
}
.liturgie-bg-overlay {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.3);
z-index: 1;
pointer-events: none;
}
</style>
</head>
<body>
<div style="transform-origin: top left;">
<div class="liturgie-board" {% if board.background_image %}
style="background-image: url('{% if board.background_image.startswith('default_') %}{{ url_for('static', filename=board.background_image) }}{% else %}{{ url_for('uploaded_file', filename=board.background_image) }}{% endif %}'); background-size: cover; background-position: center; background-repeat: no-repeat;"
{% endif %}>
{% if board.background_image %}<div class="liturgie-bg-overlay"></div>{% endif %}
{% if schedule_content %}
{% set lines = schedule_content.split('\n') %}
{% for i in range(10) %}
<div class="liturgie-line">{{ lines[i] if lines|length > i else ' ' }}</div>
{% endfor %}
{% else %}
<div class="liturgie-line">{{ board.line1 or ' ' }}</div>
<div class="liturgie-line">{{ board.line2 or ' ' }}</div>
<div class="liturgie-line">{{ board.line3 or ' ' }}</div>
<div class="liturgie-line">{{ board.line4 or ' ' }}</div>
<div class="liturgie-line">{{ board.line5 or ' ' }}</div>
<div class="liturgie-line">{{ board.line6 or ' ' }}</div>
<div class="liturgie-line">{{ board.line7 or ' ' }}</div>
<div class="liturgie-line">{{ board.line8 or ' ' }}</div>
<div class="liturgie-line">{{ board.line9 or ' ' }}</div>
<div class="liturgie-line">{{ board.line10 or ' ' }}</div>
{% endif %}
</div>
</div>
<script>
// LIVE PREVIEW FEATURE: Accept messages with new data and update the lines/background
window.addEventListener('message', function(event) {
// You may want to restrict this for security:
// if (event.origin !== 'YOUR_DOMAIN') return;
var data = event.data;
if (!data || typeof data !== 'object' || !data.type || data.type !== 'updateBoard') return;
// Update all lines
for (let i = 1; i <= 10; i++) {
var el = document.querySelector('.liturgie-line:nth-child(' + (data.bg ? i+1 : i) + ')');
if (el) el.textContent = (data['line'+i] || ' ');
}
// Update background if sent
if ('bg' in data) {
document.querySelector('.liturgie-board').style.backgroundImage = data.bg ? ("url('" + data.bg + "')") : 'none';
// Show/hide overlay
var overlay = document.querySelector('.liturgie-bg-overlay');
if (overlay) {
overlay.style.display = data.bg ? '' : 'none';
}
}
}, false);
</script>
<script>
// Poll every 5 seconds to check for active schedule update
(function() {
let currentLines = [];
let currentBackground = null;
{% if schedule_content %}
currentLines = {{ schedule_content.split('\n')|tojson }};
{% else %}
currentLines = Array(10).fill(' ');
{% endif %}
const boardId = {{ board.id }};
function updateBoardFromData(data) {
// Use postMessage event format to update lines & background
const msg = { type: 'updateBoard' };
for (let i = 1; i <= 10; i++) {
msg['line'+i] = data.lines[i-1] || ' ';
}
if (data.background) {
msg.bg = data.background;
} else {
msg.bg = null;
}
window.postMessage(msg, '*');
currentLines = data.lines;
currentBackground = data.background || null;
}
async function pollActiveSchedule() {
try {
const response = await fetch(`/board/${boardId}/active_schedule_json`);
if (!response.ok) throw new Error('Network response not ok');
const data = await response.json();
// Compare new lines with current lines
let changed = false;
for (let i = 0; i < 10; i++) {
if ((data.lines[i] || ' ') !== (currentLines[i] || ' ')) {
changed = true;
break;
}
}
// Check for background change
if ((data.background || null) !== currentBackground) {
changed = true;
}
if (changed) {
updateBoardFromData(data);
}
} catch (e) {
console.error('Error polling active schedule:', e);
}
}
setInterval(pollActiveSchedule, 5000);
})();
</script>
<script>
// Poll every 5 seconds to check for active schedule update
(function() {
let currentLines = [];
{% if schedule_content %}
currentLines = {{ schedule_content.split('\n')|tojson }};
{% else %}
currentLines = Array(10).fill(' ');
{% endif %}
const boardId = {{ board.id }};
function updateBoardFromData(data) {
// Use postMessage event format to update lines & background
const msg = { type: 'updateBoard' };
for (let i = 1; i <= 10; i++) {
msg['line'+i] = data.lines[i-1] || ' ';
}
if (data.background) {
msg.bg = data.background;
}
window.postMessage(msg, '*');
currentLines = data.lines;
}
async function pollActiveSchedule() {
try {
const response = await fetch(`/board/${boardId}/active_schedule_json`);
if (!response.ok) throw new Error('Network response not ok');
const data = await response.json();
// Compare new lines with current lines
let changed = false;
for (let i = 0; i < 10; i++) {
if ((data.lines[i] || ' ') !== (currentLines[i] || ' ')) {
changed = true;
break;
}
}
// Also consider background change
// We can extend this if needed, skipping for now
if (changed) {
updateBoardFromData(data);
}
} catch (e) {
console.error('Error polling active schedule:', e);
}
}
setInterval(pollActiveSchedule, 5000);
})();
</script>
</body>
</html>

391
templates/edit_board.html Normal file
View File

@@ -0,0 +1,391 @@
{% extends 'base.html' %}
{% block title %}Bewerk Liturgiebord - Digitale Liturgie{% endblock %}
{% block content %}
<style>
.edit-board-container {
padding: 1rem 1rem;
background: white;
border-radius: 0.5rem;
margin: 0 auto 1rem auto;
}
.board-title {
font-size: 2rem;
font-weight: 600;
margin-bottom: 1rem;
color: #000000;
text-align: left;
}
.lines-section {
margin-bottom: 2.5rem;
}
.line-input-group {
margin-bottom: 1.25rem;
}
.line-input-group label {
display: block;
margin-bottom: 0.75rem;
font-weight: 700;
color: #374151;
}
.line-input-group input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1.125rem;
border-radius: 0.5rem;
border: 1px solid #d1d5db;
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.line-input-group input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 5px #93c5fd;
}
.button-group {
display: flex;
gap: 1rem;
justify-content: flex-start;
margin-top: 1.5rem;
}
.button-group button {
padding: 0.75rem 1.75rem;
font-size: 1rem;
border-radius: 0.5rem;
font-weight: 700;
transition: background-color 0.15s ease-in-out;
}
.schedule-section {
max-width: 100%;
margin-bottom: 2.5rem;
}
.schedule-section h3 {
margin-bottom: 1rem;
}
.schedule-section table {
width: 100%;
}
.preview-section {
max-width: 100%;
text-align: center;
font-family: 'Lora', serif;
}
.preview-wrapper {
width: 250px;
height: 444px; /* 9:16 aspect ratio */
margin: 0 auto;
background: #222;
border-radius: 0.5rem;
overflow: hidden;
}
iframe#live-preview-iframe {
width: 1080px;
height: 1920px;
border: 0;
pointer-events: none;
transform-origin: top left;
transform: scale(0.23);
margin: 0;
display: block;
}
@media (max-width: 767px) {
.edit-board-container {
padding: 15px;
margin-bottom: 1rem;
border-radius: 0.5rem;
}
.board-title {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
.button-group {
flex-direction: column;
gap: 8px;
}
.button-group button {
width: 100%;
}
.preview-wrapper {
width: 180px;
height: 320px;
margin: 0 auto;
border-radius: 0.5rem;
background: #222;
overflow: hidden;
}
iframe#live-preview-iframe {
transform: scale(0.23);
position: static;
width: 1080px;
height: 1920px;
margin: 0 auto;
display: block;
}
}
/* Highlight for drag-over between lines in preview */
.liturgie-line.line-over {
background: rgba(255,255,255,0.22);
border: 2px dashed #fff;
box-sizing: border-box;
outline: none;
transition: background 0.12s, border 0.12s;
}
.liturgie-line.swap-animate {
animation: swap-pulse 0.35s;
}
@keyframes swap-pulse {
0% { background: #ffe066; }
60% { background: #fff3cd; }
100% { background: none; }
}
</style>
<div class="edit-board-container">
<div style="display: flex; gap: 2rem; align-items: flex-start; flex-wrap: wrap;">
<!-- Left: Live (editable) preview -->
<form method="post" id="boardForm" style="flex: 1 1 300px; min-width: 250px; max-width:250px;">
<div class="preview-section">
<div class="preview-wrapper"
style="width:250px; height:444px; background: #222; border-radius: 0.5rem; overflow: hidden; position:relative;
background-image: {% if board.background_image %}url('{% if board.background_image.startswith('default_') %}{{ url_for('static', filename=board.background_image) }}{% else %}{{ url_for('uploaded_file', filename=board.background_image) }}{% endif %}'){% else %}none{% endif %}; background-size:cover; background-position:center;">
{% if board.background_image %}
<div class="liturgie-bg-overlay" style="
content:''; position:absolute; top:0; left:0; right:0; bottom:0;
background:rgba(0,0,0,0.3); z-index:1; pointer-events:none;">
</div>
{% endif %}
{% if schedule_active %}
{% set matched_schedules = schedules|selectattr('content', 'equalto', schedule_content)|list %}
<div style="position:absolute; top:0; left:0; right:0; bottom:0; z-index:10; display:flex; justify-content:center; align-items:center; background: rgba(0,0,0,0.7); color:white; font-weight:bold; font-size:1rem; text-align:center; padding: 1rem;">
Geplande dienst ({{ matched_schedules[0].name if matched_schedules|length > 0 else 'Onbekend' }}) is nu actief
</div>
{% endif %}
<div class="liturgie-board"
style="width:100%; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:flex-start; gap:0.6vh; padding-left:5%; position:relative; z-index:2;">
{% if schedule_active %}
{% set lines = schedule_content.split('\\n') %}
{% for i in range(10) %}
<div class="liturgie-line" style="width:90%; font-size:28px; line-height:1.2; text-shadow:2px 2px 8px #000, 0 0 4px #000; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin:0; padding:0; color:#fff; text-align:left;">
{{ lines[i] if lines|length > i else ' ' }}
</div>
{% endfor %}
{% else %}
{% for i in range(1, 11) %}
<div class="liturgie-line"
contenteditable="true"
data-line="line{{ i }}"
draggable="true"
style="width:90%; font-size:28px; line-height:1.2; text-shadow:2px 2px 8px #000, 0 0 4px #000; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin:0; padding:0; color:#fff; text-align:left;"
>{{ board['line' ~ i] or ' ' }}</div>
{% endfor %}
{% endif %}
</div>
</div>
{% if schedule_active %}
<p style="color: red;">Er is een dienst actief! Pas deze aan via de planning.</p>
{% endif %}
</div>
</form>
<!-- Right: Schedule section -->
<div class="schedule-section" style="flex: 1 1 380px; min-width:330px;">
<form method="post" style="margin-bottom: 1rem;">
<input type="hidden" name="form_type" value="name">
<div style="display:flex; gap:8px; align-items:center;">
<input id="boardNameInput" name="name" type="text" value="{{ board.name }}" style="font-size:1.1rem; padding:0.3em 0.6em; border-radius:0.3em; border:1px solid #aaa; width:60%;">
<button type="submit" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold transition" style="margin-left:5px;">Opslaan</button>
</div>
</form>
<h3 class="text-xl font-semibold mb-3">Geplande diensten</h3>
<div class="overflow-x-auto rounded-lg border border-gray-300">
{% if schedules|length == 0 %}
<p class="p-4">Geen geplande inhoud.</p>
{% else %}
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Datum</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Starttijd</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Eindtijd</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for s in schedules %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-gray-900 flex items-center gap-2">
<span style="display:inline-block; width:0.75rem; height:0.75rem; border-radius:9999px;"
title="{% if schedule_active and active_schedule and s.id == active_schedule.id %}Actief{% else %}Niet actief{% endif %}"
class="{% if schedule_active and active_schedule and s.id == active_schedule.id %}bg-green-500{% else %}bg-red-500{% endif %}">
</span>
{{ s.name }}
</td>
<td class="px-6 py-4 text-center text-gray-900">{{ s.date.strftime('%Y-%m-%d') }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ s.start_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ s.end_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-center">
<!-- Removed delete button here: schedules can't be deleted from board edit page -->
<a href="{{ url_for('edit_global_schedule', schedule_id=s.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold transition">Bewerken</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<a href="{{ url_for('add_global_schedule') }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded hover:bg-yellow-300 font-semibold inline-block mt-3 transition">Dienst toevoegen</a>
<div class="button-group mt-3">
<button type="button" class="bg-white text-black border border-black px-6 py-3 rounded font-semibold hover:bg-gray-100 transition"
id="save-and-upload-bg"
data-edit-url="{{ url_for('edit_board', board_id=board.id) }}"
data-upload-url="{{ url_for('upload_background', board_id=board.id) }}">
Achtergrondafbeelding wijzigen
</button>
<!-- Removed Opslaan button as live save is implemented -->
</div>
</div>
</div>
</div>
{% if not schedule_active %}
<script>
// --- Line drag-and-swap logic ---
document.addEventListener('DOMContentLoaded', function () {
let dragSrc = null;
const lines = document.querySelectorAll('.liturgie-line[data-line]');
lines.forEach(line => {
line.addEventListener('dragstart', function (e) {
dragSrc = this;
this.classList.add('dragging');
});
line.addEventListener('dragend', function (e) {
this.classList.remove('dragging');
dragSrc = null;
lines.forEach(l => l.classList.remove('line-over'));
});
line.addEventListener('dragover', function (e) {
e.preventDefault();
if (this !== dragSrc) this.classList.add('line-over');
});
line.addEventListener('dragleave', function (e) {
this.classList.remove('line-over');
});
line.addEventListener('drop', function (e) {
e.preventDefault();
this.classList.remove('line-over');
if (dragSrc && dragSrc !== this) {
// Only swap textContent, not the whole element
const tmp = this.textContent;
this.textContent = dragSrc.textContent;
dragSrc.textContent = tmp;
// animation for both lines
this.classList.add('swap-animate');
dragSrc.classList.add('swap-animate');
setTimeout(() => {
this.classList.remove('swap-animate');
dragSrc.classList.remove('swap-animate');
}, 400);
// trigger input event for autosave
this.dispatchEvent(new Event('input'));
dragSrc.dispatchEvent(new Event('input'));
}
});
});
});
// --- End drag-and-swap logic ---
// On submit, copy preview lines to hidden fields
const boardForm = document.getElementById('boardForm');
if (boardForm) {
for (let i = 1; i <= 10; i++) {
// create hidden inputs for each line
let hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'line'+i;
hidden.id = 'hidden-line'+i;
boardForm.appendChild(hidden);
}
async function saveLines() {
const formData = new FormData();
for (let i = 1; i <= 10; i++) {
const text = document.querySelector('.liturgie-line[data-line="line'+i+'"]').textContent;
formData.append('line'+i, text);
}
const response = await fetch(boardForm.action, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (!response.ok) {
console.error('Failed to save lines.');
}
}
// Add auto save event listeners to editable lines with debounce
for (let i = 1; i <= 10; i++) {
const editableLine = document.querySelector('.liturgie-line[data-line="line'+i+'"]');
if (editableLine) {
editableLine.addEventListener('input', () => {
if (window.autoSaveTimeout) clearTimeout(window.autoSaveTimeout);
window.autoSaveTimeout = setTimeout(saveLines, 1000);
});
}
}
// Prevent form's default submit to avoid page reload on autosave
boardForm.addEventListener('submit', function(e) {
if (document.getElementById('redirect_to_upload').value === '0') {
e.preventDefault();
saveLines();
}
});
}
// Added script for wallpaper button
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('save-and-upload-bg');
if (btn) {
btn.addEventListener('click', function() {
const uploadUrl = this.getAttribute('data-upload-url');
if (uploadUrl) {
window.location.href = uploadUrl;
}
});
}
});
</script>
{% else %}
<script>
// When schedule is active, there's nothing to submit for the lines and nothing to sync.
</script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% block title %}Globale Planningen{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h1 class="text-3xl font-semibold mb-6">Globale planningen</h1>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Datum</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Starttijd</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Eindtijd</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Toegewezen borden</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700 text-center">Acties</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for schedule in schedules %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-gray-900">{{ schedule.name }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.date.strftime('%Y-%m-%d') }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.start_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-center text-gray-900">{{ schedule.end_time.strftime('%H:%M') }}</td>
<td class="px-6 py-4 text-center text-gray-900">
{% for board in schedule.boards %}{{ board.name }}{% if not loop.last %}, {% endif %}{% endfor %}
</td>
<td class="px-6 py-4 text-center">
<a href="{{ url_for('edit_global_schedule', schedule_id=schedule.id) }}" class="bg-[#f7d91a] text-black text-xs px-6 py-3 rounded font-semibold hover:bg-yellow-300 transition">Bewerken</a>
<form action="{{ url_for('delete_global_schedule', schedule_id=schedule.id) }}" method="POST" style="display:inline;" onsubmit="return confirm('Weet je zeker dat je deze planning wilt verwijderen?');">
<button type="submit" class="bg-red-600 text-white text-xs px-6 py-3 rounded font-semibold hover:bg-red-700 transition">Verwijderen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('add_global_schedule') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 inline-block mb-6 transition">Nieuwe planning toevoegen</a>
</div>
{% endblock %}

337
templates/index.html Normal file
View File

@@ -0,0 +1,337 @@
{% extends "base.html" %}
{% block title %}Welkom bij Digitale Liturgie{% endblock %}
{% block content %}
<style>
/* Modern Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
.hero-section {
position: relative;
background: url("{{ url_for('static', filename='Hero.jpg') }}") no-repeat center center/cover;
min-height: 30rem;
border-radius: 1.15rem;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
animation: fadeInHero 1.2s cubic-bezier(.77,0,.18,1) both;
}
@keyframes fadeInHero {
from { opacity: 0; transform: translateY(-30px);}
to { opacity: 1; transform: translateY(0);}
}
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(40,40,50,0.75) 0%, rgba(10,0,40,0.40) 70%);
z-index: 1;
border-radius: 1.15rem;
}
.hero-content {
position: relative;
z-index: 2;
max-width: 42rem;
text-align: center;
padding: 2rem 2rem;
animation: fadeUp 1.2s .3s cubic-bezier(.77,0,.18,1) both;
}
.hero-content h1 {
font-size: 3rem;
font-weight: 700;
text-shadow: 0 5px 32px rgba(0,0,0,0.32);
margin-bottom: 0.5rem;
letter-spacing: -1.2px;
line-height: 1.1;
}
.hero-content p {
font-size: 1.25rem;
margin-bottom: 2rem;
color: #f4f4f4;
}
.modern-btn {
background: linear-gradient(90deg, #f7d91a 0%, #ffe486 100%);
color: #181818;
font-weight: 600;
border: none;
padding: 1rem 2.3rem;
border-radius: 0.66rem;
font-size: 1.14rem;
box-shadow: 0 4px 24px rgba(247,217,26,0.13);
transition: box-shadow 0.23s, background 0.23s, transform 0.18s;
cursor: pointer;
}
.modern-btn:hover {
background: linear-gradient(90deg,#ffe486 0%, #f7d91a 100%);
box-shadow: 0 8px 28px rgba(247,217,26,0.19);
transform: translateY(-2px) scale(1.045);
color: #25201b;
}
/* Contact section styling */
.contact-section {
background: white;
border-radius: 1.15rem;
margin-top: 1rem;
padding: 2.2rem 1.5rem;
box-shadow: 0 8px 24px rgba(0,0,0,0.09);
max-width: 98vw;
min-width: 280px;
animation: fadeUp 1.1s .6s cubic-bezier(.77,0,.18,1) both;
}
.contact-section h2 {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 1.5rem;
}
.contact-section input,
.contact-section textarea {
width: 100%;
border: 1.3px solid #e6e6e6;
border-radius: 0.5rem;
padding: 0.9rem 1.14rem;
font-size: 1rem;
margin-bottom: 1rem;
background: #f8f9fa;
transition: border-color 0.2s;
}
.contact-section input:focus,
.contact-section textarea:focus {
border-color: #f7d91a;
outline: none;
background: #fffde6;
}
.contact-section button {
margin-top: 0.3rem;
width: 100%;
}
/* Cards Area */
.features-area {
display: grid;
gap: 2.2rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
max-width: 1200px;
margin: 0 auto;
margin-top: 2.2rem;
margin-bottom: 2.5rem;
animation: fadeInStagger 1s cubic-bezier(.77,0,.18,1) both;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(50px);}
to { opacity: 1; transform: translateY(0);}
}
@keyframes fadeInStagger {
from { opacity: 0;}
to { opacity: 1;}
}
.feature-card {
background: white;
border-radius: 1.09rem;
box-shadow: 0 6px 20px rgba(0,0,0,0.06);
padding: 2rem 1.2rem 1.4rem 1.2rem;
text-align: center;
min-height: 19.5rem;
animation: fadeUp 0.8s cubic-bezier(.77,0,.18,1) both;
transition: transform 0.22s, box-shadow 0.22s;
will-change: transform;
/* Animation delay stagger per card (manual in HTML) */
}
.feature-card:hover {
transform: translateY(-7px) scale(1.02);
box-shadow: 0 16px 40px rgba(247,217,26,0.13);
}
.feature-card img {
max-width: 55px;
margin-bottom: 1.1rem;
margin-top: -0.8rem;
filter: drop-shadow(0 0 2px rgba(40,40,40,0.06));
transition: transform 0.17s;
}
.feature-card:hover img {
transform: scale(1.095) rotate(2deg);
}
.feature-card h3 {
font-size: 1.23rem;
margin-bottom: 0.9rem;
font-weight: 700;
color: #181a1d;
letter-spacing: -0.02em;
}
.feature-card ul {
text-align: left;
color: #434243;
font-size: 1.02rem;
list-style: none;
padding-left: 0.7em;
line-height: 1.7;
}
.feature-card ul li::before {
content: '• ';
color: #f7d91a;
font-weight: bold;
}
/* Responsive hero/contact split */
@media (min-width: 1000px) {
.hero-container {
display: flex;
gap: 2.4rem;
max-width: 1330px;
margin: 0 auto 3.3rem auto;
}
.hero-section, .contact-section {
flex: 1.2;
min-height: 32rem;
}
.contact-section {
min-width: 350px;
margin-top: 0;
max-width: 400px;
}
}
@media (max-width: 780px) {
.hero-section {
min-height: 22rem;
}
.hero-content h1 { font-size: 2rem;}
.features-area { gap: 1rem;}
.feature-card { padding: 1.3rem 0.5rem; }
}
</style>
<div class="hero-section" style="margin-bottom:2.5rem;">
<div class="hero-overlay"></div>
<div class="hero-content">
<h1>Psalmbord Online</h1>
<p>
Welkom bij Psalmbord Online.<br>
Beheer, plan en presenteer moeiteloos je kerkdiensten.<br>
Start direct met een eigen account!
</p>
<a href="/login" class="modern-btn">Log in</a>
</div>
</div>
<div class="section-intro" style="margin-top: 3.7rem; margin-bottom:1.5rem; animation: fadeUp 1.1s .3s cubic-bezier(.77,0,.18,1) both;">
<h2 class="text-3xl font-bold text-center mb-2" style="font-size:2.1rem;">Wat kan Digitale Liturgie voor u doen?</h2>
<p class="text-center text-md mb-8" style="color:#62666b;">Beheer en presenteer moeiteloos uw kerkdiensten, gebruikers en schermen.</p>
</div>
<div class="features-area">
<div class="feature-card" style="animation-delay:.09s;">
<img src="{{ url_for('static', filename='icons/group.png') }}" alt="Gebruikersbeheer">
<h3>Gebruikersbeheer</h3>
<ul>
<li>Aanmelden & Registreren</li>
<li>Wachtwoordbeheer</li>
<li>Lid & Admin-rollen</li>
</ul>
</div>
<div class="feature-card" style="animation-delay:.20s;">
<img src="{{ url_for('static', filename='icons/church.png') }}" alt="Kerkenbeheer">
<h3>Kerkenbeheer</h3>
<ul>
<li>Kerk aanmaken & aanpassen</li>
<li>Logo uploaden</li>
<li>Activatie beheren</li>
</ul>
</div>
<div class="feature-card" style="animation-delay:.35s;">
<img src="{{ url_for('static', filename='icons/board.png') }}" alt="Bordenbeheer">
<h3>Bordenbeheer</h3>
<ul>
<li>Borden toevoegen, bewerken, verwijderen</li>
<li>Max. 10 tekstregels per bord</li>
<li>Achtergrond instellen of uploaden</li>
</ul>
</div>
<div class="feature-card" style="animation-delay:.50s;">
<img src="{{ url_for('static', filename='icons/task.png') }}" alt="Planningen">
<h3>Planningen</h3>
<ul>
<li>Diensten & liturgie-invulling per datum</li>
<li>Meerdere borden per planning</li>
<li>Automatisch tonen volgens tijd</li>
</ul>
</div>
</div>
<!-- Contact form at BOTTOM -->
<div class="contact-section" style="max-width:none; margin:3.8rem 0 0 0; width:100%;">
<h2>Contacteer ons</h2>
<form method="POST" action="/contact" autocomplete="off">
<div class="contact-form-fields">
<div class="inputs-left">
<input type="text" id="name" name="name" placeholder="Naam" required>
<input type="text" id="church" name="church" placeholder="Kerknaam" required>
<input type="tel" id="phone" name="phone" placeholder="Telefoonnummer" required>
<input type="email" id="email" name="email" placeholder="E-mail" required>
</div>
<div class="textarea-right">
<textarea id="message" name="message" rows="7" placeholder="Uw bericht..." required></textarea>
</div>
</div>
<button type="submit" class="modern-btn">Verstuur</button>
</form>
<style>
.contact-form-fields {
display: flex;
gap: 2.2rem;
align-items: stretch;
}
.inputs-left {
flex: 1 1 210px;
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 190px;
}
.inputs-left input {
opacity: 0;
transform: translateY(32px);
animation: fadeFormInput 0.8s cubic-bezier(.77,0,.18,1) forwards;
}
.inputs-left input:nth-child(1) { animation-delay: 0.18s; }
.inputs-left input:nth-child(2) { animation-delay: 0.32s; }
.inputs-left input:nth-child(3) { animation-delay: 0.46s; }
.inputs-left input:nth-child(4) { animation-delay: 0.6s; }
.textarea-right textarea {
opacity: 0;
transform: translateY(32px);
animation: fadeFormInput 0.8s 0.74s cubic-bezier(.77,0,.18,1) forwards;
height: 100%;
min-height: 150px;
resize: vertical;
}
.contact-section button.modern-btn {
opacity: 0;
transform: translateY(32px);
animation: fadeFormInput 0.8s 1s cubic-bezier(.77,0,.18,1) forwards;
}
@keyframes fadeFormInput {
from { opacity: 0; transform: translateY(32px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 800px) {
.contact-form-fields {
flex-direction: column;
gap: 0;
}
.textarea-right, .inputs-left {
min-width: 0;
}
.textarea-right textarea {
min-height: 100px;
}
}
</style>
</div>
{% endblock %}

27
templates/login.html Normal file
View File

@@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block title %}Login - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-semibold mb-6">Login</h2>
<form method="post" class="space-y-6">
<div>
<label for="username" class="block mb-3 font-semibold text-gray-700">Gebruikersnaam</label>
<input type="text" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Login</button>
</form>
</div>
</div>
{% endblock %}

106
templates/portal.html Normal file
View File

@@ -0,0 +1,106 @@
{% extends 'base.html' %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<h2 class="text-3xl font-semibold mb-6">Welkom, {{ current_user.username }}!</h2>
<div class="overflow-x-auto rounded-lg border border-gray-300">
<table class="min-w-full divide-y divide-gray-200 bg-white">
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Naam</th>
<th class="px-6 py-3 text-sm font-semibold text-gray-700">URL</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Bewerken</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Weergeven</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-700">Verwijderen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for board in boards %}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-gray-900">{{ board.name }}</td>
<td class="px-6 py-4 text-blue-600 break-words">
{% if board.church.is_active %}
<a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" target="_blank" class="underline hover:text-blue-800 transition">display/{{ board.unique_id }}</a>
{% else %}
<em>Geen actieve licentie voor dit scherm</em>
{% endif %}
</td>
<td class="text-center px-6 py-4">
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="bg-[#f7d91a] text-black text-xs px-2 py-1 rounded hover:bg-yellow-300 transition">Bewerken</a>
</td>
<td class="relative text-center px-6 py-4">
{% if board.church.is_active %}
<a href="{{ url_for('display_board_unique', unique_id=board.unique_id) }}" class="bg-black text-white text-xs px-2 py-1 rounded border border-black hover:bg-gray-800 transition show-iframe-preview" target="_blank" data-url="{{ url_for('display_board_unique', unique_id=board.unique_id) }}">Weergeven</a>
<div class="iframe-preview-popup" style="display:none; position:absolute; top:0; left:100%; margin-left:8px; width:400px; height:225px; border:1px solid #ccc; box-shadow:0 2px 8px rgba(0,0,0,0.2); z-index:50;">
<iframe src="" frameborder="0" style="width:100%; height:100%; transform-origin: top left; transform: scale(0.2);"></iframe>
</div>
{% endif %}
</td>
<td class="text-center px-6 py-4">
<form method="post" action="{{ url_for('delete_board', board_id=board.id) }}" style="display:inline;">
<button type="submit" class="bg-white text-black text-xs px-2 py-1 rounded border border-black hover:bg-gray-100 transition" onclick="return confirm('Weet je zeker dat je dit bord wilt verwijderen?');">Verwijderen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{{ url_for('add_board') }}" class="bg-[#f7d91a] text-black px-6 py-3 rounded-md font-semibold shadow hover:bg-yellow-300 inline-block mb-6 transition">Nieuw liturgiebord toevoegen</a>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const showIframeLinks = document.querySelectorAll('.show-iframe-preview');
// Create a single popup container to be reused
const popup = document.createElement('div');
popup.style.position = 'absolute';
popup.style.width = '1080px';
popup.style.height = '1920px';
popup.style.border = '1px solid #ccc';
popup.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
popup.style.zIndex = '9999';
popup.style.display = 'none';
popup.style.background = 'white';
popup.style.transformOrigin = 'top left';
popup.style.transform = 'scale(0.2)';
// iframe inside popup
const iframe = document.createElement('iframe');
iframe.frameBorder = 0;
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.transform = '';
iframe.style.transformOrigin = 'top left';
popup.appendChild(iframe);
document.body.appendChild(popup);
showIframeLinks.forEach(link => {
link.addEventListener('mouseenter', (event) => {
const url = link.getAttribute('data-url');
iframe.src = url;
const rect = link.getBoundingClientRect();
// Position popup to the right and vertically aligned with the link
popup.style.left = (window.scrollX + rect.right + 8) + 'px';
popup.style.top = (window.scrollY + rect.top) + 'px';
popup.style.display = 'block';
});
link.addEventListener('mouseleave', () => {
popup.style.display = 'none';
iframe.src = '';
});
});
// Also hide popup if mouse moves out of popup itself
popup.addEventListener('mouseleave', () => {
popup.style.display = 'none';
iframe.src = '';
});
});
</script>
{% endblock %}

32
templates/register.html Normal file
View File

@@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% block title %}Register - Digitale Liturgie{% endblock %}
{% block content %}
<div class="space-y-8 max-w-6xl mx-auto px-4">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-semibold mb-6">Register</h2>
<form method="post" class="space-y-6">
<div>
<label for="username" class="block mb-3 font-semibold text-gray-700">Emailadres</label>
<input type="email" id="username" name="username" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="password" class="block mb-3 font-semibold text-gray-700">Wachtwoord</label>
<input type="password" id="password" name="password" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<div>
<label for="church" class="block mb-3 font-semibold text-gray-700">Kerk</label>
<input type="text" id="church" name="church" placeholder="Kies of maak een kerk" required class="w-full rounded-md border border-gray-300 p-4 text-lg placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-4 font-semibold shadow hover:bg-yellow-300 transition">Register</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,274 @@
{% extends 'base.html' %}
{% block title %}{% if schedule %}Schema Bijwerken{% else %}Schema Toevoegen{% endif %}{% endblock %}
{% block content %}
<style>
.preview-container {
display: flex;
gap: 2rem;
align-items: flex-start;
flex-wrap: wrap;
max-width: 900px;
margin: 0 auto 2rem auto;
background: white;
padding: 1rem;
border-radius: 0.5rem;
}
.preview-wrapper {
width: 250px;
height: 444px;
background: #222;
border-radius: 0.5rem;
overflow: hidden;
position: relative;
background-size: cover;
background-position: center;
}
.liturgie-board {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 0.6vh;
padding-left: 5%;
position: relative;
z-index: 2;
}
.liturgie-line {
width: 90%;
font-size: 28px;
line-height: 1.2;
text-shadow: 2px 2px 8px #000, 0 0 4px #000;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
padding: 0;
color: #fff;
text-align: left;
outline: none;
cursor: text;
user-select: text;
}
/* Highlight for drag-over between lines in preview */
.liturgie-line.line-over {
background: rgba(255,255,255,0.22);
border: 2px dashed #fff;
box-sizing: border-box;
outline: none;
transition: background 0.12s, border 0.12s;
}
.liturgie-line.swap-animate {
animation: swap-pulse 0.35s;
}
@keyframes swap-pulse {
0% { background: #ffe066; }
60% { background: #fff3cd; }
100% { background: none; }
}
.input-section {
flex: 1 1 260px;
min-width: 260px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.input-section label {
font-weight: 700;
color: #374151;
display: block;
margin-bottom: 0.25rem;
}
.input-section input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 1rem;
outline: none;
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.input-section input:focus {
border-color: #2563eb;
box-shadow: 0 0 5px #93c5fd;
}
.save-button {
padding: 0.75rem 1.25rem;
background-color: #16a34a;
color: white;
border-radius: 0.375rem;
font-weight: 700;
border: none;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
margin-top: auto;
}
.save-button:hover {
background-color: #15803d;
}
@media (max-width: 767px) {
.preview-container {
flex-direction: column;
max-width: 100%;
margin-bottom: 1rem;
}
.preview-wrapper {
width: 200px;
height: 355px;
margin: 0 auto 1rem auto;
}
.input-section {
min-width: unset;
}
}
</style>
<h1 class="text-3xl font-bold mb-6 text-center">{% if schedule %}Schema Bijwerken{% else %}Schema Toevoegen{% endif %}</h1>
<form method="post" id="scheduleForm">
<div class="preview-container">
<!-- Left: Live preview with editable lines -->
<div class="preview-wrapper" style="font-family: 'Lora', serif; position: relative; background-image: {% if schedule and schedule.boards_list|length > 0 %}
{% set board_bg = schedule.boards_list[0].background_image if schedule.boards_list[0].background_image else None %}
{% if board_bg %}
url('{% if board_bg.startswith('default_') %}{{ url_for('static', filename=board_bg) }}{% else %}{{ url_for('uploaded_file', filename=board_bg) }}{% endif %}')
{% else %}none
{% endif %}
{% else %}none
{% endif %}; background-color:#222; background-size: cover; background-position: center;">
<div class="liturgie-bg-overlay" style="
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 1;
pointer-events: none;
"></div>
<div class="liturgie-board" contenteditable="false" style="position: relative; z-index: 2; font-family: 'Lora', serif;">
{% set lines = schedule.content.split('\n') if schedule and schedule.content else [' ']*10 %}
{% for i in range(10) %}
<div class="liturgie-line" contenteditable="true" data-line="line{{ i+1 }}" spellcheck="false">{{ lines[i] if i < lines|length else ' ' }}</div>
{% endfor %}
</div>
</div>
<!-- Right: Input fields for schedule name, date, time, board assignment, and save button -->
<div class="input-section">
<label for="name">Naam:</label>
<input type="text" id="name" name="name" required value="{{ schedule.name if schedule else '' }}">
<label for="date">Datum:</label>
<input type="date" id="date" name="date" required value="{{ schedule.date.strftime('%Y-%m-%d') if schedule and schedule.date else '' }}">
<div style="display: flex; gap: 1rem; align-items: center;">
<div style="flex: 1;">
<label for="start_time">Starttijd:</label>
<input type="time" id="start_time" name="start_time" required value="{{ schedule.start_time.strftime('%H:%M') if schedule and schedule.start_time else '' }}">
</div>
<div style="flex: 1;">
<label for="end_time">Eindtijd:</label>
<input type="time" id="end_time" name="end_time" required value="{{ schedule.end_time.strftime('%H:%M') if schedule and schedule.end_time else '' }}">
</div>
</div>
<label>Toegewezen borden:</label>
<div class="overflow-hidden rounded-lg max-h-56 overflow-y-auto mb-6 border border-gray-200">
<table class="min-w-full divide-y divide-gray-200 border-collapse">
<thead class="bg-gray-50">
<tr class="divide-x divide-gray-200">
<th scope="col" class="py-3.5 pl-4 pr-4 text-left text-sm font-semibold text-gray-900 sm:pl-6">Naam</th>
<th scope="col" class="px-4 py-3.5 text-center text-sm font-semibold text-gray-900">Toewijzen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{% for board in boards %}
<tr class="divide-x divide-gray-200 hover:bg-gray-50 cursor-pointer">
<td class="whitespace-nowrap py-4 pl-4 pr-4 text-sm font-semibold text-gray-900 sm:pl-6">{{ board.name }}</td>
<td class="whitespace-nowrap px-4 py-4 text-center text-sm font-semibold text-gray-900 flex justify-center items-center">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" value="{{ board.id }}" name="boards" class="sr-only peer" {% if schedule and board in schedule.boards_list %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:bg-blue-600 transition-all duration-300"></div>
<div class="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-300 peer-checked:translate-x-5"></div>
</label>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<button type="submit" class="bg-[#f7d91a] text-black px-8 py-3 rounded font-semibold hover:bg-yellow-300 transition">Opslaan</button>
</div>
</div>
<!-- Hidden inputs to store lines for form submission -->
{% for i in range(1, 11) %}
<input type="hidden" name="line{{ i }}" id="hidden-line{{ i }}" value="">
{% endfor %}
</form>
<script>
// --- Line drag-and-swap logic ---
document.addEventListener('DOMContentLoaded', function () {
let dragSrc = null;
const lines = document.querySelectorAll('.liturgie-line[data-line]');
lines.forEach(line => {
line.addEventListener('dragstart', function (e) {
dragSrc = this;
this.classList.add('dragging');
});
line.addEventListener('dragend', function (e) {
this.classList.remove('dragging');
dragSrc = null;
lines.forEach(l => l.classList.remove('line-over'));
});
line.addEventListener('dragover', function (e) {
e.preventDefault();
if (this !== dragSrc) this.classList.add('line-over');
});
line.addEventListener('dragleave', function (e) {
this.classList.remove('line-over');
});
line.addEventListener('drop', function (e) {
e.preventDefault();
this.classList.remove('line-over');
if (dragSrc && dragSrc !== this) {
// Only swap textContent, not the whole element
const tmp = this.textContent;
this.textContent = dragSrc.textContent;
dragSrc.textContent = tmp;
// animation for both lines
this.classList.add('swap-animate');
dragSrc.classList.add('swap-animate');
setTimeout(() => {
this.classList.remove('swap-animate');
dragSrc.classList.remove('swap-animate');
}, 400);
}
});
// Make lines draggable
line.setAttribute('draggable', 'true');
});
});
// --- End drag-and-swap logic ---
document.getElementById('scheduleForm').addEventListener('submit', function(e) {
// Copy content from editable divs to hidden inputs
for (let i = 1; i <= 10; i++) {
let div = document.querySelector('.liturgie-line[data-line="line' + i + '"]');
let hiddenInput = document.getElementById('hidden-line' + i);
if (div && hiddenInput) {
hiddenInput.value = div.textContent.trim() || ' ';
}
}
});
// Optional: keyboard navigation and basic shortcuts could be added here
</script>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends 'base.html' %}
{% block title %}Achtergrondafbeelding wijzigen - Digitale Liturgie{% endblock %}
{% block content %}
<div class="max-w-lg mx-auto">
<h2 class="text-3xl font-semibold mb-6">Achtergrondafbeelding wijzigen voor: {{ board.name }}</h2>
<div class="flex space-x-8">
<form method="post" enctype="multipart/form-data" class="space-y-6 flex-1">
<div>
<label for="background" class="block mb-2 font-semibold text-gray-700">Kies een afbeelding</label>
<input type="file" id="background" name="background" accept="image/*" required class="w-full rounded-md border border-gray-300 p-3 text-base placeholder-gray-400 shadow-sm focus:border-blue-600 focus:ring focus:ring-blue-300 focus:ring-opacity-50 transition">
</div>
<button type="submit" class="bg-[#f7d91a] text-black w-full rounded-md py-3 font-semibold shadow hover:bg-yellow-300 transition">Uploaden</button>
<a href="{{ url_for('edit_board', board_id=board.id) }}" class="block text-center bg-white text-black border border-black rounded-md py-3 mt-2 font-semibold shadow hover:bg-gray-100 transition w-full">Annuleren</a>
</form>
{% if board.background_image %}
<div class="flex-1">
<p class="mb-2">Huidige achtergrond:</p>
<img src="{{ url_for('uploaded_file', filename=board.background_image) }}" alt="Achtergrond" class="rounded-lg max-w-full max-h-72 object-contain">
</div>
{% endif %}
</div>
<div class="mt-10">
<h3 class="text-xl font-semibold mb-4">Kies een standaard achtergrond:</h3>
<div class="grid grid-cols-4 gap-4">
<form method="post" class="inline-block">
<input type="hidden" name="default_background" value="default_wall_1.jpg">
<img src="/static/default_wall_1.jpg" alt="Standaard achtergrond 1" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
</form>
<form method="post" class="inline-block">
<input type="hidden" name="default_background" value="default_wall_2.jpg">
<img src="/static/default_wall_2.jpg" alt="Standaard achtergrond 2" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
</form>
<form method="post" class="inline-block">
<input type="hidden" name="default_background" value="default_wall_3.jpg">
<img src="/static/default_wall_3.jpg" alt="Standaard achtergrond 3" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
</form>
<form method="post" class="inline-block">
<input type="hidden" name="default_background" value="default_wall_4.jpg">
<img src="/static/default_wall_4.jpg" alt="Standaard achtergrond 4" class="cursor-pointer rounded-lg border border-gray-300 hover:border-blue-600 transition" onclick="this.closest('form').submit()">
</form>
</div>
</div>
</div>
{% endblock %}