Cleaned up the repo and fixed some bugs.

This commit is contained in:
douwe
2026-10-09 13:53:59 +02:00
parent 72dba8bcfa
commit a2a9a773f4
150 changed files with 227 additions and 7106 deletions
+3 -13
View File
@@ -25,19 +25,9 @@ Thumbs.db
*.log
logs/
# Storage
app/storage/*
!app/storage/.gitkeep
# Public uploads (keep directory, ignore contents)
app/public/images/articles/*
!app/public/images/articles/.gitkeep
app/public/images/profiles/*
!app/public/images/profiles/.gitkeep
# LaTeX output
app/storage/latex/*
!app/storage/latex/.gitkeep
# Artikeluploads via de website (runtime gegenereerd)
resources/images/articles/*
!resources/images/articles/.gitkeep
# Database dumps
*.sql
-105
View File
@@ -1,105 +0,0 @@
\headline{Circus Barones}
\byline{11-11-2023}{Diede}
\showOneWriter{Images/ProfilePictures/Diede.jpg}{Diede}
\par Op zaterdag 11 november hadden drie vrienden en ik afgesproken in Antwerpen om iets te gaan doen. We hadden geen zin om gewoon op café te gaan en besloten daarom om naar het circus in Diest te gaan. Aangekomen bij het circus bleken er nog heel wat mensen hetzelfde plan te hebben voor die dag. Heel de tent was gevuld met ouders en jonge kindjes. Hoewel alle kinderen enthousiast waren over de acts, konden ze absoluut niet tippen aan het enthousiasme dat wij vier meebrachten. We klapten altijd het hardste en moedigde de circusartiesten luidkeels aan. Dat bleef niet onopgemerkt door de artiesten. Wanneer dan het moment gekomen was dat er enkele afgevaardigden uit het publiek op podium moesten komen, wezen de circusartiesten natuurlijk naar ons vier. Zo hebben wij onze carrière als circusartiest die zaterdag ingezet, gevolgd door een daverend applaus!
\showInlineImage{Images/Articles/Circus Barones.jpg}{}
\headline{Dagje Utrecht}
\byline{9-12-2023}{Diede}
\showOneWriter{Images/ProfilePictures/Diede.jpg}{Diede}
\par Op zaterdag 9 december ging ik met drie vrienden naar Utrecht. We hebben daar heel de dag rondgewandeld, zonder dat de regen ons kon deren. Na een tijdje waren we echter zo doorweekt dat we op zoek zijn gegaan naar een cafeetje om droog te zitten. Blijkbaar is dat op een zaterdag in Utrecht niet zo evident en verwachten ze dat je van tevoren reserveert. Na even zoeken hadden we dan toch een cafeetje gevonden waar er nog een tafeltje vrij was. Toevallig lag er op één van de stoelen aan dat tafeltje ook een katje te slapen, we vermoeden het katje van de eigenaar. Die is flink bij ons blijven liggen tot we drie uur later weer zijn weggegaan. Na het café zijn we nog tot de vroege uurtjes uitgeweest in Utrecht waarna een vriendin ons is komen ophalen om ons thuis te brengen.
\showInlineImage{Images/Articles/Dagje Utrecht.jpg}{}
\headline{En hedde gij meubelen,en hedde gij huisgerief.}
\byline{22-12-2023}{Dien}
\showOneWriter{Images/ProfilePictures/Dien.jpg}{Dien}
\par Een nieuw jaar, een nieuw interieur. Ten huize Ravers werden kosten nog moeite gespaard om hun woonkamer een nieuwe look te geven. De oude lederen hoekbank werd vervangen door een beige driezit en een Togo zetel. Een echte chill zetel waar voor gevochten gaat worden om wie er in mag zitten. We zijn al super blij met deze aankoop maar er moet nog heel wat gebeuren om de look die we voor ogen hebben,te verwezenlijken. Zo zijn we nog op zoek naar een bijpassende salontafel en een tof clubke voor erbij. In maart wordt onze nieuwe keuken geplaatst en dan kunnen we er weeral tegen voor de volgende 20 jaar.
\showInlineImage{Images/Articles/En hedde gij meubelen,en hedde gij huisgerief..jpg}{}
\headline{Het leven van alle dag}
\byline{24-12-2023}{Chris}
\showTwoWriters{Images/ProfilePictures/Chris.jpg}{Chris}{Images/ProfilePictures/Geert.jpg}{Geert}
\par Wat wij de voorbije maanden zoal deden, is zeer gewoon: werken en wat hobby's en that's it! In september start van het nieuwe schooljaar, in oktober fietsen we naar het gemeentehuis van Retie om te protesteren tegen de geplande verkaveling van de Chiroterreinen, onder initiatief van Schoonbroek stikt, waar ook onze zoon zijn schouders mee heeft ondergezet. En met succes, het project gaat 'voorlopig' niet door. Verder zijn Mathilde en ik gestart in Studio Doe, waar we potten leren draaien, gieten en inleggen. We vierden ook onze 34ste huwelijksverjaardag deze maand. In november maakten we weer een mooie herinnering met onze vriend Dirk door samen naar een optreden van zijn zoon te gaan. Hij kreeg in april de diagnose ALS en het gaat snel! In december nemen we met een paar vrienden zijn tuin onder handen. Geert ging deze maand mee op jacht als tracker en binnenkort eten we everzwijn. En verder staat december in het teken van feesten, wij hebben er al 2 achter de rug, op naar het volgende!
\showInlineImage{Images/Articles/Het leven van alle dag.jpg}{}
\headline{Workshop houtbranden}
\byline{20-9-2023}{Annick}
\showOneWriter{Images/ProfilePictures/Annick.jpg}{Annick}
\par Op IOK hadden wij personeelsdag. In de voormiddag een grote wandeling gemaakt in de Liereman in Oud-Turnhout. In de namiddag een workshop houtbranden. Dit was echt heel leuk en ontspannend om te doen. Zoals je kan zien op de foto heb ik onze achternaam weer alle eer aangedaan. 😜
\showInlineImage{Images/Articles/Workshop houtbranden.jpg}{}
\headline{Escapewandeling Hasselt}
\byline{23-11-2023}{Annick}
\showOneWriter{Images/ProfilePictures/Annick.jpg}{Annick}
\par Met de personeelsdienst van IOK hadden we een escapewandeling in Hasselt ‘s avonds na ons werk. Winterland was daar ook net begonnen. Heel leuk en gezellig .
\showInlineImage{Images/Articles/Escapewandeling Hasselt.jpg}{}
\headline{Schümbroekse diskonaaijt in de spiegeltent van Schoonbroek leeft}
\byline{23-12-2023}{Annick}
\showOneWriter{Images/ProfilePictures/Annick.jpg}{Annick}
\par Een zalig disco-dansavondje met mijn vriendinnen van de Singel in Schoonbroek. En dan nog een groen daarbij die ook familie was. Maar eigenlijk was heel die tent daar wel ergens familie van mekaar 😝. Heerlijk om zo nog eens in mijn geboortedorp op stap te gaan en bij bomma en bompa te gaan slapen. Nogmaals dank 🙏
\showInlineImage{Images/Articles/Schümbroekse diskonaaijt in de spiegeltent van Schoonbroek leeft.jpg}{}
\headline{Een dagje zoo}
\byline{1-10-2023}{Cedric}
\showOneWriter{Images/ProfilePictures/Cedric.jpg}{Cedric}
\par Cedric mocht met een vriendje van de leefgroep met de trein naar de Zoo in Antwerpen. Hij heeft er echt van genoten.
\showInlineImage{Images/Articles/Een dagje zoo.jpg}{}
\headline{Kerstfeestje in de leefgroep}
\byline{21-12-2023}{Cedric}
\showOneWriter{Images/ProfilePictures/Cedric.jpg}{Cedric}
\par Cedric had kerstfeestje in zijn leefgroep met heel lekker eten en iedereen kreeg een cadeautje . Cedric heeft sloefen van Club Brugge gekregen en hij is er héél blij mee.
\showInlineImage{Images/Articles/Kerstfeestje in de leefgroep.jpg}{}
\headline{Zalig Kerstfeest!!!!}
\byline{25-12-2023}{Cedric}
\showOneWriter{Images/ProfilePictures/Cedric.jpg}{Cedric}
\par Dit kerstmanneke wenst heel de familie ne zalige Kerstmis!!!!
\showInlineImage{Images/Articles/Zalig Kerstfeest!!!!.jpg}{}
\headline{Marathon}
\byline{12-11-2023}{Gunter}
\showOneWriter{Images/ProfilePictures/Gunter.jpg}{Gunter}
\par Inge heeft marathon gelopen in Kasterlee en ze was daarbij de snelste van Kasterlee
\showInlineImage{Images/Articles/Marathon.jpg}{}
\headline{Going to 'Murica }
\byline{19-10-2023}{Douwe}
\showTwoWriters{Images/ProfilePictures/Douwe.jpg}{Douwe}{Images/ProfilePictures/Pauline.jpg}{Pauline}
\par Van 19 oktober tot 2 november zijn Pauline en ik naar New York geweest. Ik moest naar een driedaagse conferentie op Roosevelt Island, en Pauline en ik hebben er dan een reis aan gekoppeld. Het was voor ons allebei de eerste keer naar een ander continent. Na 9 uur op het vliegtuig en 2 uur op de metro en bus begonnen we onze reis in Weehawken, New Jersey, een echt Amerikaans dorpje met de typische gele schoolbussen en deli’s in elke straat. Daarna verhuisden we naar een chic hotel op Roosevelt Island, een klein eilandje van 7km omtrek tussen de Upper East Side op Manhattan en Long Island. Toen Douwe op zijn conferentie was, ging Pauline alleen op pad. Ons laatste verblijf was diep in Brooklyn, en tot onze spijt hadden we extra gasten in ons appartement: kakkerlakken. Maar we lieten onze spirit er niet door beïnvloeden. Reden te meer om lang in de stad te blijven en op restaurant te gaan. New York moet je echt eens gezien hebben, je zou versteld staan van de cultuurshock die je toch ervaart.
\showInlineImage{Images/Articles/Going to 'Murica .jpg}{}
\headline{Eerste kerstboom in Brugge}
\byline{25-11-2023}{Douwe}
\showTwoWriters{Images/ProfilePictures/Douwe.jpg}{Douwe}{Images/ProfilePictures/Pauline.jpg}{Pauline}
\par Op 25 november zijn Pauline en ik onze allereerste kerstboom gaan kopen voor op ons appartementje. Onze kerstballen zijn helaas niet heel op ons appartement geraakt. Pauline haalde de kerstballen uit het koffer en ik nam de grote doos van de kerstboom. Maar toen ik omdraaide met de doos had ik niet gezien dat Pauline vlak naast mij stond. Eén doos kerstballen op de grond dus. Gelukkig zie je aan de versierde kerstboom niet dat er ballen ontbreken.
\showInlineImage{Images/Articles/Eerste kerstboom in Brugge.jpg}{}
\headline{Advertentie: Oeps, Kleurenavontuur!}
\byline{26-12-2023}{Daan}
\showTwoWriters{Images/ProfilePictures/Daan.jpg}{Daan}{Images/ProfilePictures/Kyala.jpg}{Kyala}
\par Heeft u ooit een verfpot geopend en een kleur ontdekt die een eigen wil lijkt te hebben? Wij wel! Ons kleurenexperiment bleek iets minder charmant dan verwacht. Voor degenen die houden van verrassingen en onconventionele interieurs: Grijp nu uw kans op de meest unieke verfkleur van het jaar! Kleurencode: S1010-Y30R Inhoud: 30L muurverf + 5L deurenverf Merk: SIGMA Limited edition – omdat sommige kleuren gewoon niet te plannen zijn. 🎨✨
\showInlineImage{Images/Articles/Advertentie Oeps, Kleurenavontuur!.jpg}{}
\headline{Mini cooper met zwembad}
\byline{26-12-2023}{Michel}
\showOneWriter{Images/ProfilePictures/Michel.jpg}{Michel}
\par Onlangs kreeg onze mini (auto) kuren, spontaan deed de auto z'n ramen ongevraagd open en bleven de lichten achteraan branden ook al stond alles af, het leek wel Herbie (de kever..) die een eigen leven leidde. Na lang zoeken bleek er elektrische kortsluiting te zitten op wat onderdelen omdat de vloer aan bestuurderszijde wat nat aanvoelde... Na het verwijderen van de vloerbekleding bleek er zowat 6liter water in de auto te staan, ik kon m'n hand gewoon onder water steken. De officiële garage was supersnel en efficiënt in het opmaken van een gepeperde offerte om alles wat nat was te vervangen, hoe en van waar het water kwam en hoe ze dit gingen oplossen daar hadden ze nog niet over nagedacht... Gelukkig was er Wim VM die nog een projectje zocht om z'n studenten automechanica voor te leggen... . In Januari verhuist ons rijdend binnenzwembad richting schoolpoort om daar binnenstebuiten gedraaid te worden om lek en elektronica onder controle te krijgen, hopelijk doet Herbie daarna terug gewoon ...
\showInlineImage{Images/Articles/Mini cooper met zwembad.jpg}{}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 823 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 KiB

-6
View File
@@ -1,6 +0,0 @@
\relax
\providecommand\babel@aux[2]{}
\@nameuse{bbl@beforestart}
\catcode `"\active
\babel@aux{dutch}{}
\gdef \@abspage@last{5}
-1366
View File
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
\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{subfiles}
\usepackage{newspaper-mod}
%%%%%%%%% Front matter %%%%%%%%%%
\usepackage{lipsum}
\begin{document}
\maketitle
\begin{multicols}{3}
\subfile{Articles}
\newpage
\end{multicols}
\end{document}
-50
View File
@@ -1,50 +0,0 @@
\usepackage{newtxtext,newtxmath}
\usepackage{etoolbox}
\newcommand{\headlinestyle}{\itshape\huge}
\newcommand{\bylinestyle}{\scshape\Large}
\patchcmd{\headline}{#1}{\headlinestyle #1}{}{}
\patchcmd{\byline}{#1}{\bylinestyle #1}{}{}
\newcommand{\showInlineImage}[2]
{
$
\begin{array}{c}
\resizebox{0.8\columnwidth}{!}{\includegraphics{#1}} \\
$#2$
\end{array}
$
}
\newcommand{\showOneWriter}[2]
{
$
\hfill
\begin{array}{c}
\begin{tikzpicture}
\clip (0,0) circle (0.2\columnwidth) ;
\node[anchor=center] at (0,0) {\includegraphics[width=0.4\columnwidth]{#1}};
\end{tikzpicture} \\
$#2$
\end{array}
\hfill
$
}
\newcommand{\showTwoWriters}[4]
{
$
\begin{array}{cc}
\begin{tikzpicture}
\clip (0,0) circle (0.2\columnwidth) ;
\node[anchor=center] at (0,0) {\includegraphics[width=0.4\columnwidth]{#1}};
\end{tikzpicture}
&
\begin{tikzpicture}
\clip (0,0) circle (0.2\columnwidth) ;
\node[anchor=center] at (0,0) {\includegraphics[width=0.4\columnwidth]{#3}};
\end{tikzpicture} \\
$#2$ & $#4$
\end{array}
$
}
+31 -19
View File
@@ -35,12 +35,6 @@ nu aangestuurd vanuit .NET.
docker compose up -d --build
```
Regressietest (draait tegen de draaiende stack; maakt testartikelen en drukt een proefeditie):
```bash
scripts/e2e-test.sh
```
Daarna:
- Website: http://localhost:8080
@@ -76,18 +70,28 @@ proostenkrant.net/
├── src/ProostenKrant/ # Blazor-applicatie
│ ├── Components/Pages/ # Alle pagina's (Razor, static SSR)
│ ├── Services/ # Db, ArticleService, LatexService, auth, uploads
│ └── wwwroot/ # css, fonts, EU-vlag, uploads (volume-mounted)
│ └── wwwroot/ # css, fonts, EU-vlag (resources volume-mounted)
├── deploy/
│ ├── Dockerfile # Build + runtime met texlive (gecachete laag)
│ └── mysql/init.sql # Database-initialisatie
├── compose.yml # app + mysql stack
├── app/ # (legacy) PHP-versie — niet verwijderd, zie hieronder
└── Latex/ # (legacy) origineel krantenontwerp ter referentie
├── resources/
│ ├── archive/ # Gedrukte edities (PDF's)
│ ├── images/
│ │ ├── profiles/ # Profielfoto's (naam = weergavenaam)
│ │ └── articles/ # Artikelafbeeldingen (via website geüpload)
│ └── templates/latex/ # LaTeX-ontwerp ter referentie
└── compose.yml # app + mysql stack
```
De artikel- en profielfoto's uit de PHP-versie worden hergebruikt via volumes:
`app/public/images/articles` en `app/public/images/profiles` worden gekoppeld aan de
uploadmappen van de Blazor-app. De gedrukte PDF's landen in `app/storage/latex`.
De volledige resources-map wordt via volumes aan de app gekoppeld:
- `resources/images` → `wwwroot/resources/images` — profielfoto's en
artikelafbeeldingen. Een profielfoto hoort bij een familielid als de
bestandsnaam (zonder extensie) overeenkomt met de weergavenaam
(`Geert.jpg` → Geert). Een later toegevoegde foto wordt dus automatisch
gekoppeld; een verwijderde foto valt terug op `Unknown.jpg`.
- `resources/archive` → `/app/archive` — de bestaande archief-edities
worden getoond en pas gedrukte kranten worden hier bewaard.
De Europese vlag in de voettekst is een SVG (`src/ProostenKrant/wwwroot/images/european-flag.svg`)
volgens de officiële specificaties (12 gouden sterren, cirkelradius ⅓ van de hoogte).
@@ -103,8 +107,9 @@ volgens de officiële specificaties (12 gouden sterren, cirkelradius ⅓ van de
4. Zet eventueel een reverse proxy met HTTPS (Caddy/nginx) voor `http://server:8080`.
Overweeg de MySQL-poort (`3306`) dicht te zetten in `compose.yml` als de database
niet van buitenaf benodigd is.
5. Backups: `resources/` (repo), `app/public/images/` (foto's) en de mysql-data volume
bevatten alles; `docker compose down -v` verwijdert de data definitief.
5. Backups: de `resources/`-map (foto's, artikelafbeeldingen, archief-PDF's) en
de mysql-data volume bevatten alles; `docker compose down -v` verwijdert de
data definitief.
De eerste build installeert texlive eenmalig (±2 GB, enkele minuten); daarna pakken
Docker-lagen alles en duurt een rebuild seconden.
@@ -116,9 +121,16 @@ Omgevingsvariabelen (of `.env`):
- `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` — database (standaard `mysql`/`proostenkrant`)
- `UNIVERSAL_PASSWORD` — inlogwachtwoord en paszin (standaard `ProostenKrant2024!`)
Paden (standaarden in `appsettings.json`, sectie `Paths`):
- `StaticArchive` / `LatexOutput` — map voor archief-edities én pas gedrukte
kranten (`archive`, gekoppeld aan `resources/archive`)
- `ResourceImages` — map voor afbeeldingen uit de resources (`resources/images`,
gekoppeld aan `resources/images`; submappen `profiles` en `articles`)
## Legacy PHP-versie
De oude PHP-applicatie staat nog in `app/` en is niet verwijderd; de oude
`docker-compose`-bestanden zijn verplaatst naar `legacy/` zodat Compose automatisch
de Blazor-stack (`compose.yml`) pakt. Zodra de Blazor-versie goedgekeurd is, kunnen
`app/`, `docker/` en `legacy/` worden opgeruimd.
De oude PHP-applicatie is opgeruimd; de foto's en archief-PDF's zijn verplaatst
naar de `resources/`-map. Alleen de oude `docker/`-map (nginx/php-config van de
PHP-versie) staat nog in de repo en kan verwijderd worden — de Blazor-stack
gebruikt enkel `compose.yml` en `deploy/Dockerfile`.
-26
View File
@@ -1,26 +0,0 @@
<?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
@@ -1,10 +0,0 @@
<?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
@@ -1,45 +0,0 @@
<?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
@@ -1,19 +0,0 @@
<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>
File diff suppressed because it is too large Load Diff
-582
View File
@@ -1,582 +0,0 @@
/* 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
-114
View File
@@ -1,114 +0,0 @@
<?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
@@ -1,97 +0,0 @@
// 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
@@ -1 +0,0 @@
/var/www/html/storage/latex
-192
View File
@@ -1,192 +0,0 @@
<?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
@@ -1,46 +0,0 @@
<?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
@@ -1,95 +0,0 @@
<?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');
}
}
}
@@ -1,28 +0,0 @@
<?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
@@ -1,17 +0,0 @@
<?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
@@ -1,285 +0,0 @@
<?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
@@ -1,141 +0,0 @@
<?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
@@ -1,39 +0,0 @@
<?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
]);
}
}
@@ -1,287 +0,0 @@
<?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
@@ -1,138 +0,0 @@
<?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
@@ -1,36 +0,0 @@
<?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
@@ -1,128 +0,0 @@
<?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
@@ -1,90 +0,0 @@
<?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
@@ -1,98 +0,0 @@
<?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
@@ -1,41 +0,0 @@
<?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
@@ -1,24 +0,0 @@
<?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
@@ -1,34 +0,0 @@
<?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
@@ -1,5 +0,0 @@
<?php
require 'controllers/Controller.php';
require 'controllers/TestController.php';
$controller = new \App\Controllers\TestController();
$controller->index();
-52
View File
@@ -1,52 +0,0 @@
<?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
@@ -1,128 +0,0 @@
<?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
@@ -1,57 +0,0 @@
<?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';
?>
@@ -1,50 +0,0 @@
<?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
@@ -1,58 +0,0 @@
<?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
@@ -1,65 +0,0 @@
<!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
@@ -1,52 +0,0 @@
<?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
@@ -1,38 +0,0 @@
<?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
@@ -1,46 +0,0 @@
<?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';
?>

Some files were not shown because too many files have changed in this diff Show More