Updated the proostekrant to work properly again.

This commit is contained in:
douwe
2026-10-09 10:33:21 +02:00
parent 56d143b74b
commit 256a81a770
133 changed files with 10491 additions and 837 deletions
+26
View File
@@ -0,0 +1,26 @@
<?php
// Application configuration
return [
'app_name' => 'ProostenKrant',
'app_description' => 'Een krant van en voor Proosten',
'app_slogan' => 'Een krant van en voor Proosten',
'universal_password' => getenv('UNIVERSAL_PASSWORD') ?: 'wachtwoord',
'upload_paths' => [
'articles' => __DIR__ . '/../public/images/articles/',
'profiles' => __DIR__ . '/../public/images/profiles/',
],
'latex_paths' => [
'templates' => __DIR__ . '/../templates/latex/',
'output' => __DIR__ . '/../storage/latex/',
],
'seasons' => [
1 => ['name' => 'Lente', 'color' => '#90EE90'],
2 => ['name' => 'Zomer', 'color' => '#FFD700'],
3 => ['name' => 'Herfst', 'color' => '#FF8C00'],
4 => ['name' => 'Winter', 'color' => '#ADD8E6'],
],
];
+10
View File
@@ -0,0 +1,10 @@
<?php
// Database configuration
return [
'host' => getenv('DB_HOST') ?: 'mysql',
'database' => getenv('DB_NAME') ?: 'proostenkrant',
'username' => getenv('DB_USER') ?: 'proostenkrant',
'password' => getenv('DB_PASSWORD') ?: 'proostenkrant',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
];
+45
View File
@@ -0,0 +1,45 @@
<?php
// Route configuration
return [
'GET' => [
'/' => 'PublicController@index',
'/index.php' => 'PublicController@index',
'/archive' => 'PublicController@archive',
// Submission routes
'/submit' => 'SubmissionController@showPassphrase',
'/submit/passphrase' => 'SubmissionController@showPassphrase',
'/submit/form' => 'SubmissionController@showForm',
'/submit/confirmation' => 'SubmissionController@showConfirmation',
'/submit/edit' => 'SubmissionController@showEditPassphrase',
'/submit/edit/list' => 'SubmissionController@showEditList',
'/submit/edit/(\d+)' => 'SubmissionController@showWriterVerification',
// Legacy routes (for logged in users)
'/login' => 'AuthController@showLogin',
'/logout' => 'AuthController@logout',
'/dashboard' => 'DashboardController@index',
'/articles' => 'ArticleController@index',
'/articles/new' => 'ArticleController@new',
'/articles/create' => 'ArticleController@create',
'/articles/(\d+)' => 'ArticleController@show',
'/articles/(\d+)/verify-writer' => 'ArticleController@showVerifyWriter',
'/articles/(\d+)/verify-passphrase' => 'ArticleController@showVerifyPassphrase',
'/seasons' => 'SeasonController@index',
'/generate-pdf' => 'PdfController@generate',
],
'POST' => [
// Submission routes
'/submit/verify' => 'SubmissionController@verifyPassphrase',
'/submit/store' => 'SubmissionController@store',
'/submit/edit/verify' => 'SubmissionController@verifyEditPassphrase',
'/submit/edit/verify-writer' => 'SubmissionController@verifyWriter',
// Legacy routes
'/login' => 'AuthController@login',
'/articles' => 'ArticleController@store',
'/upload-image' => 'UploadController@upload',
'/articles/verify-writer' => 'ArticleController@verifyWriter',
'/articles/verify-passphrase' => 'ArticleController@verifyPassphrase',
],
];
+19
View File
@@ -0,0 +1,19 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Redirect all requests to index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
# Allow .htaccess overrides
AllowOverride All
# PHP settings
<IfModule mod_php.c>
php_value upload_max_filesize 50M
php_value post_max_size 52M
php_value max_execution_time 300
</IfModule>
+1291
View File
File diff suppressed because it is too large Load Diff
+582
View File
@@ -0,0 +1,582 @@
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
/* Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
.header {
background: linear-gradient(135deg, #8B4513, #A0522D);
color: white;
padding: 20px 0;
margin-bottom: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header-content {
text-align: center;
margin-bottom: 20px;
}
.logo {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 5px;
}
.logo a {
color: white;
text-decoration: none;
}
.logo a:hover {
text-decoration: underline;
}
.slogan {
font-style: italic;
font-size: 1.2rem;
margin-bottom: 5px;
}
.location {
font-size: 1rem;
opacity: 0.9;
}
/* Navigation */
.nav {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 15px;
border-radius: 5px;
transition: background-color 0.3s;
}
.nav a:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.nav a.btn {
background-color: #4CAF50;
}
.nav a.btn:hover {
background-color: #45a049;
}
.nav a.btn-logout {
background-color: #f44336;
}
.nav a.btn-logout:hover {
background-color: #d32f2f;
}
/* Main Content */
.main {
padding: 20px 0;
min-height: calc(100vh - 200px);
}
/* Footer */
.footer {
background-color: #8B4513;
color: white;
text-align: center;
padding: 20px 0;
margin-top: 30px;
}
/* Alerts */
.alert {
padding: 15px 20px;
margin-bottom: 20px;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert .close {
cursor: pointer;
font-weight: bold;
}
/* Login Page */
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 70vh;
}
.login-card {
background-color: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
}
.login-card h2 {
text-align: center;
color: #8B4513;
margin-bottom: 20px;
}
.login-card p {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
/* Form Styles */
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.form-control {
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-control:focus {
border-color: #8B4513;
outline: none;
}
.form-control.error {
border-color: #f44336;
}
.error-message {
color: #f44336;
font-size: 0.9rem;
margin-top: 5px;
}
label {
font-weight: 500;
color: #333;
}
/* Buttons */
.btn {
display: inline-block;
padding: 12px 24px;
background-color: #8B4513;
color: white;
text-decoration: none;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #A0522D;
}
.btn-primary {
background-color: #4CAF50;
}
.btn-primary:hover {
background-color: #45a049;
}
.btn-secondary {
background-color: #607d8b;
}
.btn-secondary:hover {
background-color: #455a64;
}
.btn-small {
padding: 8px 12px;
font-size: 0.9rem;
background-color: #607d8b;
}
.btn-small:hover {
background-color: #455a64;
}
/* Tables */
.articles-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.articles-table th,
.articles-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.articles-table th {
background-color: #f5f5f5;
font-weight: 600;
}
.articles-table tr:hover {
background-color: #f9f9f9;
}
/* Status Badges */
.status-badge {
padding: 5px 10px;
border-radius: 15px;
font-size: 0.85rem;
font-weight: 500;
}
.status-badge.published {
background-color: #d4edda;
color: #155724;
}
.status-badge.draft {
background-color: #fff3cd;
color: #856404;
}
.status-badge.archived {
background-color: #f8d7da;
color: #721c24;
}
/* Cards */
.article-card,
.season-card,
.dashboard-card {
background-color: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.article-card h4,
.season-card h4,
.dashboard-card h3 {
color: #8B4513;
margin-bottom: 10px;
}
.article-card .article-meta {
color: #666;
font-size: 0.9rem;
margin-bottom: 10px;
}
.article-card p {
color: #444;
line-height: 1.5;
}
.article-image {
width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 5px;
margin-top: 15px;
}
/* Grids */
.articles-grid,
.seasons-grid,
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.dashboard-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
/* Season Banner */
.season-banner,
.season-info {
padding: 20px;
border-radius: 10px;
margin: 20px 0;
color: #333;
}
.season-banner h3,
.season-info h3 {
color: #333;
}
/* Form Layout */
.form-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.form-row .form-group {
flex: 1;
}
.form-row .full-width {
flex: none;
width: 100%;
}
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px 12px;
background-color: #f5f5f5;
border-radius: 5px;
transition: background-color 0.3s;
}
.checkbox-label:hover {
background-color: #e0e0e0;
}
.checkbox-label input[type="checkbox"] {
cursor: pointer;
}
.form-actions {
display: flex;
gap: 15px;
margin-top: 30px;
}
/* Article Detail */
.article-detail {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.article-detail h2 {
color: #8B4513;
margin-bottom: 20px;
}
.article-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.meta-item {
display: flex;
flex-direction: column;
}
.meta-item strong {
color: #8B4513;
}
.article-image-container {
margin: 20px 0;
text-align: center;
}
.article-image {
max-width: 100%;
max-height: 500px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.article-content {
line-height: 1.8;
color: #444;
}
.article-actions {
margin-top: 30px;
display: flex;
gap: 15px;
}
/* Quick Links */
.quick-links {
display: flex;
gap: 15px;
flex-wrap: wrap;
margin-top: 20px;
}
.quick-links a {
display: inline-block;
padding: 10px 20px;
background-color: white;
color: #8B4513;
text-decoration: none;
border: 1px solid #ddd;
border-radius: 5px;
transition: all 0.3s;
}
.quick-links a:hover {
background-color: #8B4513;
color: white;
}
/* Season Detail */
.season-detail {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.season-detail h2 {
color: #8B4513;
margin-bottom: 20px;
}
.season-info {
background-color: #f9f9f9;
padding: 15px;
border-radius: 5px;
margin-bottom: 30px;
}
.season-actions {
margin-top: 20px;
}
.back-link {
margin-top: 30px;
}
.back-link a {
color: #8B4513;
text-decoration: none;
}
.back-link a:hover {
text-decoration: underline;
}
/* Responsive */
@media (max-width: 768px) {
.header-content {
text-align: left;
}
.logo {
font-size: 2rem;
}
.nav {
flex-direction: column;
align-items: center;
}
.form-row {
flex-direction: column;
gap: 0;
}
.articles-grid,
.seasons-grid,
.dashboard-grid {
grid-template-columns: 1fr;
}
.article-meta {
flex-direction: column;
gap: 10px;
}
.form-actions {
flex-direction: column;
}
}
/* Print Styles */
@media print {
.nav,
.header-content {
display: none;
}
.article-card,
.season-card,
.dashboard-card {
box-shadow: none;
page-break-inside: avoid;
}
}
View File
View File
+114
View File
@@ -0,0 +1,114 @@
<?php
// Error reporting
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Start session
session_start();
// Set timezone
ini_set('date.timezone', 'Europe/Brussels');
// Autoload classes
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/../src/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relativeClass = substr($class, $len);
// Convert namespace separators to directory separators
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
// Convert to lowercase for directory names
$parts = explode('/', $file);
$newParts = [];
foreach ($parts as $part) {
if (strpos($part, '.php') === false) {
$newParts[] = strtolower($part);
} else {
$newParts[] = $part;
}
}
$file = implode('/', $newParts);
if (file_exists($file)) {
require $file;
}
});
// Load configuration
$config = include __DIR__ . '/../config/app.php';
// Database configuration
$databaseConfig = include __DIR__ . '/../config/database.php';
// Set database environment variables for Docker
if (!getenv('DB_HOST')) {
putenv('DB_HOST=mysql');
}
if (!getenv('DB_NAME')) {
putenv('DB_NAME=proostenkrant');
}
if (!getenv('DB_USER')) {
putenv('DB_USER=proostenkrant');
}
if (!getenv('DB_PASSWORD')) {
putenv('DB_PASSWORD=proostenkrant');
}
// Load routes
$routes = include __DIR__ . '/../config/routes.php';
// Get current request
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Route matching
$matched = false;
foreach ($routes[$method] ?? [] as $pattern => $handler) {
// Convert route pattern to regex
// First, replace route parameter patterns before quoting
$patternForRegex = preg_replace('/\(\d+\)/', '(\d+)', $pattern);
$patternForRegex = preg_replace('/\(\w+\)/', '([^\/]+)', $patternForRegex);
$regex = '#^' . $patternForRegex . '$#';
if (preg_match($regex, $uri, $matches)) {
// Remove first match (full match)
array_shift($matches);
// Parse handler
list($controllerName, $actionName) = explode('@', $handler);
// Instantiate controller
$controllerClass = 'App\\Controllers\\' . $controllerName;
if (class_exists($controllerClass)) {
$controller = new $controllerClass();
// Call action with parameters
if (method_exists($controller, $actionName)) {
call_user_func_array([$controller, $actionName], $matches);
$matched = true;
break;
}
}
}
}
// Handle 404
if (!$matched) {
http_response_code(404);
// Show a simple 404 page
echo '<!DOCTYPE html>';
echo '<html lang="nl">';
echo '<head><meta charset="UTF-8"><title>404 - Pagina niet gevonden</title></head>';
echo '<body style="text-align: center; padding: 50px;">';
echo '<h1>404 - Pagina niet gevonden</h1>';
echo '<p><a href="/">Ga terug naar de homepagina</a></p>';
echo '</body></html>';
}
+97
View File
@@ -0,0 +1,97 @@
// ProostenKrant JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Auto-hide alerts after 5 seconds
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
setTimeout(() => {
alert.style.opacity = '0';
setTimeout(() => {
alert.remove();
}, 500);
}, 5000);
});
// Form validation feedback
const formGroups = document.querySelectorAll('.form-group');
formGroups.forEach(group => {
const input = group.querySelector('.form-control');
const errorMessage = group.querySelector('.error-message');
if (input && errorMessage && errorMessage.textContent.trim() !== '') {
input.addEventListener('focus', function() {
input.classList.remove('error');
errorMessage.style.display = 'none';
});
}
});
// Auto-focus first input on login page
if (document.querySelector('.login-form')) {
const firstInput = document.querySelector('.login-form .form-control');
if (firstInput) {
firstInput.focus();
}
}
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Image preview for article form
const imageInputs = [document.getElementById('article_image'), document.getElementById('image')];
imageInputs.forEach(input => {
if (input) {
const form = input.closest('form');
const previewContainer = document.createElement('div');
previewContainer.className = 'image-preview';
previewContainer.style.display = 'none';
previewContainer.style.marginTop = '15px';
input.parentNode.parentNode.appendChild(previewContainer);
input.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
previewContainer.innerHTML = '<img src="' + e.target.result + '" style="max-width: 200px; max-height: 200px; border-radius: 5px;" />';
previewContainer.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
previewContainer.style.display = 'none';
}
});
}
});
// Confirm before logout
const logoutLinks = document.querySelectorAll('.btn-logout, a[href="/logout"]');
logoutLinks.forEach(link => {
link.addEventListener('click', function(e) {
if (!confirm('Weet u zeker dat u wilt uitloggen?')) {
e.preventDefault();
}
});
});
// Season color indicator
const seasonCards = document.querySelectorAll('.season-card');
seasonCards.forEach(card => {
const bgColor = card.style.backgroundColor;
if (bgColor) {
card.style.borderLeft = '5px solid ' + bgColor;
}
});
});
+1
View File
@@ -0,0 +1 @@
/var/www/html/storage/latex
+192
View File
@@ -0,0 +1,192 @@
<?php
namespace App\Controllers;
use App\Models\Article;
use App\Models\Season;
use App\Models\User;
class ArticleController extends Controller {
public function index() {
$this->requireAuth();
$articles = Article::all();
$this->view('articles/index', ['articles' => $articles]);
}
public function new() {
$this->requireAuth();
$seasons = Season::all();
$users = User::all();
$this->view('articles/new', [
'seasons' => $seasons,
'users' => $users
]);
}
public function create() {
// This will handle the form display for creating a new article
$this->new();
}
public function store() {
$this->requireAuth();
$validationRules = [
'title' => 'required|min:5|max:255',
'content' => 'required|min:10',
'author_id' => 'required',
'event_date' => 'required|date',
'season_id' => 'required',
];
$errors = $this->validate($_POST, $validationRules);
if (!empty($errors)) {
$_SESSION['errors'] = $errors;
$_SESSION['old_input'] = $_POST;
$this->redirect('/articles/new');
}
// Process participants
$participants = $_POST['participants'] ?? [];
if (empty($participants)) {
$participants = [$_POST['author_id']];
}
$articleImage = null;
if (isset($_FILES['article_image']) && $_FILES['article_image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = $this->config['upload_paths']['articles'];
$filename = uniqid() . '_' . basename($_FILES['article_image']['name']);
$targetPath = $uploadDir . $filename;
if (move_uploaded_file($_FILES['article_image']['tmp_name'], $targetPath)) {
$articleImage = 'images/articles/' . $filename;
}
}
$articleData = [
'title' => $_POST['title'],
'content' => $_POST['content'],
'author_id' => $_POST['author_id'],
'participants' => implode(',', $participants),
'event_date' => $_POST['event_date'],
'season_id' => $_POST['season_id'],
'article_image' => $articleImage,
'status' => 'published',
];
$articleId = Article::create($articleData);
$_SESSION['success'] = 'Artikel succesvol toegevoegd!';
$this->redirect('/articles');
}
public function show($id) {
$article = Article::find($id);
if (!$article) {
$this->redirect('/articles');
}
$participantIds = explode(',', $article['participants']);
$participants = User::getByIds($participantIds);
$this->view('articles/show', [
'article' => $article,
'participants' => $participants
]);
}
public function showVerifyWriter($id) {
$this->requireAuth();
$article = Article::find($id);
if (!$article) {
$_SESSION['error'] = 'Artikel niet gevonden.';
$this->redirect('/articles');
}
$users = User::all();
$this->view('articles/verify_writer', [
'article' => $article,
'users' => $users
]);
}
public function showVerifyPassphrase($id) {
$this->requireAuth();
$article = Article::find($id);
if (!$article) {
$_SESSION['error'] = 'Artikel niet gevonden.';
$this->redirect('/articles');
}
// Ensure edit_article_id is set in session for the passphrase verification
if (!isset($_SESSION['edit_article_id']) || $_SESSION['edit_article_id'] != $id) {
$_SESSION['edit_article_id'] = $id;
}
$this->view('articles/verify_passphrase', [
'article' => $article
]);
}
public function verifyWriter() {
$this->requireAuth();
$articleId = $_POST['article_id'] ?? null;
$writerId = $_POST['writer_id'] ?? null;
if (!$articleId || !$writerId) {
$_SESSION['error'] = 'Ongeldige verificatie.';
$this->redirect('/articles');
}
$article = Article::find($articleId);
if (!$article) {
$_SESSION['error'] = 'Artikel niet gevonden.';
$this->redirect('/articles');
}
if ($article['author_id'] != $writerId) {
$_SESSION['error'] = 'Enkel de schrijver van het artikel kan deze bewerken.';
$this->redirect('/articles');
}
$this->redirect('/articles/' . $articleId . '/verify-passphrase');
}
public function verifyPassphrase() {
$this->requireAuth();
$articleId = $_SESSION['edit_article_id'] ?? null;
$passphrase = $_POST['passphrase'] ?? '';
$universalPassword = $this->config['universal_password'] ?? 'ProostenKrant2024!';
if (!$articleId) {
$_SESSION['error'] = 'Geen artikel geselecteerd voor bewerken.';
$this->redirect('/articles');
}
$article = Article::find($articleId);
if (!$article) {
$_SESSION['error'] = 'Artikel niet gevonden.';
$this->redirect('/articles');
}
if ($passphrase !== $universalPassword) {
$_SESSION['error'] = 'Onjuiste paszin. Probeer het opnieuw.';
$this->redirect('/articles/' . $articleId . '/verify-passphrase');
}
// Redirect to submission form with article data
$this->redirect('/submit/form');
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Controllers;
use App\Models\User;
use App\Models\Setting;
class AuthController extends Controller {
public function showLogin() {
if ($this->isLoggedIn()) {
$this->redirect('/dashboard');
}
$this->view('login', ['page_title' => 'Inloggen']);
}
public function login() {
$password = $_POST['password'] ?? '';
$username = $_POST['username'] ?? '';
$universalPassword = Setting::get('universal_password', $this->config['universal_password']);
$user = User::findByUsername($username);
if ($user && $password === $universalPassword) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['display_name'] = $user['display_name'];
$_SESSION['is_logged_in'] = true;
$redirect = $_SESSION['redirect_after_login'] ?? '/dashboard';
unset($_SESSION['redirect_after_login']);
$this->redirect($redirect);
} else {
$_SESSION['error'] = 'Ongeldige gebruikersnaam of wachtwoord';
$this->redirect('/login');
}
}
public function logout() {
session_unset();
session_destroy();
$this->redirect('/login');
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace App\Controllers;
class Controller {
protected $config;
protected $data = [];
public function __construct() {
$this->config = include __DIR__ . '/../../config/app.php';
}
protected function view($viewName, $data = []) {
$this->data = array_merge($this->data, $data);
// Try different view directories
$viewPaths = [
__DIR__ . '/../views/' . $viewName . '.php',
__DIR__ . '/../views/pages/' . $viewName . '.php',
__DIR__ . '/../views/public/' . $viewName . '.php',
__DIR__ . '/../views/submission/' . $viewName . '.php',
__DIR__ . '/../views/articles/' . $viewName . '.php',
__DIR__ . '/../views/seasons/' . $viewName . '.php',
];
$viewPath = null;
foreach ($viewPaths as $path) {
if (file_exists($path)) {
$viewPath = $path;
break;
}
}
if (!$viewPath) {
die("View not found: " . $viewName);
}
// Make config available to views
$this->data['config'] = $this->config;
extract($this->data);
require $viewPath;
}
protected function redirect($url) {
header("Location: {$url}");
exit();
}
protected function validate($data, $rules) {
$errors = [];
foreach ($rules as $field => $ruleString) {
$rulesList = explode('|', $ruleString);
foreach ($rulesList as $rule) {
$ruleParts = explode(':', $rule);
$ruleName = $ruleParts[0];
$ruleParam = $ruleParts[1] ?? null;
if ($ruleName === 'required' && empty($data[$field])) {
$errors[$field] = "Het veld {$field} is verplicht";
}
if ($ruleName === 'min' && isset($data[$field]) && strlen($data[$field]) < $ruleParam) {
$errors[$field] = "Het veld {$field} moet minstens {$ruleParam} karakters lang zijn";
}
if ($ruleName === 'max' && isset($data[$field]) && strlen($data[$field]) > $ruleParam) {
$errors[$field] = "Het veld {$field} mag maximaal {$ruleParam} karakters lang zijn";
}
if ($ruleName === 'date' && isset($data[$field]) && !$this->isValidDate($data[$field])) {
$errors[$field] = "Het veld {$field} moet een geldige datum zijn";
}
}
}
return $errors;
}
protected function isValidDate($date) {
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date);
}
protected function isLoggedIn() {
return isset($_SESSION['user_id']);
}
protected function requireAuth() {
if (!$this->isLoggedIn()) {
$_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
$this->redirect('/login');
}
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Controllers;
use App\Models\Article;
use App\Models\Season;
use App\Models\User;
class DashboardController extends Controller {
public function index() {
$this->requireAuth();
$currentSeason = Season::getCurrent();
$userId = $_SESSION['user_id'];
// Get articles by current user
$userArticles = Article::all();
// Get all users
$users = User::all();
$this->view('dashboard', [
'currentSeason' => $currentSeason,
'userArticles' => $userArticles,
'users' => $users
]);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Controllers;
use App\Models\Article;
use App\Models\Season;
class HomeController extends Controller {
public function index() {
// Always show the public home page now
$this->redirect('/');
}
public function test() {
$this->view('test', ['page_title' => 'Test']);
}
}
+285
View File
@@ -0,0 +1,285 @@
<?php
namespace App\Controllers;
use App\Models\Article;
use App\Models\Season;
use App\Models\User;
class PdfController extends Controller {
public function generate() {
$this->requireAuth();
$seasonId = $_GET['season_id'] ?? null;
if ($seasonId) {
$articles = Article::getBySeason($seasonId);
$season = Season::find($seasonId);
} else {
$articles = Article::all('published');
$season = Season::getCurrent();
}
if (empty($articles)) {
$_SESSION['error'] = 'Geen artikelen gevonden voor deze editie';
$this->redirect('/articles');
}
// Generate LaTeX
$latexContent = $this->generateLatex($articles, $season);
// Save LaTeX file
$latexDir = $this->config['latex_paths']['output'];
if (!is_dir($latexDir)) {
mkdir($latexDir, 0755, true);
}
$latexFilename = $latexDir . 'ProostenKrant_' . ($season ? $season['name'] : 'Algemeen') . '_' . date('Y-m-d') . '.tex';
file_put_contents($latexFilename, $latexContent);
// Generate PDF using LaTeX compiler
$this->compileLatex($latexFilename);
$_SESSION['success'] = 'PDF succesvol gegenereerd!';
$this->redirect('/articles');
}
private function generateLatex($articles, $season = null) {
$config = $this->config;
$latex = "\\documentclass{article}\\
";
$latex .= "\\usepackage[utf8]{inputenc}\\
";
$latex .= "\\usepackage[T1]{fontenc}\\
";
$latex .= "\\usepackage{microtype}\\
";
$latex .= "\\usepackage[dutch]{babel}\\
";
$latex .= "\\usepackage{newspaper}\\
";
$latex .= "\\usepackage{graphicx}\\
";
$latex .= "\\usepackage{tikz}\\
";
$latex .= "\\usepackage{multicol}\\
";
$latex .= "\\usepackage{geometry}\\
";
$latex .= "\\usepackage{array}\\
";
$latex .= "\\usepackage{wrapfig}\\
";
$latex .= "\\usepackage{lipsum}\\
";
$latex .= "\\usepackage{newtxtext,newtxmath}\\
";
$latex .= "\\usepackage{etoolbox}\\
";
// Custom styling from original project
$latex .= "\\newcommand{\\headlinestyle}{\\itshape\\huge}\\
";
$latex .= "\\newcommand{\\bylinestyle}{\\scshape\\Large}\\
";
$latex .= "\\patchcmd{\\headline}{#1}{\\headlinestyle #1}{}{}\\
";
$latex .= "\\patchcmd{\\byline}{#1}{\\bylinestyle #1}{}{}\\
";
// Improved image handling commands
$latex .= $this->getImageCommands();
// Document metadata
$latex .= "\\\\date{\\today}\\
";
$latex .= "\\currentvolume{1}\\
";
$latex .= "\\currentissue{1}\\
";
$latex .= "\\SetPaperName{Proo{s:}tenKrant}\\
";
$latex .= "\\SetHeaderName{ProostenKrant}\\
";
$latex .= "\\SetPaperLocation{$config['app_location']}\\
";
$latex .= "\\SetPaperSlogan{{$config['app_slogan']}}\\
";
$latex .= "\\SetPaperPrice{$config['app_price']}\\
";
$latex .= "\\\\begin{document}\\
";
$latex .= "\\maketitle\\\\\\
";
// Determine season name
$seasonName = $season ? $season['name'] : 'Algemeen';
$latex .= "\\\\noindent\\textbf{\\LARGE Editie: {$seasonName}}\\
";
$latex .= "\\\\\\
";
$latex .= "\\\\begin{multicols}{3}\\
";
// Add articles
foreach ($articles as $article) {
$latex .= $this->articleToLatex($article);
}
$latex .= "\\\\end{multicols}\\
";
$latex .= "\\end{document}";
return $latex;
}
private function getImageCommands() {
return
"\\newcommand{\\showInlineImage}[2]\n" .
"{\n" .
" \\begin{center}\n" .
" \\begin{minipage}{0.9\\columnwidth}\n" .
" \\centering\n" .
" \\framebox{\n" .
" \\resizebox{\\linewidth}{!}{\\\\includegraphics{#1}}\n" .
" }\\\\\n" .
" \\small #2\\\n" .
" \\end{minipage}\n" .
" \\end{center}\n" .
"}\n\n" .
"\\newcommand{\\showPortraitImage}[2]\n" .
"{\n" .
" \\begin{minipage}[t]{0.45\\columnwidth}\n" .
" \\centering\n" .
" \\begin{tikzpicture}\n" .
" \\clip (0,0) circle (1.5cm);\n" .
" \\node[anchor=center] at (0,0) {\\\\includegraphics[width=3cm, height=3cm, keepaspectratio]{#1}};\n" .
" \\end{tikzpicture}\n" .
" \\smallskip\\\n" .
" #2\\\n" .
" \\end{minipage}\n" .
"}\n\n" .
"\\newcommand{\\showLandscapeImage}[2]\n" .
"{\n" .
" \\begin{minipage}[t]{\\columnwidth}\n" .
" \\centering\n" .
" \\includegraphics[width=\\linewidth, keepaspectratio]{#1}\\par\n" .
" \\small #2\\\n" .
" \\end{minipage}\n" .
"}\n\n" .
"\\newcommand{\\showOneWriter}[2]\n" .
"{\n" .
" \\hfill\n" .
" \\begin{minipage}[t]{0.4\\columnwidth}\n" .
" \\centering\n" .
" \\begin{tikzpicture}\n" .
" \\clip (0,0) circle (1.2cm);\n" .
" \\node[anchor=center] at (0,0) {\\\\includegraphics[width=2.5cm, height=2.5cm, keepaspectratio]{#1}};\n" .
" \\end{tikzpicture}\n" .
" \\\n" .
" \\small #2\\\n" .
" \\end{minipage}\n" .
" \\hfill\n" .
"}\n\n" .
"\\newcommand{\\showTwoWriters}[4]\n" .
"{\n" .
" \\begin{center}\n" .
" \\begin{minipage}[t]{0.8\\columnwidth}\n" .
" \\centering\n" .
" \\begin{tikzpicture}\n" .
" \\clip (0,0) circle (1cm);\n" .
" \\node[anchor=west] at (0,0) {\\\\includegraphics[width=2cm, height=2cm, keepaspectratio]{#1}};\n" .
" \\end{tikzpicture}\n" .
" \\hspace{0.5cm}\n" .
" \\begin{tikzpicture}\n" .
" \\clip (0,0) circle (1cm);\n" .
" \\node[anchor=east] at (0,0) {\\\\includegraphics[width=2cm, height=2cm, keepaspectratio]{#3}};\n" .
" \\end{tikzpicture}\n" .
" \\\n" .
" \\small #2 \\quad #4\\\n" .
" \\end{minipage}\n" .
" \\end{center}\n" .
"}\n\n";
}
private function articleToLatex($article) {
$latex = "\\headline{{$article['title']}}\\
";
$latex .= "\\byline{{$article['event_date']}}{{$article['author_name']}}\\
";
// Get base public path from config
$uploadPaths = $this->config['upload_paths'] ?? [];
$profilesPath = $uploadPaths['profiles'] ?? __DIR__ . '/../../public/images/profiles/';
$articlesPath = $uploadPaths['articles'] ?? __DIR__ . '/../../public/images/articles/';
$publicPath = dirname(dirname($profilesPath)) . '/';
// Get participants
$participantIds = explode(',', $article['participants']);
$participants = User::getByIds($participantIds);
if (count($participants) === 1) {
$profileImage = $participants[0]['profile_image'] ? $publicPath . $participants[0]['profile_image'] : $profilesPath . 'Unknown.jpg';
$latex .= "\\showOneWriter{{$profileImage}}{{$participants[0]['display_name']}}\\
";
} elseif (count($participants) === 2) {
$profileImage1 = $participants[0]['profile_image'] ? $publicPath . $participants[0]['profile_image'] : $profilesPath . 'Unknown.jpg';
$profileImage2 = $participants[1]['profile_image'] ? $publicPath . $participants[1]['profile_image'] : $profilesPath . 'Unknown.jpg';
$latex .= "\\showTwoWriters{{$profileImage1}}{{$participants[0]['display_name']}}{{$profileImage2}}{{$participants[1]['display_name']}}\\
";
}
// Content
$content = $article['content'];
// Escape special LaTeX characters
$content = str_replace(['#', '$', '%', '&', '_', '{', '}', '\\', '~', '^'],
['\\#', '\\$', '\\%', '\\&', '\\_', '\\{', '\\}', '\\\\', '\\~', '\\^'],
$content);
$latex .= "\\par {$content}\\par\n\\\n";
// Article image
if ($article['article_image']) {
$imagePath = $publicPath . $article['article_image'];
// Check if image exists
if (file_exists($imagePath)) {
$latex .= "\\showInlineImage{{$imagePath}}{}\\
";
}
}
$latex .= "\\\n\\\n";
return $latex;
}
private function compileLatex($latexFilename) {
$outputDir = pathinfo($latexFilename, PATHINFO_DIRNAME);
$baseName = pathinfo($latexFilename, PATHINFO_FILENAME);
// Run pdflatex twice to resolve references
$commands = [
"cd {$outputDir} && pdflatex -interaction=nonstopmode {$latexFilename}",
"cd {$outputDir} && pdflatex -interaction=nonstopmode {$latexFilename}",
];
foreach ($commands as $command) {
shell_exec($command);
}
// Clean up temporary files
$tempFiles = [$baseName . '.aux', $baseName . '.log', $baseName . '.out'];
foreach ($tempFiles as $tempFile) {
$tempPath = $outputDir . '/' . $tempFile;
if (file_exists($tempPath)) {
unlink($tempPath);
}
}
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace App\Controllers;
use App\Models\Article;
use App\Models\Season;
use App\Models\User;
class PublicController extends Controller {
public function index() {
$config = $this->config;
// Get current season
$currentSeason = Season::getCurrent();
// Get recent articles (draft status for WIP)
$draftArticles = Article::all('draft');
// Get published articles for archive preview
$publishedArticles = Article::all('published');
// Scan for PDF files in multiple directories
$pdfFiles = [];
// Check storage/latex directory
$pdfDirs = [
__DIR__ . '/../../storage/latex/',
__DIR__ . '/../../../Latex/',
__DIR__ . '/../../../Build/',
__DIR__ . '/../../../Proostenkrant.NET/',
];
foreach ($pdfDirs as $pdfDir) {
if (is_dir($pdfDir)) {
$files = scandir($pdfDir);
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'pdf') {
$filepath = $pdfDir . $file;
$pdfFiles[] = [
'filename' => $file,
'path' => $this->getPdfPath($pdfDir, $file),
'date' => date('Y-m-d', filemtime($filepath)),
'fullpath' => $filepath
];
}
}
}
}
// Sort by date descending
usort($pdfFiles, function($a, $b) {
return strtotime($b['date']) - strtotime($a['date']);
});
$users = User::all();
$usersById = [];
foreach ($users as $user) {
$usersById[$user['id']] = $user['display_name'];
}
$this->view('public/home', [
'page_title' => 'Home',
'show_nav' => true,
'config' => $config,
'currentSeason' => $currentSeason,
'draftArticles' => $draftArticles,
'publishedArticles' => $publishedArticles,
'pdfFiles' => $pdfFiles,
'usersById' => $usersById
]);
}
private function getPdfPath($directory, $filename) {
// Map directory to public URL
$baseDir = __DIR__ . '/../../';
if (strpos($directory, '/storage/latex') !== false) {
return '/storage/latex/' . $filename;
} elseif (strpos($directory, '/Latex/') !== false) {
return '/Latex/' . $filename;
} elseif (strpos($directory, '/Build/') !== false) {
return '/Build/' . $filename;
} elseif (strpos($directory, '/Proostenkrant.NET/') !== false) {
return '/Proostenkrant.NET/' . $filename;
}
return '/' . basename($directory) . '/' . $filename;
}
public function archive() {
$pdfFiles = [];
// Check multiple directories for PDFs
$pdfDirs = [
__DIR__ . '/../../storage/latex/',
__DIR__ . '/../../../Latex/',
__DIR__ . '/../../../Build/',
__DIR__ . '/../../../Proostenkrant.NET/',
];
foreach ($pdfDirs as $pdfDir) {
if (is_dir($pdfDir)) {
$files = scandir($pdfDir);
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'pdf') {
$filepath = $pdfDir . $file;
// Try to extract date and edition from filename
$basename = pathinfo($file, PATHINFO_FILENAME);
$date = date('Y-m-d', filemtime($filepath));
// Check if filename has a date pattern
if (preg_match('/(\d{4}-\d{2}-\d{2})/', $basename, $matches)) {
$date = $matches[1];
}
$editionName = str_replace(['_', '-'], ' ', $basename);
$pdfFiles[] = [
'filename' => $file,
'path' => $this->getPdfPath($pdfDir, $file),
'date' => $date,
'edition_name' => $editionName,
'fullpath' => $filepath
];
}
}
}
}
// Sort by date descending
usort($pdfFiles, function($a, $b) {
return strtotime($b['date']) - strtotime($a['date']);
});
$this->view('public/archive', [
'page_title' => 'Archief',
'show_nav' => true,
'pdfFiles' => $pdfFiles
]);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Controllers;
use App\Models\Season;
use App\Models\Article;
class SeasonController extends Controller {
public function index() {
$this->requireAuth();
$seasons = Season::all();
$currentSeason = Season::getCurrent();
foreach ($seasons as &$season) {
$season['article_count'] = count(Article::getBySeason($season['id']));
}
$this->view('seasons/index', [
'seasons' => $seasons,
'currentSeason' => $currentSeason
]);
}
public function show($id) {
$season = Season::find($id);
if (!$season) {
$this->redirect('/seasons');
}
$articles = Article::getBySeason($id);
$this->view('seasons/show', [
'season' => $season,
'articles' => $articles
]);
}
}
+287
View File
@@ -0,0 +1,287 @@
<?php
namespace App\Controllers;
use App\Models\Article;
use App\Models\User;
class SubmissionController extends Controller {
public function showPassphrase() {
$this->view('submission/passphrase', [
'page_title' => 'Artikel Indienen - Beveiliging',
'show_nav' => true,
'next_page' => '/submit/form'
]);
}
public function verifyPassphrase() {
$passphrase = $_POST['passphrase'] ?? '';
$universalPassword = $this->config['universal_password'] ?? 'ProostenKrant2024!';
if ($passphrase === $universalPassword) {
// Store in session that user has passed the passphrase check
$_SESSION['passphrase_verified'] = true;
$_SESSION['passphrase_timestamp'] = time();
// Redirect to submission form
$this->redirect('/submit/form');
} else {
$_SESSION['error'] = 'Onjuiste paszin. Probeer het opnieuw.';
$this->redirect('/submit');
}
}
public function showForm() {
// Check if passphrase is verified OR if we're coming from article edit flow
$isEditFlow = isset($_SESSION['edit_article_id']) && $_SESSION['edit_article_id'];
if (!$isEditFlow && (!isset($_SESSION['passphrase_verified']) || !$_SESSION['passphrase_verified'])) {
$this->redirect('/submit');
}
$users = User::all();
// Get old input if validation failed
$oldInput = $_SESSION['old_input'] ?? [];
unset($_SESSION['old_input']);
$errors = $_SESSION['errors'] ?? [];
unset($_SESSION['errors']);
// Check if we're editing an existing article
$articleId = $_SESSION['edit_article_id'] ?? null;
$article = null;
if ($articleId) {
$article = Article::find($articleId);
unset($_SESSION['edit_article_id']);
}
$this->view('submission/form', [
'page_title' => $article ? 'Artikel Bewerken' : 'Artikel Indienen',
'show_nav' => true,
'users' => $users,
'oldInput' => $oldInput,
'errors' => $errors,
'article' => $article
]);
}
public function store() {
// Check if passphrase is verified OR if we're coming from article edit flow (which already verified)
$isEditFlow = isset($_POST['article_id']) && $_POST['article_id'];
if (!$isEditFlow && (!isset($_SESSION['passphrase_verified']) || !$_SESSION['passphrase_verified'])) {
$_SESSION['error'] = 'Je moet eerst de beveiligingspaszin invoeren.';
$this->redirect('/submit');
}
$users = User::all();
$userMap = [];
foreach ($users as $user) {
$userMap[$user['id']] = $user['display_name'];
}
$validationRules = [
'title' => 'required|min:5|max:255',
'content' => 'required|min:10',
'writer' => 'required',
'participants' => 'required',
];
$errors = $this->validate($_POST, $validationRules);
// Check if image was uploaded
$articleImage = null;
if (isset($_FILES['image'])) {
if ($_FILES['image']['error'] === UPLOAD_ERR_OK) {
// Use configured upload path for articles
$uploadDir = $this->config['upload_paths']['articles'] ?? __DIR__ . '/../../public/images/articles/';
$filename = uniqid() . '_' . basename($_FILES['image']['name']);
$targetPath = $uploadDir . '/' . $filename;
// Make sure the directory exists and is writable
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
// Ensure directory has proper permissions
if (!is_writable($uploadDir)) {
chmod($uploadDir, 0777);
}
if (move_uploaded_file($_FILES['image']['tmp_name'], $targetPath)) {
$articleImage = 'images/articles/' . $filename;
// Store in session to show preview on redirect
$_SESSION['uploaded_image'] = $articleImage;
} else {
$error = error_get_last();
$errors['image'] = 'Fout bij het uploaden van de afbeelding: ' . ($error ? $error['message'] : 'Onbekende fout');
}
} else {
// Image upload error - no file selected
$errors['image'] = 'Selecteer een afbeelding om te uploaden.';
}
} else {
// No image file was sent
$errors['image'] = 'Een afbeelding is verplicht.';
}
if (!empty($errors)) {
$_SESSION['errors'] = $errors;
$_SESSION['old_input'] = $_POST;
$this->redirect('/submit/form');
}
// Get the writer's user ID
$writerId = $_POST['writer'];
$writerName = $userMap[$writerId] ?? 'Onbekend';
// Process participants
$participantIds = $_POST['participants'] ?? [];
if (!is_array($participantIds)) {
$participantIds = [$participantIds];
}
// Ensure writer is in participants
if (!in_array($writerId, $participantIds)) {
array_unshift($participantIds, $writerId);
}
$articleData = [
'title' => $_POST['title'],
'content' => $_POST['content'],
'author_id' => $writerId,
'participants' => implode(',', $participantIds),
'event_date' => $_POST['event_date'] ?? date('Y-m-d'),
'article_image' => $articleImage,
'status' => 'draft',
'created_at' => date('Y-m-d H:i:s'),
];
// Check if we're editing or creating new
$articleId = $_POST['article_id'] ?? null;
if ($articleId) {
// For existing articles, preserve the status
$existingArticle = Article::find($articleId);
if ($existingArticle) {
$articleData['status'] = $existingArticle['status'];
}
Article::update($articleId, $articleData);
$_SESSION['success'] = 'Artikel succesvol bijgewerkt!';
} else {
$articleId = Article::create($articleData);
$_SESSION['success'] = 'Artikel succesvol ingediend!';
}
// Clear passphrase verification for security
unset($_SESSION['passphrase_verified']);
unset($_SESSION['passphrase_timestamp']);
// Store article ID for confirmation
$_SESSION['submitted_article_id'] = $articleId;
$this->redirect('/submit/confirmation');
}
public function showConfirmation() {
// Check if there's a submitted article
$articleId = $_SESSION['submitted_article_id'] ?? null;
unset($_SESSION['submitted_article_id']);
$article = null;
if ($articleId) {
$article = Article::find($articleId);
}
$this->view('submission/confirmation', [
'page_title' => 'Bevestiging',
'show_nav' => true,
'article' => $article
]);
}
public function showEditList() {
// Check if passphrase is verified
if (!isset($_SESSION['passphrase_verified']) || !$_SESSION['passphrase_verified']) {
$this->redirect('/submit/edit');
}
$articles = Article::all('draft');
$users = User::all();
$usersById = [];
foreach ($users as $user) {
$usersById[$user['id']] = $user['display_name'];
}
$this->view('submission/edit_list', [
'page_title' => 'Artikelen Bewerken',
'show_nav' => true,
'articles' => $articles,
'usersById' => $usersById
]);
}
public function verifyEditPassphrase() {
$passphrase = $_POST['passphrase'] ?? '';
$universalPassword = $this->config['universal_password'] ?? 'ProostenKrant2024!';
if ($passphrase === $universalPassword) {
$_SESSION['passphrase_verified'] = true;
$_SESSION['passphrase_timestamp'] = time();
$this->redirect('/submit/edit/list');
} else {
$_SESSION['error'] = 'Onjuiste paszin. Probeer het opnieuw.';
$this->redirect('/submit/edit');
}
}
public function showEditPassphrase() {
$this->view('submission/edit_passphrase', [
'page_title' => 'Artikelen Bewerken - Beveiliging',
'show_nav' => true
]);
}
public function showWriterVerification($articleId) {
$article = Article::find($articleId);
if (!$article) {
$_SESSION['error'] = 'Artikel niet gevonden.';
$this->redirect('/submit/edit/list');
}
$users = User::all();
$this->view('submission/verify_writer', [
'page_title' => 'Verifieer Schrijver',
'show_nav' => true,
'article' => $article,
'users' => $users
]);
}
public function verifyWriter() {
$articleId = $_POST['article_id'] ?? null;
$writerId = $_POST['writer_id'] ?? null;
if (!$articleId || !$writerId) {
$_SESSION['error'] = 'Ongeldige verificatie.';
$this->redirect('/submit/edit/list');
}
$article = Article::find($articleId);
if (!$article) {
$_SESSION['error'] = 'Artikel niet gevonden.';
$this->redirect('/submit/edit/list');
}
if ($article['author_id'] != $writerId) {
$_SESSION['error'] = 'Alleen de schrijver van het artikel kan deze bewerken.';
$this->redirect('/submit/edit/list');
}
// Store article ID in session for editing
$_SESSION['edit_article_id'] = $articleId;
$this->redirect('/submit/form');
}
}
+138
View File
@@ -0,0 +1,138 @@
<?php
namespace App\Controllers;
use App\Models\User;
class UploadController extends Controller {
public function upload() {
$this->requireAuth();
if (!isset($_FILES['file'])) {
echo json_encode(['error' => 'No file uploaded']);
exit();
}
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['error' => 'Upload error: ' . $file['error']]);
exit();
}
$type = $_POST['type'] ?? 'articles';
$validTypes = ['articles', 'profiles'];
if (!in_array($type, $validTypes)) {
echo json_encode(['error' => 'Invalid upload type']);
exit();
}
$uploadDir = $this->config['upload_paths'][$type];
// Create directory if it doesn't exist
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Generate unique filename
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $ext;
$targetPath = $uploadDir . $filename;
// Validate image
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array(strtolower($ext), $allowedExtensions)) {
echo json_encode(['error' => 'Only images are allowed']);
exit();
}
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
echo json_encode(['error' => 'Invalid image file']);
exit();
}
// Resize large images to prevent issues with LaTeX
if ($imageInfo[0] > 4000 || $imageInfo[1] > 4000) {
$this->resizeImage($file['tmp_name'], $targetPath, 2000, 2000);
} else {
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
echo json_encode(['error' => 'Failed to move uploaded file']);
exit();
}
}
echo json_encode([
'success' => true,
'filename' => $filename,
'path' => 'images/' . $type . '/' . $filename,
'url' => '/images/' . $type . '/' . $filename
]);
}
private function resizeImage($source, $target, $maxWidth, $maxHeight) {
$imageInfo = getimagesize($source);
$width = $imageInfo[0];
$height = $imageInfo[1];
$mime = $imageInfo['mime'];
$ratio = min($maxWidth / $width, $maxHeight / $height);
$newWidth = (int)($width * $ratio);
$newHeight = (int)($height * $ratio);
$sourceImage = null;
switch ($mime) {
case 'image/jpeg':
$sourceImage = imagecreatefromjpeg($source);
break;
case 'image/png':
$sourceImage = imagecreatefrompng($source);
break;
case 'image/gif':
$sourceImage = imagecreatefromgif($source);
break;
case 'image/webp':
$sourceImage = imagecreatefromwebp($source);
break;
default:
move_uploaded_file($source, $target);
return;
}
if (!$sourceImage) {
move_uploaded_file($source, $target);
return;
}
$destinationImage = imagecreatetruecolor($newWidth, $newHeight);
// Preserve transparency for PNG and GIF
if ($mime === 'image/png' || $mime === 'image/gif') {
imagealphablending($destinationImage, false);
imagesavealpha($destinationImage, true);
$transparent = imagecolorallocatealpha($destinationImage, 255, 255, 255, 127);
imagefilledrectangle($destinationImage, 0, 0, $newWidth, $newHeight, $transparent);
}
imagecopyresampled($destinationImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
switch ($mime) {
case 'image/jpeg':
imagejpeg($destinationImage, $target, 90);
break;
case 'image/png':
imagepng($destinationImage, $target, 9);
break;
case 'image/gif':
imagegif($destinationImage, $target);
break;
case 'image/webp':
imagewebp($destinationImage, $target, 90);
break;
}
imagedestroy($sourceImage);
imagedestroy($destinationImage);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Helpers;
class AuthHelper {
public static function check() {
return isset($_SESSION['is_logged_in']) && $_SESSION['is_logged_in'] === true;
}
public static function user() {
if (!self::check()) {
return null;
}
return [
'id' => $_SESSION['user_id'] ?? null,
'username' => $_SESSION['username'] ?? null,
'display_name' => $_SESSION['display_name'] ?? null,
];
}
public static function requireAuth() {
if (!self::check()) {
$_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'] ?? '/';
header('Location: /login');
exit();
}
}
public static function redirectIfAuthenticated() {
if (self::check()) {
header('Location: /dashboard');
exit();
}
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace App\Helpers;
class ImageHelper {
public static function resizeImage($source, $target, $maxWidth, $maxHeight, $quality = 90) {
if (!file_exists($source)) {
return false;
}
$imageInfo = getimagesize($source);
if ($imageInfo === false) {
return false;
}
list($width, $height, $type) = $imageInfo;
// If image is smaller than max dimensions, just copy it
if ($width <= $maxWidth && $height <= $maxHeight) {
return copy($source, $target);
}
// Calculate new dimensions maintaining aspect ratio
$ratio = min($maxWidth / $width, $maxHeight / $height);
$newWidth = (int)($width * $ratio);
$newHeight = (int)($height * $ratio);
// Create source image resource
$sourceImage = null;
switch ($type) {
case IMAGETYPE_JPEG:
$sourceImage = imagecreatefromjpeg($source);
break;
case IMAGETYPE_PNG:
$sourceImage = imagecreatefrompng($source);
break;
case IMAGETYPE_GIF:
$sourceImage = imagecreatefromgif($source);
break;
case IMAGETYPE_WEBP:
$sourceImage = imagecreatefromwebp($source);
break;
default:
return copy($source, $target);
}
if (!$sourceImage) {
return false;
}
// Create destination image
$destinationImage = imagecreatetruecolor($newWidth, $newHeight);
// Preserve transparency for PNG and GIF
if ($type === IMAGETYPE_PNG || $type === IMAGETYPE_GIF) {
imagealphablending($destinationImage, false);
imagesavealpha($destinationImage, true);
$transparent = imagecolorallocatealpha($destinationImage, 255, 255, 255, 127);
imagefilledrectangle($destinationImage, 0, 0, $newWidth, $newHeight, $transparent);
}
// Resample image
imagecopyresampled($destinationImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
// Save image
switch ($type) {
case IMAGETYPE_JPEG:
return imagejpeg($destinationImage, $target, $quality);
case IMAGETYPE_PNG:
return imagepng($destinationImage, $target, 9);
case IMAGETYPE_GIF:
return imagegif($destinationImage, $target);
case IMAGETYPE_WEBP:
return imagewebp($destinationImage, $target, $quality);
default:
return false;
}
}
public static function isImagePortrait($imagePath) {
if (!file_exists($imagePath)) {
return false;
}
$imageInfo = getimagesize($imagePath);
if ($imageInfo === false) {
return false;
}
list($width, $height) = $imageInfo;
return $height > $width;
}
public static function isImageLandscape($imagePath) {
if (!file_exists($imagePath)) {
return false;
}
$imageInfo = getimagesize($imagePath);
if ($imageInfo === false) {
return false;
}
list($width, $height) = $imageInfo;
return $width > $height;
}
public static function getImageOrientation($imagePath) {
if (!file_exists($imagePath)) {
return 'unknown';
}
$imageInfo = getimagesize($imagePath);
if ($imageInfo === false) {
return 'unknown';
}
list($width, $height) = $imageInfo;
if ($width > $height) {
return 'landscape';
} elseif ($height > $width) {
return 'portrait';
} else {
return 'square';
}
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Models;
class Article {
private static $table = 'articles';
public static function all($status = 'published') {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT a.*, u.display_name as author_name, s.name as season_name " .
"FROM " . self::$table . " a " .
"LEFT JOIN users u ON a.author_id = u.id " .
"LEFT JOIN seasons s ON a.season_id = s.id " .
"WHERE a.status = ? ORDER BY a.event_date DESC, a.created_at DESC",
[$status]
);
}
public static function find($id) {
$db = Database::getInstance();
return $db->fetch(
"SELECT a.*, u.display_name as author_name, s.name as season_name " .
"FROM " . self::$table . " a " .
"LEFT JOIN users u ON a.author_id = u.id " .
"LEFT JOIN seasons s ON a.season_id = s.id " .
"WHERE a.id = ?",
[$id]
);
}
public static function create($data) {
$db = Database::getInstance();
// Process participants
if (isset($data['participants']) && is_array($data['participants'])) {
$data['participants'] = implode(',', $data['participants']);
}
return $db->insert(self::$table, $data);
}
public static function update($id, $data) {
$db = Database::getInstance();
if (isset($data['participants']) && is_array($data['participants'])) {
$data['participants'] = implode(',', $data['participants']);
}
return $db->update(self::$table, $data, ['id' => $id]);
}
public static function delete($id) {
$db = Database::getInstance();
return $db->delete(self::$table, ['id' => $id]);
}
public static function getBySeason($seasonId) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT a.*, u.display_name as author_name " .
"FROM " . self::$table . " a " .
"LEFT JOIN users u ON a.author_id = u.id " .
"WHERE a.season_id = ? AND a.status = 'published' " .
"ORDER BY a.event_date DESC",
[$seasonId]
);
}
public static function getByUser($userId) {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT a.*, u.display_name as author_name " .
"FROM " . self::$table . " a " .
"LEFT JOIN users u ON a.author_id = u.id " .
"WHERE a.author_id = ? OR a.participants LIKE ? " .
"ORDER BY a.event_date DESC",
[$userId, "%,{$userId},%"]
);
}
public static function getCurrentSeasonId() {
$db = Database::getInstance();
$result = $db->fetch(
"SELECT id FROM seasons " .
"WHERE start_date <= CURDATE() AND end_date >= CURDATE() " .
"ORDER BY start_date DESC LIMIT 1"
);
return $result ? $result['id'] : null;
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace App\Models;
use PDO;
use PDOException;
class Database {
private static $instance = null;
private $connection;
private function __construct() {
$config = include __DIR__ . '/../../config/database.php';
$dsn = "mysql:host={$config['host']};dbname={$config['database']};charset={$config['charset']}";
try {
$this->connection = new PDO(
$dsn,
$config['username'],
$config['password'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->connection;
}
public function query($sql, $params = []) {
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
return $stmt;
}
public function fetchAll($sql, $params = []) {
return $this->query($sql, $params)->fetchAll();
}
public function fetch($sql, $params = []) {
return $this->query($sql, $params)->fetch();
}
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$stmt = $this->connection->prepare($sql);
$stmt->execute($data);
return $this->connection->lastInsertId();
}
public function update($table, $data, $where) {
$setParts = [];
foreach ($data as $key => $value) {
$setParts[] = "{$key} = :{$key}";
}
$whereParts = [];
foreach ($where as $key => $value) {
$whereParts[] = "{$key} = :where_{$key}";
$data["where_{$key}"] = $value;
}
$sql = "UPDATE {$table} SET " . implode(', ', $setParts) .
" WHERE " . implode(' AND ', $whereParts);
$stmt = $this->connection->prepare($sql);
return $stmt->execute($data);
}
public function delete($table, $where) {
$whereParts = [];
$params = [];
foreach ($where as $key => $value) {
$whereParts[] = "{$key} = :{$key}";
$params[$key] = $value;
}
$sql = "DELETE FROM {$table} WHERE " . implode(' AND ', $whereParts);
return $this->query($sql, $params)->rowCount();
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Models;
class Season {
private static $table = 'seasons';
public static function all() {
$db = Database::getInstance();
return $db->fetchAll("SELECT * FROM " . self::$table . " ORDER BY start_date");
}
public static function find($id) {
$db = Database::getInstance();
return $db->fetch("SELECT * FROM " . self::$table . " WHERE id = ?", [$id]);
}
public static function getActive() {
$db = Database::getInstance();
return $db->fetchAll(
"SELECT * FROM " . self::$table . " WHERE is_active = 1 ORDER BY start_date"
);
}
public static function getCurrent() {
$db = Database::getInstance();
return $db->fetch(
"SELECT * FROM " . self::$table . " " .
"WHERE start_date <= CURDATE() AND end_date >= CURDATE() " .
"ORDER BY start_date DESC LIMIT 1"
);
}
public static function getNext() {
$db = Database::getInstance();
return $db->fetch(
"SELECT * FROM " . self::$table . " " .
"WHERE start_date > CURDATE() " .
"ORDER BY start_date ASC LIMIT 1"
);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
class Setting {
private static $table = 'settings';
public static function get($key, $default = null) {
$db = Database::getInstance();
$result = $db->fetch("SELECT value FROM " . self::$table . " WHERE key_name = ?", [$key]);
return $result ? $result['value'] : $default;
}
public static function set($key, $value) {
$db = Database::getInstance();
$existing = $db->fetch("SELECT id FROM " . self::$table . " WHERE key_name = ?", [$key]);
if ($existing) {
return $db->update(self::$table, ['value' => $value], ['key_name' => $key]);
} else {
return $db->insert(self::$table, ['key_name' => $key, 'value' => $value]);
}
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
class User {
private static $table = 'users';
public static function all() {
$db = Database::getInstance();
return $db->fetchAll("SELECT * FROM " . self::$table . " WHERE is_active = 1 ORDER BY display_name");
}
public static function find($id) {
$db = Database::getInstance();
return $db->fetch("SELECT * FROM " . self::$table . " WHERE id = ?", [$id]);
}
public static function findByUsername($username) {
$db = Database::getInstance();
return $db->fetch("SELECT * FROM " . self::$table . " WHERE username = ? AND is_active = 1", [$username]);
}
public static function getByIds($ids) {
if (empty($ids)) return [];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$db = Database::getInstance();
return $db->fetchAll("SELECT * FROM " . self::$table . " WHERE id IN ({$placeholders}) AND is_active = 1", $ids);
}
public static function updateProfileImage($userId, $imagePath) {
$db = Database::getInstance();
return $db->update(self::$table, ['profile_image' => $imagePath], ['id' => $userId]);
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
require 'controllers/Controller.php';
require 'controllers/TestController.php';
$controller = new \App\Controllers\TestController();
$controller->index();
+52
View File
@@ -0,0 +1,52 @@
<?php
$page_title = 'Artikelen';
ob_start();
?>
<div class="articles-list">
<div class="header-actions">
<h2>Alle Artikelen</h2>
<a href="/articles/new" class="btn">Nieuw Artikel</a>
</div>
<?php if (!empty($articles)): ?>
<table class="articles-table">
<thead>
<tr>
<th>Titel</th>
<th>Auteur</th>
<th>Datum</th>
<th>Seizoen</th>
<th>Status</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
<?php foreach ($articles as $article): ?>
<tr>
<td><?= htmlspecialchars($article['title']) ?></td>
<td><?= htmlspecialchars($article['author_name'] ?? 'Onbekend') ?></td>
<td><?= date('d-m-Y', strtotime($article['event_date'])) ?></td>
<td><?= htmlspecialchars($article['season_name'] ?? 'N/A') ?></td>
<td>
<span class="status-badge <?= strtolower($article['status']) ?>">
<?= htmlspecialchars($article['status']) ?>
</span>
</td>
<td>
<a href="/articles/<?= $article['id'] ?>" class="btn-small">Bekijken</a>
<a href="/articles/<?= $article['id'] ?>/verify-writer" class="edit-icon" title="Bewerken">✏️</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>Er zijn nog geen artikelen.</p>
<?php endif; ?>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+128
View File
@@ -0,0 +1,128 @@
<?php
$page_title = 'Nieuw Artikel';
ob_start();
// Get old input if validation failed
$oldInput = $_SESSION['old_input'] ?? [];
unset($_SESSION['old_input']);
$errors = $_SESSION['errors'] ?? [];
unset($_SESSION['errors']);
$users = \App\Models\User::all();
$seasons = \App\Models\Season::all();
$currentSeason = \App\Models\Season::getCurrent();
?>
<div class="article-form">
<h2>Nieuw Artikel Toevoegen</h2>
<form action="/articles" method="post" enctype="multipart/form-data">
<div class="form-row">
<div class="form-group">
<label for="title">Titel *</label>
<input type="text" name="title" id="title"
value="<?= htmlspecialchars($oldInput['title'] ?? '') ?>"
required class="form-control <?= isset($errors['title']) ? 'error' : '' ?>">
<?php if (isset($errors['title'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['title']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="content">Inhoud *</label>
<textarea name="content" id="content" rows="10"
required class="form-control <?= isset($errors['content']) ? 'error' : '' ?>"><?= htmlspecialchars($oldInput['content'] ?? '') ?></textarea>
<?php if (isset($errors['content'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['content']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="event_date">Datum van het evenement *</label>
<input type="date" name="event_date" id="event_date"
value="<?= htmlspecialchars($oldInput['event_date'] ?? date('Y-m-d')) ?>"
required class="form-control <?= isset($errors['event_date']) ? 'error' : '' ?>">
<?php if (isset($errors['event_date'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['event_date']) ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="author_id">Auteur *</label>
<select name="author_id" id="author_id" required
class="form-control <?= isset($errors['author_id']) ? 'error' : '' ?>">
<option value="">-- Selecteer auteur --</option>
<?php foreach ($users as $user): ?>
<option value="<?= $user['id'] ?>"
<?= (($oldInput['author_id'] ?? '') == $user['id'] || (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $user['id'])) ? 'selected' : '' ?>>
<?= htmlspecialchars($user['display_name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php if (isset($errors['author_id'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['author_id']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>Deelnemers</label>
<div class="checkbox-group">
<?php foreach ($users as $user): ?>
<label class="checkbox-label">
<input type="checkbox" name="participants[]"
value="<?= $user['id'] ?>"
<?= (isset($oldInput['participants']) && in_array($user['id'], $oldInput['participants'])) ? 'checked' : '' ?>>
<?= htmlspecialchars($user['display_name']) ?>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="season_id">Seizoen *</label>
<select name="season_id" id="season_id" required
class="form-control <?= isset($errors['season_id']) ? 'error' : '' ?>">
<option value="">-- Selecteer seizoen --</option>
<?php foreach ($seasons as $season): ?>
<option value="<?= $season['id'] ?>"
<?= (($oldInput['season_id'] ?? '') == $season['id'] || ($currentSeason && $currentSeason['id'] == $season['id'])) ? 'selected' : '' ?>>
<?= htmlspecialchars($season['name']) ?>
(<?= date('d-m-Y', strtotime($season['start_date'])) ?> -
<?= date('d-m-Y', strtotime($season['end_date'])) ?>)
</option>
<?php endforeach; ?>
</select>
<?php if (isset($errors['season_id'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['season_id']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="article_image">Afbeelding (optioneel)</label>
<input type="file" name="article_image" id="article_image"
accept="image/*" class="form-control">
<p class="help-text">Upload een foto bij het artikel. tantoon landscape als portret foto's worden ondersteund.</p>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Artikel Opslaan</button>
<a href="/articles" class="btn btn-secondary">Annuleren</a>
</div>
</form>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+57
View File
@@ -0,0 +1,57 @@
<?php
$page_title = htmlspecialchars($article['title'] ?? 'Artikel');
ob_start();
$participantNames = [];
foreach ($participants as $participant) {
$participantNames[] = htmlspecialchars($participant['display_name']);
}
$participantsString = implode(', ', $participantNames);
?>
<div class="article-detail">
<h2><?= htmlspecialchars($article['title']) ?></h2>
<div class="article-meta">
<span class="meta-item">
<strong>Auteur:</strong> <?= htmlspecialchars($article['author_name'] ?? 'Onbekend') ?>
</span>
<span class="meta-item">
<strong>Datum:</strong> <?= date('d-m-Y', strtotime($article['event_date'])) ?>
</span>
<span class="meta-item">
<strong>Seizoen:</strong> <?= htmlspecialchars($article['season_name'] ?? 'N/A') ?>
</span>
<span class="meta-item">
<strong>Deelnemers:</strong> <?= $participantsString ?>
</span>
<span class="meta-item">
<strong>Status:</strong>
<span class="status-badge <?= strtolower($article['status']) ?>">
<?= htmlspecialchars($article['status']) ?>
</span>
</span>
</div>
<?php if ($article['article_image']): ?>
<div class="article-image-container">
<img src="/<?= htmlspecialchars($article['article_image']) ?>"
alt="<?= htmlspecialchars($article['title']) ?>"
class="article-image">
</div>
<?php endif; ?>
<div class="article-content">
<?= nl2br(htmlspecialchars($article['content'])) ?>
</div>
<div class="article-actions">
<a href="/articles" class="btn btn-secondary">Terug naar Overzicht</a>
<a href="/articles/<?= $article['id'] ?>/edit" class="btn">Bewerken</a>
</div>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
@@ -0,0 +1,50 @@
<?php
$page_title = 'Beveiligingscontrole - Artikel Bewerken';
ob_start();
?>
<div class="passphrase-container">
<h2>Beveiligingscontrole</h2>
<?php if ($article): ?>
<div class="card mb-30">
<h4><?= htmlspecialchars($article['title']) ?></h4>
<p class="text-muted">
Voer het algemene passphrase in om dit artikel te bewerken.
</p>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-error">
<?= htmlspecialchars($_SESSION['error']) ?>
<?php unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<form action="/articles/verify-passphrase" method="post">
<div class="form-group">
<label for="passphrase">Algemeen Passphrase</label>
<input type="password"
name="passphrase"
id="passphrase"
class="form-control"
required
autocomplete="off"
placeholder="Voer het passphrase in">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
Verifieer en Bewerk Artikel
</button>
<a href="/articles" class="btn btn-secondary">Annuleren</a>
</div>
</form>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+58
View File
@@ -0,0 +1,58 @@
<?php
$page_title = 'Verifieer Schrijver';
ob_start();
?>
<div class="writer-verification">
<h2>Verifieer Schrijver</h2>
<?php if ($article): ?>
<div class="card mb-30">
<h4><?= htmlspecialchars($article['title']) ?></h4>
<p class="text-muted">
Om dit artikel te bewerken, bevestig aub wie de schrijver is.
</p>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-error">
<?= htmlspecialchars($_SESSION['error']) ?>
<?php unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<form action="/articles/verify-writer" method="post">
<input type="hidden" name="article_id" value="<?= htmlspecialchars($article['id'] ?? '') ?>">
<div class="form-group">
<label for="writer_id">Selecteer de Schrijver *</label>
<select name="writer_id"
id="writer_id"
required
class="form-control">
<option value="">-- Selecteer de schrijver --</option>
<?php foreach ($users as $user): ?>
<option value="<?= $user['id'] ?>"
<?= ($article && $article['author_id'] == $user['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($user['display_name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
Verifieer en Ga Verder
</button>
<a href="/articles" class="btn btn-secondary">
Terug naar Artikelen
</a>
</div>
</form>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($config['app_name'] ?? 'ProostenKrant') ?> - <?= htmlspecialchars($this->data['page_title'] ?? '') ?></title>
<link rel="stylesheet" href="/css/douwco-style.css">
<script src="/js/app.js" defer></script>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="header-content">
<h1 class="logo">
<a href="/"><?= htmlspecialchars($config['app_name'] ?? 'ProostenKrant') ?></a>
</h1>
<p class="slogan"><?= htmlspecialchars($config['app_slogan'] ?? 'Een krant van en voor Proosten') ?></p>
</div>
<?php if (isset($this->data['show_nav']) && $this->data['show_nav']): ?>
<nav class="nav">
<?php if (isset($_SESSION['is_logged_in']) && $_SESSION['is_logged_in']): ?>
<a href="/dashboard">Dashboard</a>
<a href="/articles">Artikelen</a>
<a href="/articles/new">Nieuw Artikel</a>
<a href="/seasons">Seizoenen</a>
<a href="/generate-pdf" class="btn">PDF Genereren</a>
<a href="/logout" class="btn-secondary">Uitloggen</a>
<?php endif; ?>
</nav>
<?php endif; ?>
</header>
<!-- Main Content -->
<main class="main">
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success">
<span><?= htmlspecialchars($_SESSION['success']) ?></span>
<button class="close" onclick="this.parentElement.style.display='none'">×</button>
<?php unset($_SESSION['success']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-error">
<span><?= htmlspecialchars($_SESSION['error']) ?></span>
<button class="close" onclick="this.parentElement.style.display='none'">×</button>
<?php unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<?= $content ?? '' ?>
</main>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<span class="copyright">Copyright © <?= date('Y') ?></span>
<span class="hosted">Made and hosted in <span class="eu-flag">🇪🇺</span></span>
</div>
</footer>
</div>
</body>
</html>
+52
View File
@@ -0,0 +1,52 @@
<?php
$page_title = 'Dashboard';
?>
<div class="dashboard">
<h2>Welkom, <?= htmlspecialchars($_SESSION['display_name'] ?? '') ?>!</h2>
<?php if ($currentSeason): ?>
<div class="season-info" style="background-color: <?= $config['seasons'][$currentSeason['id']]['color'] ?? '#f0f0f0' ?>">
<h3>Huidig Seizoen: <?= htmlspecialchars($currentSeason['name']) ?></h3>
<p><?= htmlspecialchars($currentSeason['description'] ?? '') ?></p>
</div>
<?php endif; ?>
<div class="dashboard-grid">
<div class="dashboard-card">
<h3>Artikelen Toevoegen</h3>
<p>Voeg nieuwe artikelen toe voor de volgende editie.</p>
<a href="/articles/new" class="btn">Nieuw Artikel</a>
</div>
<div class="dashboard-card">
<h3>Mijn Artikelen</h3>
<p>Beheer uw eigen artikelen.</p>
<a href="/articles" class="btn">Bekijk Artikelen</a>
</div>
<div class="dashboard-card">
<h3>Seizoenen</h3>
<p>Bekijk alle seizoensedities.</p>
<a href="/seasons" class="btn">Bekijk Seizoenen</a>
</div>
<div class="dashboard-card">
<h3>PDF Genereren</h3>
<p>Genereer een PDF van de huidige editie.</p>
<a href="/generate-pdf" class="btn">Genereer PDF</a>
</div>
</div>
<h3>Sneltoetsen</h3>
<div class="quick-links">
<a href="/articles/new">+ Nieuw Artikel</a>
<a href="/articles">Artikelen Lijst</a>
<a href="/seasons">Seizoenen</a>
<a href="/generate-pdf">PDF</a>
</div>
</div>
<?php
require __DIR__ . '/../layouts/main.php';
?>
+38
View File
@@ -0,0 +1,38 @@
<?php
?>
<div class="login-container">
<div class="login-card">
<h2>Inloggen</h2>
<p>Voer uw gebruikersnaam en het universele wachtwoord in om toegang te krijgen.</p>
<form action="/login" method="post" class="login-form">
<div class="form-group">
<label for="username">Gebruikersnaam:</label>
<select name="username" id="username" required class="form-control">
<option value="">-- Selecteer een gebruiker --</option>
<?php
$users = \App\Models\User::all();
foreach ($users as $user):
?>
<option value="<?= htmlspecialchars($user['username']) ?>"
<?= (isset($_POST['username']) && $_POST['username'] === $user['username']) ? 'selected' : '' ?>>
<?= htmlspecialchars($user['display_name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="password">Wachtwoord:</label>
<input type="password" name="password" id="password" required class="form-control">
</div>
<button type="submit" class="btn btn-primary">Inloggen</button>
</form>
</div>
</div>
<?php
require __DIR__ . '/../layouts/main.php';
?>
+46
View File
@@ -0,0 +1,46 @@
<?php
$page_title = 'Archief';
ob_start();
?>
<div class="archive-container">
<div class="section-header">
<h2>Archief van ProostenKrant Edities</h2>
</div>
<?php if (!empty($pdfFiles)): ?>
<div class="pdf-list">
<?php foreach ($pdfFiles as $pdf): ?>
<div class="pdf-item slide-up">
<div class="pdf-info">
<h4><?= htmlspecialchars($pdf['edition_name']) ?></h4>
<p>
<span class="text-muted">Datum:</span>
<?= date('d-m-Y', strtotime($pdf['date'])) ?>
</p>
</div>
<div class="pdf-actions">
<a href="<?= htmlspecialchars($pdf['path']) ?>"
class="pdf-icon"
target="_blank"
title="Bekijken/Download">
<span>📄</span>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="card text-center">
<p class="text-muted">Er zijn nog geen PDF edities beschikbaar in het archief.</p>
<p class="text-muted mt-20">
<a href="/" class="btn btn-secondary">Terug naar Home</a>
</p>
</div>
<?php endif; ?>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+121
View File
@@ -0,0 +1,121 @@
<?php
$page_title = 'Home';
ob_start();
?>
<div class="home-sections">
<!-- Section 1: Article Submission -->
<section class="submit-section slide-up">
<h2>Artikel Indienen</h2>
<p>Heb je een leuk verhaal, een bijeenkomst of een bijzondere gebeurtenis meegemaakt?
Deel het met de hele familie door een artikel in te dienen voor de ProostenKrant!</p>
<a href="/submit" class="btn btn-primary">
<span class="icon">+</span> Artikel Indienen
</a>
</section>
<!-- Section 2: WIP Newspaper Preview -->
<section class="wip-section slide-up">
<div class="section-header">
<h2>Huidige Editie (Werk in Uitvoering)</h2>
<div class="section-actions">
<?php if (!empty($draftArticles)): ?>
<span class="status-badge draft">
<?= count($draftArticles) ?> artikelen in voorbereiding
</span>
<?php else: ?>
<span class="text-muted">Geen artikelen in voorbereiding</span>
<?php endif; ?>
</div>
</div>
<?php if (!empty($draftArticles)): ?>
<div class="wip-articles">
<?php foreach ($draftArticles as $article): ?>
<div class="wip-article">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<h4 style="margin-right: 10px;"><?= htmlspecialchars($article['title']) ?></h4>
<?php if (isset($_SESSION['user_id'])): ?>
<a href="/articles/<?= $article['id'] ?>/verify-writer"
class="edit-icon"
title="Bewerken"
style="margin-top: -10px;">
<span>✏️</span>
</a>
<?php endif; ?>
</div>
<p class="wip-content">
<span class="text-muted">Deelnemers:</span>
<strong>
<?php
$participantIds = explode(',', $article['participants']);
$participantNames = [];
foreach ($participantIds as $id) {
if (!empty($id) && isset($usersById[$id])) {
$participantNames[] = htmlspecialchars($usersById[$id]);
}
}
echo implode(', ', $participantNames);
?>
</strong>
<?php if (!empty($article['event_date'])): ?>
<br>
<span class="text-muted">Datum:</span>
<strong><?= date('d-m-Y', strtotime($article['event_date'])) ?></strong>
<?php endif; ?>
</p>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="card text-center">
<p class="text-muted">Er zijn momenteel geen artikelen in voorbereiding.</p>
<p class="text-muted">Wees de eerste om een artikel in te dienen!</p>
</div>
<?php endif; ?>
</section>
<!-- Section 3: Archive -->
<section class="archive-section slide-up">
<div class="section-header">
<h2>Archief</h2>
<a href="/archive" class="btn btn-secondary btn-small">
Alles Bekijken
<span class="icon">→</span>
</a>
</div>
<?php if (!empty($pdfFiles)): ?>
<div class="pdf-list">
<?php foreach (array_slice($pdfFiles, 0, 5) as $pdf): ?>
<div class="pdf-item">
<div class="pdf-info">
<h4><?= htmlspecialchars(basename($pdf['filename'], '.pdf')) ?></h4>
<p>
<span class="text-muted">Gepubliceerd op:</span>
<?= date('d-m-Y', strtotime($pdf['date'])) ?>
</p>
</div>
<div class="pdf-actions">
<a href="<?= htmlspecialchars($pdf['path']) ?>"
class="pdf-icon"
target="_blank"
title="Bekijken/Download">
<span>📄</span>
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="card text-center">
<p class="text-muted">Er zijn nog geen eerdere edities beschikbaar.</p>
</div>
<?php endif; ?>
</section>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+44
View File
@@ -0,0 +1,44 @@
<?php
$page_title = 'Seizoenen';
ob_start();
?>
<div class="seasons-list">
<h2>Seizoensedities</h2>
<p>Hier vindt u een overzicht van alle seizoensedities.</p>
<?php if ($currentSeason): ?>
<div class="current-season">
<h3>Huidig Seizoen</h3>
<div class="season-card" style="background-color: <?= $config['seasons'][$currentSeason['id']]['color'] ?? '#f0f0f0' ?>">
<h4><?= htmlspecialchars($currentSeason['name']) ?></h4>
<p><?= htmlspecialchars($currentSeason['description'] ?? '') ?></p>
<p>Van <?= date('d-m-Y', strtotime($currentSeason['start_date'])) ?>
tot <?= date('d-m-Y', strtotime($currentSeason['end_date'])) ?></p>
<p>Aantal artikelen: <?= $currentSeason['article_count'] ?? 0 ?></p>
<a href="/generate-pdf?season_id=<?= $currentSeason['id'] ?>" class="btn">PDF Genereren</a>
</div>
</div>
<?php endif; ?>
<h3>Alle Seizoenen</h3>
<div class="seasons-grid">
<?php foreach ($seasons as $season): ?>
<div class="season-card" style="background-color: <?= $config['seasons'][$season['id']]['color'] ?? '#f0f0f0' ?>">
<h4><?= htmlspecialchars($season['name']) ?></h4>
<p><?= htmlspecialchars($season['description'] ?? '') ?></p>
<p>Van <?= date('d-m-Y', strtotime($season['start_date'])) ?>
tot <?= date('d-m-Y', strtotime($season['end_date'])) ?></p>
<p>Aantal artikelen: <?= $season['article_count'] ?? 0 ?></p>
<a href="/generate-pdf?season_id=<?= $season['id'] ?>" class="btn-small">PDF</a>
<a href="/seasons/<?= $season['id'] ?>" class="btn-small">Bekijken</a>
</div>
<?php endforeach; ?>
</div>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+57
View File
@@ -0,0 +1,57 @@
<?php
$page_title = htmlspecialchars($season['name'] ?? 'Seizoen');
ob_start();
?>
<div class="season-detail">
<h2><?= htmlspecialchars($season['name']) ?></h2>
<div class="season-info">
<p><strong>Beschrijving:</strong> <?= htmlspecialchars($season['description'] ?? '') ?></p>
<p><strong>Periode:</strong> Van <?= date('d-m-Y', strtotime($season['start_date'])) ?>
tot <?= date('d-m-Y', strtotime($season['end_date'])) ?></p>
</div>
<h3>Artikelen in deze editie</h3>
<?php if (!empty($articles)): ?>
<div class="articles-grid">
<?php foreach ($articles as $article): ?>
<div class="article-card">
<h4><?= htmlspecialchars($article['title']) ?></h4>
<p class="article-meta">
Door: <?= htmlspecialchars($article['author_name']) ?> |
<?= date('d-m-Y', strtotime($article['event_date'])) ?>
</p>
<p><?= htmlspecialchars(substr($article['content'], 0, 150)) ?>...</p>
<?php if ($article['article_image']): ?>
<img src="/<?= htmlspecialchars($article['article_image']) ?>"
alt="<?= htmlspecialchars($article['title']) ?>"
class="article-image">
<?php endif; ?>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<a href="/articles/<?= $article['id'] ?>" class="btn-small">Lees meer</a>
<?php if (isset($_SESSION['user_id'])): ?>
<a href="/articles/<?= $article['id'] ?>/verify-writer" class="edit-icon" title="Bewerken">✏️</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="season-actions">
<a href="/generate-pdf?season_id=<?= $season['id'] ?>" class="btn">PDF Genereren voor deze editie</a>
</div>
<?php else: ?>
<p>Er zijn nog geen artikelen voor deze editie.</p>
<?php endif; ?>
<div class="back-link">
<a href="/seasons">Terug naar alle seizoenen</a>
</div>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+37
View File
@@ -0,0 +1,37 @@
<?php
$page_title = 'Bevestiging';
ob_start();
?>
<div class="confirmation-container slide-up">
<div class="icon">✓</div>
<h2 class="title">Artikel Succesvol Ingediend!</h2>
<?php if ($article): ?>
<div class="card mt-30">
<h4 class="card-title"><?= htmlspecialchars($article['title']) ?></h4>
<div class="article-meta">
<span><strong>Datum:</strong> <?= date('d-m-Y', strtotime($article['event_date'])) ?></span>
</div>
<p class="card-content mt-20">
Je artikel is opgeslagen en zal zichtbaar zijn in de volgende editie van de ProostenKrant.
</p>
</div>
<?php else: ?>
<p>Je artikel is succesvol ingediend en zal zichtbaar zijn in de volgende editie.</p>
<?php endif; ?>
<div class="form-actions mt-30">
<a href="/" class="btn btn-primary">
<span class="icon">←</span> Terug naar Home
</a>
<a href="/submit" class="btn btn-secondary">
<span class="icon">+</span> Nog een Artikel Indienen
</a>
</div>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+57
View File
@@ -0,0 +1,57 @@
<?php
$page_title = 'Artikelen Bewerken';
ob_start();
?>
<div class="edit-list">
<div class="section-header">
<h2>Artikelen om te Bewerken</h2>
<a href="/" class="btn btn-secondary btn-small">
<span class="icon">←</span> Terug
</a>
</div>
<?php if (!empty($articles)): ?>
<div class="cards-grid">
<?php foreach ($articles as $article): ?>
<div class="article-card">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<h4 class="article-title"><?= htmlspecialchars($article['title']) ?></h4>
<a href="/submit/edit/<?= $article['id'] ?>"
class="edit-icon"
title="Bewerken">
<span>✏️</span>
</a>
</div>
<div class="article-meta">
<span><strong>Deelnemers:</strong>
<?php
$participantIds = explode(',', $article['participants']);
$participantNames = [];
foreach ($participantIds as $id) {
if (!empty($id) && isset($usersById[$id])) {
$participantNames[] = htmlspecialchars($usersById[$id]);
}
}
echo implode(', ', $participantNames);
?>
</span>
<span><strong>Datum:</strong> <?= date('d-m-Y', strtotime($article['event_date'])) ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="card text-center">
<p class="text-muted">Er zijn geen artikelen om te bewerken.</p>
<p class="mt-20">
<a href="/submit" class="btn btn-primary">Nieuw Artikel Indienen</a>
</p>
</div>
<?php endif; ?>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+34
View File
@@ -0,0 +1,34 @@
<?php
$page_title = 'Artikelen Bewerken - Beveiliging';
ob_start();
?>
<div class="passphrase-container slide-up">
<h2 class="title">Artikelen Bewerken</h2>
<p>Voer de familypaszin in om artikelen te kunnen bewerken:</p>
<form action="/submit/edit/verify" method="post">
<div class="form-group">
<label for="passphrase">Familie Paszin</label>
<input type="password"
name="passphrase"
id="passphrase"
class="form-control"
required
autocomplete="off"
placeholder="Voer de paszin in">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon">→</span> Doorgaan
</button>
<a href="/" class="btn btn-secondary">Annuleren</a>
</div>
</form>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+192
View File
@@ -0,0 +1,192 @@
<?php
$page_title = 'Nieuw Artikel Indienen';
ob_start();
$users = $users ?? [];
$oldInput = $oldInput ?? [];
$errors = $errors ?? [];
$article = $article ?? null;
// If editing, populate old input from article
if ($article && empty($oldInput)) {
$oldInput = [
'title' => $article['title'],
'content' => $article['content'],
'writer' => $article['author_id'],
'event_date' => $article['event_date'],
'article_id' => $article['id']
];
// Parse participants
if (!empty($article['participants'])) {
$participantIds = explode(',', $article['participants']);
} else {
$participantIds = [];
}
}
// Get selected participants from old input
$selectedParticipants = [];
if (!empty($oldInput['participants'])) {
if (is_array($oldInput['participants'])) {
$selectedParticipants = $oldInput['participants'];
} else {
$selectedParticipants = [$oldInput['participants']];
}
} elseif (isset($participantIds)) {
$selectedParticipants = $participantIds;
}
// Check if we have an uploaded image in session or from old input
$uploadedImage = null;
if (isset($_SESSION['uploaded_image'])) {
$uploadedImage = $_SESSION['uploaded_image'];
unset($_SESSION['uploaded_image']);
} elseif ($article && $article['article_image']) {
$uploadedImage = $article['article_image'];
}
?>
<div class="article-form">
<div class="section-header">
<h2><?= $article ? 'Artikel Bewerken' : 'Nieuw Artikel Indienen' ?></h2>
<a href="/" class="btn btn-secondary btn-small">
<span class="icon">×</span> Annuleren
</a>
</div>
<form action="/submit/store" method="post" enctype="multipart/form-data">
<?php if ($article): ?>
<input type="hidden" name="article_id" value="<?= $article['id'] ?>">
<?php endif; ?>
<div class="form-row">
<div class="form-group">
<label for="title">Titel *</label>
<input type="text"
name="title"
id="title"
value="<?= htmlspecialchars($oldInput['title'] ?? '') ?>"
required
class="form-control <?= isset($errors['title']) ? 'error' : '' ?>"
placeholder="Voer de titel in">
<?php if (isset($errors['title'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['title']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label for="content">Tekst *</label>
<textarea name="content"
id="content"
rows="8"
required
class="form-control <?= isset($errors['content']) ? 'error' : '' ?>"
placeholder="Schrijf hier de inhoud van je artikel..."><?= htmlspecialchars($oldInput['content'] ?? '') ?></textarea>
<?php if (isset($errors['content'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['content']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="writer">Schrijver *</label>
<select name="writer"
id="writer"
required
class="form-control <?= isset($errors['writer']) ? 'error' : '' ?>">
<option value="">-- Selecteer schrijver --</option>
<?php foreach ($users as $user): ?>
<option value="<?= $user['id'] ?>"
<?= (($oldInput['writer'] ?? '') == $user['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($user['display_name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php if (isset($errors['writer'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['writer']) ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="event_date">Datum van het evenement *</label>
<input type="date"
name="event_date"
id="event_date"
value="<?= htmlspecialchars($oldInput['event_date'] ?? date('Y-m-d')) ?>"
required
class="form-control">
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label>Deelnemers *</label>
<p class="help-text">Selecteer alle personages die bij dit verhaal betrokken zijn.</p>
<div class="checkbox-group">
<?php foreach ($users as $user): ?>
<label class="checkbox-label">
<input type="checkbox"
name="participants[]"
value="<?= $user['id'] ?>"
<?= (in_array($user['id'], $selectedParticipants)) ? 'checked' : '' ?>>
<?= htmlspecialchars($user['display_name']) ?>
</label>
<?php endforeach; ?>
</div>
<p class="help-text">Selecteer minstens één deelnemer</p>
<?php if (isset($errors['participants'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['participants']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label for="image">Afbeelding *</label>
<div class="file-upload-wrapper">
<input type="file"
name="image"
id="image"
accept="image/*"
required>
<label for="image" class="file-upload-label">
<span class="icon">📷</span> Kies een afbeelding
</label>
</div>
<p class="help-text file-upload-info">
Upload een foto bij het artikel. Zowel landscape als portret foto's worden ondersteund.
</p>
<?php if ($uploadedImage): ?>
<div class="image-preview mt-20">
<div class="image-preview-container">
<img src="/<?= htmlspecialchars($uploadedImage) ?>" alt="Geselecteerde afbeelding">
</div>
<p class="text-muted mt-10">Afbeelding succesvol geupload: <?= htmlspecialchars(basename($uploadedImage)) ?></p>
</div>
<?php endif; ?>
<?php if (isset($errors['image'])): ?>
<span class="error-message"><?= htmlspecialchars($errors['image']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon"><?= $article ? '✓' : '+' ?></span>
<?= $article ? 'Artikel Bijwerken' : 'Artikel Indienen' ?>
</button>
<a href="/" class="btn btn-secondary">Annuleren</a>
</div>
</form>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+34
View File
@@ -0,0 +1,34 @@
<?php
$page_title = 'Artikel Indienen - Beveiliging';
ob_start();
?>
<div class="passphrase-container slide-up">
<h2 class="title">Beveiligingscontrole</h2>
<p>Voer de familypaszin in om een artikel in te dienen:</p>
<form action="/submit/verify" method="post">
<div class="form-group">
<label for="passphrase">Familie Paszin</label>
<input type="password"
name="passphrase"
id="passphrase"
class="form-control"
required
autocomplete="off"
placeholder="Voer de paszin in">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon">→</span> Doorgaan
</button>
<a href="/" class="btn btn-secondary">Annuleren</a>
</div>
</form>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
+51
View File
@@ -0,0 +1,51 @@
<?php
$page_title = 'Verifieer Schrijver';
ob_start();
?>
<div class="writer-verification slide-up">
<h2 class="title">Verifieer Schrijver</h2>
<?php if ($article): ?>
<div class="card mb-30">
<h4 class="card-title"><?= htmlspecialchars($article['title']) ?></h4>
<p class="text-muted">
Om dit artikel te bewerken, bevestig aub wie de schrijver is.
</p>
</div>
<?php endif; ?>
<form action="/submit/edit/verify-writer" method="post">
<input type="hidden" name="article_id" value="<?= htmlspecialchars($article['id'] ?? '') ?>">
<div class="form-group">
<label for="writer_id">Selecteer de Schrijver *</label>
<select name="writer_id"
id="writer_id"
required
class="form-control">
<option value="">-- Selecteer de schrijver --</option>
<?php foreach ($users as $user): ?>
<option value="<?= $user['id'] ?>"
<?= ($article && $article['author_id'] == $user['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($user['display_name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon">→</span> Verifieer en Bewerk
</button>
<a href="/submit/edit/list" class="btn btn-secondary">
<span class="icon">←</span> Terug naar Lijst
</a>
</div>
</form>
</div>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layouts/main.php';
?>
View File
+54
View File
@@ -0,0 +1,54 @@
\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{microtype}
\usepackage{newspaper}
\usepackage[dutch]{babel}
\date{\today}
\currentvolume{1}
\currentissue{1}
%% The banner headline on the first page
%% (The colon after s: is to get a more
%% modern majuscule s in this font instead of
%% the medieval tall s.
\SetPaperName{Proo{s:}tenKrant}
%% The name used in the running header after
%% the first page
\SetHeaderName{ProostenKrant}
%% and also...
\SetPaperLocation{Regio Kempen}
\SetPaperSlogan{``Een krant van en voor Proosten.''}
\SetPaperPrice{NUL EURO}
\usepackage{graphicx}
\usepackage{tikz}
\usepackage{multicol}
\usepackage{geometry}
\usepackage{array}
\usepackage{wrapfig}
\usepackage{lipsum}
\usepackage{newtxtext,newtxmath}
\usepackage{etoolbox}
% Custom styling for better image handling
\input{newspaper-mod}
%%%%%%%%% Front matter %%%%%%%%%%
\begin{document}
\maketitle
\begin{multicols}{3}
% Articles will be inserted here by the PHP generator
\newpage
\end{multicols}
\end{document}
+119
View File
@@ -0,0 +1,119 @@
% Improved newspaper-mod.sty for ProostenKrant
% Better image handling for different formats and sizes
\newcommand{\headlinestyle}{\itshape\huge}
\newcommand{\bylinestyle}{\scshape\Large}
\patchcmd{\headline}{#1}{\headlinestyle #1}{}{}
\patchcmd{\byline}{#1}{\bylinestyle #1}{}{}
% Improved image commands with better handling of different aspect ratios
% Show inline image with caption (automatically handles landscape and portrait)
\newcommand{\showInlineImage}[2]
{
\\\n \hspace*{0pt}% Ensure proper spacing
\vspace*{10pt}% Add space before image
\begin{center}
\begin{minipage}{0.95\columnwidth}
\centering
% Use max dimensions that fit in column
\setlength{\fboxsep}{5pt}
\framebox{
% Check image dimensions and scale appropriately
\includegraphics[max width=\linewidth, max height=0.6\textheight, keepaspectratio]{#1}
}
\vspace*{5pt}
{\small #2}
\end{minipage}
\end{center}
\vspace*{10pt}% Add space after image
\par
}
% Show single writer with circular profile image
\newcommand{\showOneWriter}[2]
{
\\\n \hfill
\begin{minipage}[t]{0.4\columnwidth}
\centering
\begin{tikzpicture}
% Circular clip for profile image
\clip (0,0) circle (1.3cm);
% Use square aspect ratio and scale to fit
\node[anchor=center] at (0,0) {
\includegraphics[width=2.8cm, height=2.8cm, keepaspectratio]{#1}
};
\end{tikzpicture}
\vspace*{5pt}
{\small #2}
\end{minipage}
\hfill
\\\n}
% Show two writers with circular profile images
\newcommand{\showTwoWriters}[4]
{
\\\n \begin{center}
\begin{minipage}[t]{0.8\columnwidth}
\centering
\begin{tikzpicture}
\clip (0,0) circle (1.1cm);
\node[anchor=west] at (0,0) {
\includegraphics[width=2.4cm, height=2.4cm, keepaspectratio]{#1}
};
\end{tikzpicture}
\hspace*{15pt}
\begin{tikzpicture}
\clip (0,0) circle (1.1cm);
\node[anchor=east] at (0,0) {
\includegraphics[width=2.4cm, height=2.4cm, keepaspectratio]{#3}
};
\end{tikzpicture}
\vspace*{8pt}
{\small #2 \\quad #4}
\end{minipage}
\end{center}
\\\n}
% Alternative commands for different layouts
% For portrait images - maintains aspect ratio
\newcommand{\showPortraitImage}[2]
{
\\\n \begin{minipage}[t]{0.45\columnwidth}
\centering
\begin{tikzpicture}
% For portrait images, use vertical layout
\clip (0,-1.5cm) rectangle (2cm,1.5cm);
\node[anchor=center] at (1cm,0) {
\includegraphics[height=3cm, keepaspectratio]{#1}
};
\end{tikzpicture}
\vspace*{5pt}
{\small #2}
\end{minipage}
\\\n}
% For landscape images - maintains aspect ratio
\newcommand{\showLandscapeImage}[2]
{
\\\n \begin{minipage}[t]{\columnwidth}
\centering
% For landscape images, use full width
\includegraphics[width=\linewidth, height=0.4\textheight, keepaspectratio]{#1}
\vspace*{5pt}
{\small #2}
\end{minipage}
\\\n}
% Flexible image wrapper that automatically handles different orientations
\newcommand{\showFlexibleImage}[2]
{
% First, we need to check the image dimensions
% This requires the epstopdf package and shell-escape
\IfFileExists{#1}{
% Image exists, try to determine orientation
% For now, we'll use a balanced approach that works for both
\showInlineImage{#1}{#2}
}{}
}