Cleaned up the repo and fixed some bugs.
@@ -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
|
||||
|
||||
@@ -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}{}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 427 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 439 KiB |
|
Before Width: | Height: | Size: 461 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 583 KiB |
|
Before Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 912 KiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 823 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 733 KiB |
|
Before Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 422 KiB |
|
Before Width: | Height: | Size: 786 KiB |
|
Before Width: | Height: | Size: 560 KiB |
|
Before Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 955 KiB |
@@ -1,6 +0,0 @@
|
||||
\relax
|
||||
\providecommand\babel@aux[2]{}
|
||||
\@nameuse{bbl@beforestart}
|
||||
\catcode `"\active
|
||||
\babel@aux{dutch}{}
|
||||
\gdef \@abspage@last{5}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
$
|
||||
}
|
||||
@@ -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`.
|
||||
|
||||
@@ -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'],
|
||||
],
|
||||
];
|
||||
@@ -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',
|
||||
];
|
||||
@@ -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',
|
||||
],
|
||||
];
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>';
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
/var/www/html/storage/latex
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
require 'controllers/Controller.php';
|
||||
require 'controllers/TestController.php';
|
||||
$controller = new \App\Controllers\TestController();
|
||||
$controller->index();
|
||||
@@ -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';
|
||||
?>
|
||||
@@ -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';
|
||||
?>
|
||||
@@ -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';
|
||||
?>
|
||||
@@ -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';
|
||||
?>
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
?>
|
||||
@@ -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';
|
||||
?>
|
||||
@@ -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';
|
||||
?>
|
||||