Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b40720b15c | ||
|
|
c619d77067 | ||
|
|
529f9aafd6 | ||
|
|
6e634a960b | ||
|
|
19ec4b9520 | ||
|
|
fa81b64558 | ||
|
|
a2a9a773f4 | ||
|
|
72dba8bcfa | ||
|
|
256a81a770 | ||
|
|
56d143b74b | ||
|
|
fa61d9f5ae | ||
|
|
abf812c6e9 |
@@ -0,0 +1,14 @@
|
||||
# Houd de build-context klein (context is de repo-root)
|
||||
.git
|
||||
.vs
|
||||
.env
|
||||
app
|
||||
Latex
|
||||
Build
|
||||
Proostenkrant.NET
|
||||
docker
|
||||
node_modules
|
||||
**/bin
|
||||
**/obj
|
||||
*.pdf
|
||||
*.log
|
||||
@@ -0,0 +1,16 @@
|
||||
# ProostenKrant Environment Configuration
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=mysql
|
||||
DB_NAME=proostenkrant
|
||||
DB_USER=proostenkrant
|
||||
DB_PASSWORD=proostenkrant
|
||||
|
||||
# Application Configuration
|
||||
UNIVERSAL_PASSWORD=ProostenKrant2024!
|
||||
|
||||
# PHP Configuration
|
||||
UPLOAD_MAX_FILESIZE=50M
|
||||
POST_MAX_SIZE=52M
|
||||
MEMORY_LIMIT=512M
|
||||
MAX_EXECUTION_TIME=300
|
||||
@@ -1,301 +1,56 @@
|
||||
## Core latex/pdflatex auxiliary files:
|
||||
*.aux
|
||||
*.lof
|
||||
# Dependencies
|
||||
/vendor
|
||||
composer.lock
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
.docker
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
*.lot
|
||||
*.fls
|
||||
*.out
|
||||
*.toc
|
||||
*.fmt
|
||||
*.fot
|
||||
*.cb
|
||||
*.cb2
|
||||
.*.lb
|
||||
|
||||
## Intermediate documents:
|
||||
*.dvi
|
||||
*.xdv
|
||||
*-converted-to.*
|
||||
# these rules might exclude image files for figures etc.
|
||||
# *.ps
|
||||
# *.eps
|
||||
# *.pdf
|
||||
|
||||
## Generated if empty string is given at "Please type another file name for output:"
|
||||
.pdf
|
||||
|
||||
## Bibliography auxiliary files (bibtex/biblatex/biber):
|
||||
*.bbl
|
||||
*.bcf
|
||||
*.blg
|
||||
*-blx.aux
|
||||
*-blx.bib
|
||||
*.run.xml
|
||||
|
||||
## Build tool auxiliary files:
|
||||
*.fdb_latexmk
|
||||
*.synctex
|
||||
*.synctex(busy)
|
||||
*.synctex.gz
|
||||
*.synctex.gz(busy)
|
||||
*.pdfsync
|
||||
|
||||
## Build tool directories for auxiliary files
|
||||
# latexrun
|
||||
latex.out/
|
||||
|
||||
## Auxiliary and intermediate files from other packages:
|
||||
# algorithms
|
||||
*.alg
|
||||
*.loa
|
||||
|
||||
# achemso
|
||||
acs-*.bib
|
||||
|
||||
# amsthm
|
||||
*.thm
|
||||
|
||||
# beamer
|
||||
*.nav
|
||||
*.pre
|
||||
*.snm
|
||||
*.vrb
|
||||
|
||||
# changes
|
||||
*.soc
|
||||
|
||||
# comment
|
||||
*.cut
|
||||
|
||||
# cprotect
|
||||
*.cpt
|
||||
|
||||
# elsarticle (documentclass of Elsevier journals)
|
||||
*.spl
|
||||
|
||||
# endnotes
|
||||
*.ent
|
||||
|
||||
# fixme
|
||||
*.lox
|
||||
|
||||
# feynmf/feynmp
|
||||
*.mf
|
||||
*.mp
|
||||
*.t[1-9]
|
||||
*.t[1-9][0-9]
|
||||
*.tfm
|
||||
|
||||
#(r)(e)ledmac/(r)(e)ledpar
|
||||
*.end
|
||||
*.?end
|
||||
*.[1-9]
|
||||
*.[1-9][0-9]
|
||||
*.[1-9][0-9][0-9]
|
||||
*.[1-9]R
|
||||
*.[1-9][0-9]R
|
||||
*.[1-9][0-9][0-9]R
|
||||
*.eledsec[1-9]
|
||||
*.eledsec[1-9]R
|
||||
*.eledsec[1-9][0-9]
|
||||
*.eledsec[1-9][0-9]R
|
||||
*.eledsec[1-9][0-9][0-9]
|
||||
*.eledsec[1-9][0-9][0-9]R
|
||||
|
||||
# glossaries
|
||||
*.acn
|
||||
*.acr
|
||||
*.glg
|
||||
*.glo
|
||||
*.gls
|
||||
*.glsdefs
|
||||
*.lzo
|
||||
*.lzs
|
||||
*.slg
|
||||
*.slo
|
||||
*.sls
|
||||
|
||||
# uncomment this for glossaries-extra (will ignore makeindex's style files!)
|
||||
# *.ist
|
||||
|
||||
# gnuplot
|
||||
*.gnuplot
|
||||
*.table
|
||||
|
||||
# gnuplottex
|
||||
*-gnuplottex-*
|
||||
|
||||
# gregoriotex
|
||||
*.gaux
|
||||
*.glog
|
||||
*.gtex
|
||||
|
||||
# htlatex
|
||||
*.4ct
|
||||
*.4tc
|
||||
*.idv
|
||||
*.lg
|
||||
*.trc
|
||||
*.xref
|
||||
|
||||
# hyperref
|
||||
*.brf
|
||||
|
||||
# knitr
|
||||
*-concordance.tex
|
||||
# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files
|
||||
# *.tikz
|
||||
*-tikzDictionary
|
||||
|
||||
# listings
|
||||
*.lol
|
||||
|
||||
# luatexja-ruby
|
||||
*.ltjruby
|
||||
|
||||
# makeidx
|
||||
*.idx
|
||||
*.ilg
|
||||
*.ind
|
||||
|
||||
# minitoc
|
||||
*.maf
|
||||
*.mlf
|
||||
*.mlt
|
||||
*.mtc[0-9]*
|
||||
*.slf[0-9]*
|
||||
*.slt[0-9]*
|
||||
*.stc[0-9]*
|
||||
|
||||
# minted
|
||||
_minted*
|
||||
*.pyg
|
||||
|
||||
# morewrites
|
||||
*.mw
|
||||
|
||||
# newpax
|
||||
*.newpax
|
||||
|
||||
# nomencl
|
||||
*.nlg
|
||||
*.nlo
|
||||
*.nls
|
||||
|
||||
# pax
|
||||
*.pax
|
||||
|
||||
# pdfpcnotes
|
||||
*.pdfpc
|
||||
|
||||
# sagetex
|
||||
*.sagetex.sage
|
||||
*.sagetex.py
|
||||
*.sagetex.scmd
|
||||
|
||||
# scrwfile
|
||||
*.wrt
|
||||
|
||||
# svg
|
||||
svg-inkscape/
|
||||
|
||||
# sympy
|
||||
*.sout
|
||||
*.sympy
|
||||
sympy-plots-for-*.tex/
|
||||
|
||||
# pdfcomment
|
||||
*.upa
|
||||
*.upb
|
||||
|
||||
# pythontex
|
||||
*.pytxcode
|
||||
pythontex-files-*/
|
||||
|
||||
# tcolorbox
|
||||
*.listing
|
||||
|
||||
# thmtools
|
||||
*.loe
|
||||
|
||||
# TikZ & PGF
|
||||
*.dpth
|
||||
*.md5
|
||||
*.auxlock
|
||||
|
||||
# titletoc
|
||||
*.ptc
|
||||
|
||||
# todonotes
|
||||
*.tdo
|
||||
|
||||
# vhistory
|
||||
*.hst
|
||||
*.ver
|
||||
|
||||
# easy-todo
|
||||
*.lod
|
||||
|
||||
# xcolor
|
||||
*.xcp
|
||||
|
||||
# xmpincl
|
||||
*.xmpi
|
||||
|
||||
# xindy
|
||||
*.xdy
|
||||
|
||||
# xypic precompiled matrices and outlines
|
||||
*.xyc
|
||||
*.xyd
|
||||
|
||||
# endfloat
|
||||
*.ttt
|
||||
*.fff
|
||||
|
||||
# Latexian
|
||||
TSWLatexianTemp*
|
||||
|
||||
## Editors:
|
||||
# WinEdt
|
||||
*.bak
|
||||
*.sav
|
||||
|
||||
# Texpad
|
||||
.texpadtmp
|
||||
|
||||
# LyX
|
||||
*.lyx~
|
||||
|
||||
# Kile
|
||||
*.backup
|
||||
|
||||
# gummi
|
||||
.*.swp
|
||||
|
||||
# KBibTeX
|
||||
*~[0-9]*
|
||||
|
||||
# TeXnicCenter
|
||||
*.tps
|
||||
|
||||
# auto folder when using emacs and auctex
|
||||
./auto/*
|
||||
*.el
|
||||
|
||||
# expex forward references with \gathertags
|
||||
*-tags.tex
|
||||
|
||||
# standalone packages
|
||||
*.sta
|
||||
|
||||
# Makeindex log files
|
||||
*.lpz
|
||||
|
||||
# xwatermark package
|
||||
*.xwm
|
||||
|
||||
# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib
|
||||
# option is specified. Footnotes are the stored in a file with suffix Notes.bib.
|
||||
# Uncomment the next line to have this generated file ignored.
|
||||
#*Notes.bib
|
||||
logs/
|
||||
|
||||
# Artikeluploads via de website (runtime gegenereerd)
|
||||
resources/images/articles/*
|
||||
!resources/images/articles/.gitkeep
|
||||
|
||||
# Database dumps
|
||||
*.sql
|
||||
!deploy/mysql/init.sql
|
||||
backup/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
cache/
|
||||
|
||||
# Build files
|
||||
/build
|
||||
/obj
|
||||
|
||||
# Old project files
|
||||
*.exe
|
||||
*.pdb
|
||||
*.dll
|
||||
*.config
|
||||
*.sln
|
||||
|
||||
# Blazor (.NET) build artifacts
|
||||
src/**/bin/
|
||||
src/**/obj/
|
||||
*.user
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ExpandedNodes": [
|
||||
""
|
||||
],
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
\headline{Annick en Cedric vieren 25 jaar Ateljee Hagelbos}
|
||||
\byline{Vrijdag 23 September}{Annick en Cedric}
|
||||
|
||||
\showTwoWriters{Fotos/Profielfotos/Cedric.png}{Cedric}{Fotos/Profielfotos/Annick.png}{Annick}
|
||||
|
||||
Wij komen net van de viering van 25-jarig bestaan van Ateljee Hagelbos. Onze Cedric zijn werk. Het was heel leuk met optredens, receptie ( veel toastjes en Fanta) en een rondleiding. Nu breng ik Cedric terug naar huis 20. Hij is content dat hij nu eens niet moet wandelen en met de mama meekan met de auto. \newline \par
|
||||
|
||||
\showInlineImage{Fotos/Artikels/Annick.jpeg}{25 jaar Ateljee}
|
||||
@@ -1,12 +0,0 @@
|
||||
\headline{Ontbijt in Zoersel}
|
||||
\byline{Zondag 2 Oktober}{Chiel, Mathilde en Pieter}
|
||||
|
||||
\showTwoWriters{Fotos/Profielfotos/Mathilde.jpeg}{Mathilde}{Fotos/Profielfotos/Pieter.jpeg}{Pieter}
|
||||
|
||||
\showOneWriter{Fotos/Profielfotos/Unknown.png}{Anoniem kind}
|
||||
|
||||
\par
|
||||
|
||||
Dochter Christine gaat met haar kroost lekker ontbijten op haar school in Zoersel. Iedereen buikje rond en voldaan. In een interview met één van de kinderen die anoniem wou blijven liet de persoon in kwestie weten dat de croissants een beetje taai waren maar dat de mama heel hard bedankt is voor de traktatie.
|
||||
|
||||
\showInlineImage{Fotos/Artikels/Chiel.jpeg}{Christine (P/k)roost}
|
||||
@@ -1,8 +0,0 @@
|
||||
\headline{Bibberen in de schoolstraat}
|
||||
\byline{Maandag 26 september}{Christine}
|
||||
|
||||
\showOneWriter{Fotos/Profielfotos/Chris.jpeg}{Christine}
|
||||
|
||||
In de Schoolstraat wordt gebibberd. De inbouwhaard moest eruit en de nieuwe gietijzeren stoof van 159 kg moest erin. Bezint voor ge begint! Een CO vergiftiging en 1 week later is dit het resultaat. Voorlopig bibberen we nog steeds want ze mag pas na 28 dagen aan. Kwestie van alle specie goed te laten drogen. We houden het hoofd koel...en de rest ook.
|
||||
|
||||
\showInlineImage{Fotos/Artikels/Chris.jpeg}{Verbouwingswerken Van Der Veken}
|
||||
@@ -1,12 +0,0 @@
|
||||
\headline{Rocken op de werkvloer}
|
||||
\byline{Donderdag 29 september}{Daan}
|
||||
|
||||
\showOneWriter{Fotos/Profielfotos/Daan.jpg}{Daan}
|
||||
|
||||
Op 29 september wordt er bij GMI group, waar ik werk, een jaarlijkse "Kick-Off" georganiseerd. Dit is een personeelsfeest waar alle 105 werknemers samenkomen voor een hapje en een drankje. \par
|
||||
Voor de editie van dit jaar heb ik me laten overtuigen om samen met twee werknemers te zorgen voor de muzikale invulling van de avond, onder de bandnaam ‘the Consultants of Swing’ (als variatie op The Sultans of Swing van The Dire Straits). Tijdens de presentatie zorgden wij drieën voor tunes, of korte live gebrachte geluidsfragmenten tussen de presentaties van het management door. Voor deze tunes speelde ik de drums, samen met mijn collega’s op basgitaar en keyboard. \par
|
||||
Na de presentaties was ik er echter nog niet vanaf; na een walking dinner heb ik op gitaar nog enkele nummers meegespeeld met de coverband die het entertainment van de rest van de avond verzorgde. Het zou natuurlijk een enorme gemiste kans geweest zijn als we ‘the (con)Sultan(t)s of Swing’ niet zouden spelen van the Dire Straits, dus ook dat nummer maakte deel uit van het repertoire! \par
|
||||
Twee maal met knikkende knieën het podium op, maar achteraf toch blij dat ik de stap gewaagd heb en kunnen genieten van een fijne avond! \par
|
||||
Vòòr de kick-off heb ik al even een foto kunnen nemen van de opgestelde instrumenten. Ik heb in het publiek echter een fotograaf gespot, dus wanneer ik meerdere sfeerbeelden ontvang, komen deze ook zeker jullie richting uit! \par
|
||||
|
||||
\showInlineImage{Fotos/Artikels/Daan.jpeg}{Het podium}
|
||||
@@ -1,8 +0,0 @@
|
||||
\headline{Verrassing soep voor Gunter}
|
||||
\byline{Maandag 19 september}{Daan en Dries}
|
||||
|
||||
\showTwoWriters{Fotos/Profielfotos/Daan.jpg}{Daan}{Fotos/Profielfotos/Dries.jpeg}{Dries}
|
||||
|
||||
Als verrassing voor onze pa zijn 53e verjaardag, bereiden Dries en ik een driegangenmenu voor. We beginnen alvast met het voorgerecht: een courgetteroomsoep met spekjes!
|
||||
|
||||
\showInlineImage{Fotos/Artikels/DaanEnDries.jpeg}{De gedekte tafel}
|
||||
@@ -1,8 +0,0 @@
|
||||
\headline{Diede en Vincent naar Antwerpen voor wat cultuur}
|
||||
\byline{Zaterdag 24 September}{Diede en Vincent}
|
||||
|
||||
\showTwoWriters{Fotos/Profielfotos/Died.jpeg}{Diede}{Fotos/Profielfotos/Vincent.jpg}{Vincent}
|
||||
|
||||
Op zaterdag 24 september zijn wij naar de heropening van het Koninklijk Museum voor Schone Kunsten Antwerpen geweest. Vier vrienden die in Antwerpen wonen waren ook mee gegaan. Ze hadden 11 jaar gewerkt aan het museum dus het was echt de moeite! Eigenlijk vonden we het gebouw zelf nog mooier dan de kunstwerken die er hingen. Na het museum zijn we met z'n allen ramen gaan eten (een soort noedelsoep met groenten) en nog op café geweest. Dus desondanks de regen was het toch nog een hele leuke dag! \newline \par
|
||||
|
||||
\showInlineImage{Fotos/Artikels/Diede.jpg}{Koninklijk Museum voor $\\$ Schone Kunsten}
|
||||
@@ -1,8 +0,0 @@
|
||||
\headline{Avonturieren in Malaga}
|
||||
\byline{Van 21 tot 26 september}{Dien}
|
||||
|
||||
\showOneWriter{Fotos/Profielfotos/dien.jpeg}{Dien}
|
||||
|
||||
Twee weken na mijn zomerverlof, vond ik het weeral tijd om de zon op te gaan zoeken. Zeker omdat het in België koud en nat was. Ik trok met mijn vriendin Katrien naar Malaga waar het nog 29 graden was. We kozen een hotel met een zwembad boven op het dak en vlakbij het centrum van de stad. De eerste dag bezochten we een prachtige kathedraal en heb ik het mooiste schilderij van mijn leven gezien. En we bezochten het centre Pompidou. Op dag twee reden we met een bus naar het noord westelijk deel van de provincie Malaga en bezochten we een kloof "caminito del rey" genaamd. Het wandelpad hing 100 meter hoog en hoewel ik hoogtevrees heb, heb ik het helemaal tot het einde afgelopen. De gids heeft me geholpen toen ik een licht paniekske kreeg. Moe van de inspanning,hebben we het op dag drie rustig gehouden. We bezochten het Picassomuseum en verder werd het een stranddagje. Zalig was dat, drankje,boekje en genieten van de zon. Op de vierde dag bezochten we twee kastelen uit de jaren 1500 boven op een berg. Was prachtig maar heel steil om er te geraken. De laatste dag hebben we nog heel intens genoten van het zwembad op het dak en de laatste zonnestralen en vlogen toen weer naar huis. Onderweg hebben we heel veel turbulentie gehad in het vliegtuig wat best wel eng was. Samengevat,het was een heerlijk verlof en Katrien heeft er ook heel erg van genoten na alle miserie die ze heeft meegemaakt de laatste twee jaren. \newline \par
|
||||
|
||||
\showInlineImage{Fotos/Artikels/dien.jpeg}{Brug in Caminito del rey}
|
||||
@@ -1,8 +0,0 @@
|
||||
\headline{Toeristen in Luik}
|
||||
\byline{22 tot 24 september}{Douwe en Pauline}
|
||||
|
||||
\showTwoWriters{Fotos/Profielfotos/Douwe.jpeg}{Douwe}{Fotos/Profielfotos/Pauline.jpeg}{Pauline}
|
||||
|
||||
Van 22 tot 24 september zijn wij op weekend geweest naar Luik. We verbleven in een mooi appartementje vlak naast de cathédrale Saint-Paul. Vrijdagvoormiddag waagden we ons aan de 374 treden van de Montagne de Bueren, die uitkomt aan de citadel van Luik. Dat ging vlotjes (lees: Douwe heeft Pauline de laatste meters omhoog geduwd). Ook het station van Luik was een bezoekje waard, want sinds augustus kreeg het allemaal verschillende kleurtjes voor een kunstproject. Ons weekendje sloten we af met een Indisch etentje, maar we kregen allebei het verkeerde gerecht. Toch heeft het gesmaakt, al kregen we zonder twijfel hét pikantste op de kaart. \newline \par
|
||||
|
||||
\showInlineImage{Fotos/Artikels/PaulineDouwe.jpeg}{Bovenaan de Montagne de Bueren}
|
||||
@@ -1,10 +0,0 @@
|
||||
\headline{Smullen in de Kleynhoeve}
|
||||
\byline{Donderdag 1 September}{Lucca en Dries}
|
||||
|
||||
\showTwoWriters{Fotos/Profielfotos/Dries.jpeg}{Dries}{Fotos/Profielfotos/Lucca.jpeg}{Lucca}
|
||||
|
||||
Wij zijn een maand geleden voor ons 2 jaar samen zijn lekker gaan eten in de Kleynhoeve in retie. Dries heeft voor de eerste keer escargots gegegen. Het was heel lekker en gezellig en we zijn nog steeds heel gelukkig samen. \par
|
||||
\showInlineImage{Fotos/Artikels/Lucca.jpeg}{Dries nerveus voor $\\$ zijn eerste escargots}
|
||||
|
||||
Toen we vertrokken naar de Kleynhoeve voelde Lucca iets zitten in haar oor, Dries zag niks zitten. Na het eten zijn we gaan slapen en heeft Lucca kei veel zeer aan haar oor gehad. 's Ochtends voelde ze iets en pakt ze dat er uit. Bleek dat gewoon een f*cking spin te zijn. \par
|
||||
\showInlineImage{Fotos/Artikels/Spin.jpeg}{Lucca's oorbewoner}
|
||||
@@ -1,10 +0,0 @@
|
||||
\headline{Het verhaal van Gunter en de duiven}
|
||||
\byline{Lang lang geleden...}{Gunter}
|
||||
|
||||
\showOneWriter{Fotos/Profielfotos/Gunter.jpg}{Gunter}
|
||||
|
||||
Gunter had maar een korte zin ingestuurd dus ik (Douwe) heb gebruik gemaakt van een computerprogramma met artificiële intelligentie die van één zin een verhaaltje maakt. \par
|
||||
|
||||
Gunter zorgt voor duiven. Deze duiven waren klaar voor een wedstrijd duivenvliegen. Gunter voelde de stress van zijn verantwoordelijkheid. Het was moeilijk om je een wereld voor te stellen zonder duiven, of een wereld zonder mensen die wisten hoe ze op ze moesten jagen. Onze voorouders hadden gemakkelijk een stad om hen heen kunnen bouwen, een dierentuin, als je wilt. Hij zette een val voor duiven op de binnenplaats. Hij zou er één voor één een handvol uithalen. Terwijl de duif naar de val zou vliegen, greep Gunter hem in zijn hand en gooide hem terug in zijn kooi.\par
|
||||
|
||||
\showInlineImage{Fotos/Artikels/Gunter.jpeg}{Gunter klaar om duiven te vangen}
|
||||
@@ -1,2 +0,0 @@
|
||||
Liefste bomma en bompa,
|
||||
Omdat we jullie up-to-date willen houden van wat er allemaal gebeurd in onze levens roepen we vanaf vandaag het maandelijkse ‘Proostenkrantje’ in het leven. Daarmee willen we jullie op de hoogte houden van wat er allemaal gebeurt in onze levens. Bezie het maar als sociale media maar dan op een traditionele wijze! En als je een artikel wilt ‘liken’ zoals op Facebook, dan steken jullie je duim maar omhoog, dan weten wij dat jullie het een leuk artikel vinden!
|
||||
@@ -1,9 +0,0 @@
|
||||
\headline{De jongste proost op zijn proclamatie}
|
||||
\byline{Vrijdag 30 september}{Mattis}
|
||||
|
||||
\showOneWriter{Fotos/Profielfotos/Mattis.jpeg}{Mattis}
|
||||
|
||||
\par
|
||||
|
||||
Mattis heeft op 30 september zijn proclamatie gehad op de Thomas More in Geel. Op deze proclamatie heeft Mattis zijn diploma als maatschappelijk werker overhandigd gekregen. Nu kan hij aan de volgende stap beginnen. Namelijk werk gaan zoeken.
|
||||
\showInlineImage{Fotos/Artikels/Mattis.jpeg}{WK hoeden gooien}
|
||||
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 505 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 538 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 684 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 955 KiB |
@@ -0,0 +1,48 @@
|
||||
# ProostenKrant — Blazor versie
|
||||
# De texlive-installatie zit in een eigen Docker-laag; normale builds
|
||||
# herinstalleren LaTeX dus NIET. Gebruik `clean` met zorg (verwijdert ook de MySQL-data).
|
||||
|
||||
.PHONY: help build up down restart logs logs-app logs-mysql shell mysql clean
|
||||
|
||||
help:
|
||||
@echo "ProostenKrant (Blazor) development commands"
|
||||
@echo ""
|
||||
@echo " build - Build images en start containers"
|
||||
@echo " up - Start containers"
|
||||
@echo " down - Stop containers"
|
||||
@echo " restart - Restart containers"
|
||||
@echo " logs - Vol alle logs"
|
||||
@echo " logs-app - Vol app logs"
|
||||
@echo " shell - Shell in de app container"
|
||||
@echo " mysql - MySQL shell"
|
||||
@echo " clean - Verwijder containers én volumes (mysql data gaat verloren!)"
|
||||
|
||||
build:
|
||||
docker compose up -d --build
|
||||
@echo "Applicatie draait op http://localhost:8080"
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
@echo "Applicatie draait op http://localhost:8080"
|
||||
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
restart:
|
||||
docker compose restart
|
||||
|
||||
logs:
|
||||
docker compose logs -f
|
||||
|
||||
logs-app:
|
||||
docker compose logs -f app
|
||||
|
||||
shell:
|
||||
docker compose exec app bash
|
||||
|
||||
mysql:
|
||||
docker compose exec mysql mysql -u proostenkrant -pproostenkrant proostenkrant
|
||||
|
||||
clean:
|
||||
docker compose down -v
|
||||
@echo "Containers en volumes verwijderd (ook mysql-data)"
|
||||
@@ -1,2 +1,136 @@
|
||||
# ProostenKrant
|
||||
A newspaper for our family
|
||||
# ProostenKrant 3.0 — Blazor
|
||||
|
||||
De ProostenKrant is een webkrant van en voor de familie Proosten: familieleden leveren
|
||||
artikelen in via de website, de redactie publiceert ze per seizoeneditie en drukt de
|
||||
krant als PDF via LaTeX.
|
||||
|
||||
Deze versie vervangt de PHP/MySQL-applicatie door een **Blazor Server**-applicatie
|
||||
(.NET 10) met dezelfde database en hetzelfde LaTeX-krantenontwerp.
|
||||
|
||||
## Architectuur
|
||||
|
||||
| Onderdeel | Keuze |
|
||||
|---------------|---------------------------------------------------|
|
||||
| Framework | Blazor Web App (static SSR, geen JavaScript nodig) |
|
||||
| Database | MySQL 8 (zelfde schema als voorheen, Dapper) |
|
||||
| Authenticatie | Cookie-auth met universeel wachtwoord + paszin |
|
||||
| PDF-generatie | LaTeX (`newspaper`-pakket) aangeroepen vanuit .NET |
|
||||
| Hosting | Docker Compose: `app` (Blazor + texlive) + `mysql`|
|
||||
|
||||
**Waarom Blazor Server en niet WebAssembly?** De applicatie is servergebonden:
|
||||
MySQL-toegang, bestandsuploads en LaTeX-compilatie gebeuren allemaal op de server.
|
||||
Met WASM zou je een aparte REST-API met token-authenticatie moeten bouwen en dezelfde
|
||||
werkzaamheden toch op de server doen. Server-side rendering is bovendien sneller voor
|
||||
een kleine gebruikersgroep en werkt zonder JavaScript.
|
||||
|
||||
**Waarom LaTeX en niet een .NET PDF-bibliotheek?** .NET kan goede PDF's genereren
|
||||
(bijv. met QuestPDF, gratis Community-licentie), maar echte krantenzetting —
|
||||
automatische tekstverdeling over drie kolommen, microtype, woordafbreking,
|
||||
kopbalancering — is waar TeX in uitblinkt. De krant blijft daarom met LaTeX gedrukt,
|
||||
nu aangestuurd vanuit .NET.
|
||||
|
||||
## Snel starten
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Daarna:
|
||||
|
||||
- Website: http://localhost:8080
|
||||
- Inloggen redactie: kies je naam + universel wachtwoord (standaard `ProostenKrant2024!`,
|
||||
via `UNIVERSAL_PASSWORD` in `.env`)
|
||||
- Paszin voor inzendingen: hetzelfde wachtwoord
|
||||
|
||||
De eerste build duurt lang (de texlive-installatie is ±2 GB). Daarna cachet Docker de
|
||||
LaTeX-laag: **hertesten vraagt geen herinstallatie meer**, zolang je niet
|
||||
`--no-cache` gebruikt.
|
||||
|
||||
## Gebruik
|
||||
|
||||
| Route | Doel |
|
||||
|------------------------------|---------------------------------------------------|
|
||||
| `/` | Voorpagina (krantenstijl) |
|
||||
| `/indienen` | Artikel inleveren via paszin |
|
||||
| `/indienen/bewerken` | Kladartikel bewerken (alleen de schrijver) |
|
||||
| `/archief` | Gedrukte edities + gepubliceerde artikelen |
|
||||
| `/login` | Redactie-login |
|
||||
| `/dashboard` | Overzicht, statistieken, krant drukken |
|
||||
| `/artikelen`, `/artikelen/nieuw`, `/artikelen/{id}` | Artikelbeheer (redactie) |
|
||||
| `/seizoenen`, `/seizoenen/{id}` | Edities drukken (redactie) |
|
||||
| `/edities/{bestand}` | PDF-download |
|
||||
|
||||
Werkstroom: familie dient in via `/indienen` (status *klad*) → redactie publiceert
|
||||
via `/artikelen/{id}` → redactie drukt de editie via `/seizoenen` of `/dashboard`.
|
||||
|
||||
## Projectstructuur
|
||||
|
||||
```
|
||||
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 (resources volume-mounted)
|
||||
├── deploy/
|
||||
│ ├── Dockerfile # Build + runtime met texlive (gecachete laag)
|
||||
│ └── mysql/init.sql # Database-initialisatie
|
||||
├── 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 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).
|
||||
|
||||
## Deployen op een server
|
||||
|
||||
1. Vereiste: Docker + Docker Compose op de server.
|
||||
2. Clone de repository en zet in `.env` een **sterk wachtwoord** voordat je live gaat:
|
||||
- `UNIVERSAL_PASSWORD` — inlogwachtwoord én paszin (standaard `ProostenKrant2024!`)
|
||||
- `DB_PASSWORD` / `MYSQL_ROOT_PASSWORD` — database (`MYSQL_ROOT_PASSWORD` staat in `compose.yml`)
|
||||
3. Start: `docker compose up -d --build` — de database wordt automatisch geïnitialiseerd
|
||||
(users incl. Kevin, Luc en Tilde; seizoenen).
|
||||
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: 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.
|
||||
|
||||
## Configuratie
|
||||
|
||||
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 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`.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# ProostenKrant — Blazor Server stack
|
||||
# Gebruik: docker compose up -d --build
|
||||
# App: http://localhost:8080
|
||||
|
||||
name: proostenkrant
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/Dockerfile
|
||||
container_name: proostenkrant-app
|
||||
ports:
|
||||
- "8050:8080"
|
||||
environment:
|
||||
DB_HOST: mysql
|
||||
DB_NAME: ${DB_NAME:-proostenkrant}
|
||||
DB_USER: ${DB_USER:-proostenkrant}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-proostenkrant}
|
||||
UNIVERSAL_PASSWORD: ${UNIVERSAL_PASSWORD:-ProostenKrant2024!}
|
||||
HOOGTES_BEWAREN: ${HOOGTES_BEWAREN:-0}
|
||||
volumes:
|
||||
# Foto's en afbeeldingen uit de resources-map: profielfoto's
|
||||
# (resources/images/profiles) en artikeluploads (resources/images/articles)
|
||||
- ./resources/images:/app/wwwroot/resources/images
|
||||
# Archief-edities: bestaande PDF's tonen én pas gedrukte kranten
|
||||
# worden hier bewaard (resources/archive)
|
||||
- ./resources/archive:/app/archive
|
||||
# DataProtection-sleutels (versleuteling van login-cookies en
|
||||
# antiforgery-tokens). Zonder dit volume worden alle sessies
|
||||
# ongeldig bij elke herdeploy van de app-container.
|
||||
- app-dataprotection:/root/.aspnet/DataProtection-Keys
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: proostenkrant-mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpassword
|
||||
MYSQL_DATABASE: ${DB_NAME:-proostenkrant}
|
||||
MYSQL_USER: ${DB_USER:-proostenkrant}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD:-proostenkrant}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ./deploy/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
ports:
|
||||
- "3306:3306"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-prootpassword"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
app-dataprotection:
|
||||
@@ -0,0 +1,46 @@
|
||||
# ProostenKrant — Blazor Server + LaTeX
|
||||
# De texlive-installatie staat in een eigen, vroege laag van de runtime-image,
|
||||
# zodat app-wijzigingen de (trage) LaTeX-installatie niet opnieuw triggeren.
|
||||
|
||||
# ---------- Build ----------
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY src/ProostenKrant/ProostenKrant.csproj ./ProostenKrant/
|
||||
RUN dotnet restore ProostenKrant/ProostenKrant.csproj
|
||||
|
||||
COPY src/ProostenKrant/ ./ProostenKrant/
|
||||
RUN dotnet publish ProostenKrant/ProostenKrant.csproj -c Release -o /app/publish
|
||||
|
||||
# ---------- Runtime ----------
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
|
||||
# LaTeX (eenmalig geinstalleerd, daarna door Docker-laagcache stabiel)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
texlive-latex-base \
|
||||
texlive-latex-recommended \
|
||||
texlive-latex-extra \
|
||||
texlive-fonts-recommended \
|
||||
texlive-fonts-extra \
|
||||
texlive-publishers \
|
||||
texlive-pictures \
|
||||
texlive-lang-european \
|
||||
ghostscript \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# binhex.tex (nodig voor newtx) zit in texlive-plain-generic.
|
||||
# Aparte laag: wijzigingen hierboven herinstalleren de grote texlive-laag niet.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
texlive-plain-generic \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:8080 \
|
||||
ASPNETCORE_ENVIRONMENT=Production
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
RUN mkdir -p /app/wwwroot/resources/images/profiles /app/wwwroot/resources/images/articles /app/archive
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "ProostenKrant.dll"]
|
||||
@@ -0,0 +1,93 @@
|
||||
-- ProostenKrant Database Schema
|
||||
-- Dutch Family Newspaper System
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS proostenkrant;
|
||||
USE proostenkrant;
|
||||
|
||||
-- Users table (family members)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
profile_image VARCHAR(255),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Seasons table
|
||||
CREATE TABLE IF NOT EXISTS seasons (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Articles table
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
author_id INT NOT NULL,
|
||||
participants TEXT, -- Comma-separated list of user IDs
|
||||
event_date DATE NOT NULL,
|
||||
season_id INT,
|
||||
article_image VARCHAR(255),
|
||||
status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- System settings table
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
key_name VARCHAR(100) NOT NULL UNIQUE,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert default seasons
|
||||
INSERT INTO seasons (name, description, start_date, end_date) VALUES
|
||||
('Lente', 'Lente editie - Voorjaar', '2026-03-20', '2026-06-20'),
|
||||
('Zomer', 'Zomer editie', '2026-06-21', '2026-09-22'),
|
||||
('Herfst', 'Herfst editie', '2026-09-23', '2026-12-20'),
|
||||
('Winter', 'Winter editie', '2026-12-21', '2026-03-19');
|
||||
|
||||
-- Insert default users (family members)
|
||||
INSERT INTO users (username, display_name, profile_image) VALUES
|
||||
('douwe', 'Douwe', NULL),
|
||||
('pauline', 'Pauline', NULL),
|
||||
('diede', 'Diede', NULL),
|
||||
('dien', 'Dien', NULL),
|
||||
('chris', 'Chris', NULL),
|
||||
('geert', 'Geert', NULL),
|
||||
('annick', 'Annick', NULL),
|
||||
('cedric', 'Cedric', NULL),
|
||||
('gunter', 'Gunter', NULL),
|
||||
('daan', 'Daan', NULL),
|
||||
('kyala', 'Kyala', NULL),
|
||||
('michel', 'Michel', NULL),
|
||||
('dries', 'Dries', NULL),
|
||||
('pieter', 'Pieter', NULL),
|
||||
('mathilde', 'Mathilde', NULL),
|
||||
('kevin', 'Kevin', NULL),
|
||||
('luc', 'Luc', NULL),
|
||||
('tilde', 'Tilde', NULL),
|
||||
('mattis', 'Mattis', NULL),
|
||||
('chiel', 'Chiel', NULL),
|
||||
('tinne', 'Tinne', NULL);
|
||||
|
||||
-- Insert universal password setting
|
||||
INSERT INTO settings (key_name, value) VALUES
|
||||
('universal_password', 'ProostenKrant2024!');
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_articles_author ON articles(author_id);
|
||||
CREATE INDEX idx_articles_season ON articles(season_id);
|
||||
CREATE INDEX idx_articles_date ON articles(event_date);
|
||||
CREATE INDEX idx_articles_status ON articles(status);
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM texlive/texlive:latest
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
ghostscript \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /var/www
|
||||
|
||||
# LaTeX is already installed in the texlive image
|
||||
# No need to install additional packages
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
COPY docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,18 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /var/www/html/public;
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
FROM php:8.2-fpm-alpine
|
||||
|
||||
# Install dependencies for GD extension
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
freetype-dev \
|
||||
libjpeg-turbo-dev \
|
||||
libpng-dev \
|
||||
libwebp-dev \
|
||||
zlib-dev
|
||||
|
||||
# Install and enable GD extension with all image format support
|
||||
# Note: --with-png and --with-jpeg are now built-in, only need freetype and webp
|
||||
RUN docker-php-ext-configure gd \
|
||||
--with-freetype \
|
||||
--with-webp
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install -j$(nproc) \
|
||||
gd \
|
||||
mysqli \
|
||||
pdo \
|
||||
pdo_mysql
|
||||
|
||||
# Clean up build dependencies
|
||||
RUN apk del .build-deps
|
||||
|
||||
# Also install runtime libraries for GD
|
||||
RUN apk add --no-cache \
|
||||
freetype \
|
||||
libjpeg-turbo \
|
||||
libpng \
|
||||
libwebp \
|
||||
zlib
|
||||
|
||||
# Copy PHP configuration
|
||||
COPY docker/php/php.ini /usr/local/etc/php/conf.d/custom.ini
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
CMD ["php-fpm"]
|
||||
@@ -0,0 +1,7 @@
|
||||
upload_max_filesize = 50M
|
||||
post_max_size = 52M
|
||||
memory_limit = 512M
|
||||
max_execution_time = 300
|
||||
display_errors = On
|
||||
error_reporting = E_ALL
|
||||
date.timezone = Europe/Brussels
|
||||
@@ -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}
|
||||
$
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
\documentclass[10pt]{article}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{microtype}
|
||||
\usepackage[dutch]{babel}
|
||||
\usepackage{newspaper}
|
||||
\usepackage[letterpaper,left=36pt,right=36pt,top=0.55in,bottom=36pt,headheight=40pt,headsep=12pt]{geometry}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{tikz}
|
||||
\usepackage[export]{adjustbox}
|
||||
\usepackage{calc}
|
||||
\usepackage{newtxtext}
|
||||
|
||||
\date{\today}
|
||||
\currentvolume{1}
|
||||
\currentissue{1}
|
||||
\SetPaperName{Proo{s:}tenKrant}
|
||||
\SetHeaderName{ProostenKrant}
|
||||
\SetPaperLocation{Regio Kempen}
|
||||
\SetPaperSlogan{``Een krant van en voor Proosten.''}
|
||||
\SetPaperPrice{NUL EURO}
|
||||
|
||||
\newcommand{\blocksep}{\par\nointerlineskip\vspace{11pt}\noindent{\rule{\linewidth}{0.4pt}}\par\vspace{11pt}}
|
||||
\newcommand{\colrule}{\hfill{\vrule width 0.3pt}\hfill}
|
||||
\begin{document}
|
||||
\maketitle
|
||||
\noindent\textbf{\LARGE Editie: Herfst}\par
|
||||
\vspace{8pt}
|
||||
|
||||
\expandafter\newsavebox\csname blk1W\endcsname
|
||||
\expandafter\savebox\csname blk1W\endcsname{\parbox{356.0pt}{{\noindent{\LARGE\itshape De Proostenkrant is back baby!!!}\par
|
||||
\vspace{3pt}{\footnotesize\scshape 9 oktober 2026 --- Douwe}\par
|
||||
\vspace{6pt}\noindent
|
||||
\begin{minipage}[t]{0.63\linewidth}
|
||||
\vspace*{0pt}Whoehoe! We voelde allemaal dat er iets miste in ons leven de laatste tijd. Sommige dachten dat dat een McDonalds in Schoonbroek was, andere dachten dat ze gewoon naar de andere kant van de wereld moesten reizen voor onbepaalde duur, maar gelukkig weet ik echt wat er mist. De 4 jaarlijkse editie van de ProostenKrant. Met men eigen server-infrastructuur ter Turnhout kan ik nu lekker gemakkelijk zelf de ProostenKrant website hosten en kunnen jullie allemaal nog gemakkelijker artikels indienen. Dus aan allen der Proosten, verdeel en rapporteer u!\par
|
||||
\end{minipage}
|
||||
\hfill
|
||||
\begin{minipage}[t]{0.34\linewidth}
|
||||
\vspace*{0pt}\setlength{\fboxsep}{3pt}\framebox{\includegraphics[max width=\dimexpr\linewidth-2\fboxsep-2\fboxrule\relax,max height=0.26\textheight,keepaspectratio]{/app/wwwroot/resources/images/articles/05568e0b_ScreenshotFrom20261009133307.png}}\par
|
||||
\end{minipage}
|
||||
\par\vspace{6pt}\centering\begin{minipage}[c]{103.0pt}\centering\begin{tikzpicture}\clip (0,0) circle (44.5pt);\node at (0,0) {\includegraphics[width=89.0pt,height=89.0pt,keepaspectratio]{/app/wwwroot/resources/images/profiles/Douwe.jpg}};\end{tikzpicture}\par\vspace{2pt}{\footnotesize Douwe}\end{minipage}\par
|
||||
}
|
||||
}}
|
||||
|
||||
\noindent
|
||||
\begin{minipage}[t]{356.0pt}
|
||||
\expandafter\usebox\csname blk1W\endcsname\par\vspace{14pt}
|
||||
\noindent
|
||||
\begin{minipage}[t]{172.0pt}
|
||||
\vspace*{0pt}
|
||||
\end{minipage}\colrule
|
||||
\begin{minipage}[t]{172.0pt}
|
||||
\vspace*{0pt}
|
||||
\end{minipage}
|
||||
\end{minipage}\colrule
|
||||
\begin{minipage}[t]{172.0pt}
|
||||
\vspace*{0pt}
|
||||
\end{minipage}
|
||||
\end{document}
|
||||
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 733 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 482 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 745 KiB After Width: | Height: | Size: 745 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@@ -30,43 +30,24 @@
|
||||
\usepackage{graphicx}
|
||||
\usepackage{tikz}
|
||||
\usepackage{multicol}
|
||||
\usepackage{subfiles}
|
||||
\usepackage{newspaper-mod}
|
||||
\usepackage{geometry}
|
||||
\usepackage{array}
|
||||
\usepackage{wrapfig}
|
||||
\usepackage{lipsum}
|
||||
\usepackage{newtxtext,newtxmath}
|
||||
\usepackage{etoolbox}
|
||||
|
||||
% Custom styling for better image handling
|
||||
\input{newspaper-mod}
|
||||
|
||||
%%%%%%%%% Front matter %%%%%%%%%%
|
||||
|
||||
\usepackage{lipsum}
|
||||
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
\begin{multicols}{3}
|
||||
|
||||
\subfile{Artikels/Intro}
|
||||
\closearticle
|
||||
\subfile{Artikels/Annick}
|
||||
\closearticle
|
||||
\subfile{Artikels/Died}
|
||||
\closearticle
|
||||
\subfile{Artikels/Douwe}
|
||||
\closearticle
|
||||
\subfile{Artikels/Dien}
|
||||
\closearticle
|
||||
\subfile{Artikels/Chris}
|
||||
\closearticle
|
||||
\subfile{Artikels/Chiel}
|
||||
\closearticle
|
||||
\subfile{Artikels/Gunter}
|
||||
\closearticle
|
||||
\subfile{Artikels/Dries}
|
||||
\closearticle
|
||||
\subfile{Artikels/Mattis}
|
||||
\closearticle
|
||||
\subfile{Artikels/DaanEnDries}
|
||||
\closearticle
|
||||
\subfile{Artikels/Daan}
|
||||
\closearticle
|
||||
% Articles will be inserted here by the PHP generator
|
||||
|
||||
\newpage
|
||||
\end{multicols}
|
||||
@@ -0,0 +1,119 @@
|
||||
% Improved newspaper-mod.sty for ProostenKrant
|
||||
% Better image handling for different formats and sizes
|
||||
|
||||
\newcommand{\headlinestyle}{\itshape\huge}
|
||||
\newcommand{\bylinestyle}{\scshape\Large}
|
||||
\patchcmd{\headline}{#1}{\headlinestyle #1}{}{}
|
||||
\patchcmd{\byline}{#1}{\bylinestyle #1}{}{}
|
||||
|
||||
% Improved image commands with better handling of different aspect ratios
|
||||
|
||||
% Show inline image with caption (automatically handles landscape and portrait)
|
||||
\newcommand{\showInlineImage}[2]
|
||||
{
|
||||
\\\n \hspace*{0pt}% Ensure proper spacing
|
||||
\vspace*{10pt}% Add space before image
|
||||
\begin{center}
|
||||
\begin{minipage}{0.95\columnwidth}
|
||||
\centering
|
||||
% Use max dimensions that fit in column
|
||||
\setlength{\fboxsep}{5pt}
|
||||
\framebox{
|
||||
% Check image dimensions and scale appropriately
|
||||
\includegraphics[max width=\linewidth, max height=0.6\textheight, keepaspectratio]{#1}
|
||||
}
|
||||
\vspace*{5pt}
|
||||
{\small #2}
|
||||
\end{minipage}
|
||||
\end{center}
|
||||
\vspace*{10pt}% Add space after image
|
||||
\par
|
||||
}
|
||||
|
||||
% Show single writer with circular profile image
|
||||
\newcommand{\showOneWriter}[2]
|
||||
{
|
||||
\\\n \hfill
|
||||
\begin{minipage}[t]{0.4\columnwidth}
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
% Circular clip for profile image
|
||||
\clip (0,0) circle (1.3cm);
|
||||
% Use square aspect ratio and scale to fit
|
||||
\node[anchor=center] at (0,0) {
|
||||
\includegraphics[width=2.8cm, height=2.8cm, keepaspectratio]{#1}
|
||||
};
|
||||
\end{tikzpicture}
|
||||
\vspace*{5pt}
|
||||
{\small #2}
|
||||
\end{minipage}
|
||||
\hfill
|
||||
\\\n}
|
||||
|
||||
% Show two writers with circular profile images
|
||||
\newcommand{\showTwoWriters}[4]
|
||||
{
|
||||
\\\n \begin{center}
|
||||
\begin{minipage}[t]{0.8\columnwidth}
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\clip (0,0) circle (1.1cm);
|
||||
\node[anchor=west] at (0,0) {
|
||||
\includegraphics[width=2.4cm, height=2.4cm, keepaspectratio]{#1}
|
||||
};
|
||||
\end{tikzpicture}
|
||||
\hspace*{15pt}
|
||||
\begin{tikzpicture}
|
||||
\clip (0,0) circle (1.1cm);
|
||||
\node[anchor=east] at (0,0) {
|
||||
\includegraphics[width=2.4cm, height=2.4cm, keepaspectratio]{#3}
|
||||
};
|
||||
\end{tikzpicture}
|
||||
\vspace*{8pt}
|
||||
{\small #2 \\quad #4}
|
||||
\end{minipage}
|
||||
\end{center}
|
||||
\\\n}
|
||||
|
||||
% Alternative commands for different layouts
|
||||
|
||||
% For portrait images - maintains aspect ratio
|
||||
\newcommand{\showPortraitImage}[2]
|
||||
{
|
||||
\\\n \begin{minipage}[t]{0.45\columnwidth}
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
% For portrait images, use vertical layout
|
||||
\clip (0,-1.5cm) rectangle (2cm,1.5cm);
|
||||
\node[anchor=center] at (1cm,0) {
|
||||
\includegraphics[height=3cm, keepaspectratio]{#1}
|
||||
};
|
||||
\end{tikzpicture}
|
||||
\vspace*{5pt}
|
||||
{\small #2}
|
||||
\end{minipage}
|
||||
\\\n}
|
||||
|
||||
% For landscape images - maintains aspect ratio
|
||||
\newcommand{\showLandscapeImage}[2]
|
||||
{
|
||||
\\\n \begin{minipage}[t]{\columnwidth}
|
||||
\centering
|
||||
% For landscape images, use full width
|
||||
\includegraphics[width=\linewidth, height=0.4\textheight, keepaspectratio]{#1}
|
||||
\vspace*{5pt}
|
||||
{\small #2}
|
||||
\end{minipage}
|
||||
\\\n}
|
||||
|
||||
% Flexible image wrapper that automatically handles different orientations
|
||||
\newcommand{\showFlexibleImage}[2]
|
||||
{
|
||||
% First, we need to check the image dimensions
|
||||
% This requires the epstopdf package and shell-escape
|
||||
\IfFileExists{#1}{
|
||||
% Image exists, try to determine orientation
|
||||
% For now, we'll use a balanced approach that works for both
|
||||
\showInlineImage{#1}{#2}
|
||||
}{}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
\documentclass[10pt]{article}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{microtype}
|
||||
\usepackage[dutch]{babel}
|
||||
\usepackage{newspaper}
|
||||
\usepackage[letterpaper,left=36pt,right=36pt,top=0.55in,bottom=36pt,headheight=40pt,headsep=12pt]{geometry}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{tikz}
|
||||
\usepackage[export]{adjustbox}
|
||||
\usepackage{calc}
|
||||
\usepackage{newtxtext}
|
||||
|
||||
\date{\today}
|
||||
\currentvolume{1}
|
||||
\currentissue{1}
|
||||
\SetPaperName{Proo{s:}tenKrant}
|
||||
\SetHeaderName{ProostenKrant}
|
||||
\SetPaperLocation{Regio Kempen}
|
||||
\SetPaperSlogan{``Een krant van en voor Proosten.''}
|
||||
\SetPaperPrice{NUL EURO}
|
||||
|
||||
\newcommand{\blocksep}{\par\nointerlineskip\vspace{11pt}\noindent{\rule{\linewidth}{0.4pt}}\par\vspace{11pt}}
|
||||
\newcommand{\colrule}{\hfill{\vrule width 0.3pt}\hfill}
|
||||
\newwrite\hoogtes
|
||||
\immediate\openout\hoogtes=hoogtes.txt
|
||||
\begin{document}
|
||||
\maketitle
|
||||
\noindent\textbf{\LARGE Editie: Herfst}\par
|
||||
\vspace{8pt}
|
||||
\pdfsavepos
|
||||
\newpage
|
||||
\immediate\write\hoogtes{MASTHEAD \the\pdflastypos}
|
||||
\immediate\write\hoogtes{GEOM \the\textwidth,\the\textheight}
|
||||
|
||||
\expandafter\newsavebox\csname blk1V\endcsname
|
||||
\expandafter\savebox\csname blk1V\endcsname{\parbox{172.0pt}{{\noindent{\large\itshape De Proostenkrant is back baby!!!}\par
|
||||
\vspace{3pt}{\footnotesize\scshape 9 oktober 2026 --- Douwe}\par
|
||||
\vspace{5pt}Whoehoe! We voelde allemaal dat er iets miste in ons leven de laatste tijd. Sommige dachten dat dat een McDonalds in Schoonbroek was, andere dachten dat ze gewoon naar de andere kant van de wereld moesten reizen voor onbepaalde duur, maar gelukkig weet ik echt wat er mist. De 4 jaarlijkse editie van de ProostenKrant. Met men eigen server-infrastructuur ter Turnhout kan ik nu lekker gemakkelijk zelf de ProostenKrant website hosten en kunnen jullie allemaal nog gemakkelijker artikels indienen. \par Dus aan allen der Proosten, verdeel en rapporteer u!\par
|
||||
\vspace{5pt}\centering\begin{minipage}[c]{57.0pt}\centering\begin{tikzpicture}\clip (0,0) circle (21.5pt);\node at (0,0) {\includegraphics[width=43.0pt,height=43.0pt,keepaspectratio]{/app/wwwroot/resources/images/profiles/Douwe.jpg}};\end{tikzpicture}\par\vspace{2pt}{\footnotesize Douwe}\end{minipage}\par
|
||||
\vspace{5pt}\setlength{\fboxsep}{3pt}\framebox{\includegraphics[max width=\dimexpr\linewidth-2\fboxsep-2\fboxrule\relax,max height=0.22\textheight,keepaspectratio]{/app/wwwroot/resources/images/articles/05568e0b_ScreenshotFrom20261009133307.png}}\par
|
||||
}}}
|
||||
\setlength{\dimen0}{\ht\csname blk1V\endcsname}%
|
||||
\advance\dimen0 by \dp\csname blk1V\endcsname
|
||||
\immediate\write\hoogtes{H 1 V \the\dimen0}
|
||||
|
||||
\expandafter\newsavebox\csname blk1H\endcsname
|
||||
\expandafter\savebox\csname blk1H\endcsname{\parbox{172.0pt}{{\noindent
|
||||
\begin{minipage}[t]{0.63\linewidth}
|
||||
\vspace*{0pt}\noindent{\large\itshape De Proostenkrant is back baby!!!}\par
|
||||
\vspace{3pt}{\footnotesize\scshape 9 oktober 2026 --- Douwe}\par
|
||||
\vspace{5pt}Whoehoe! We voelde allemaal dat er iets miste in ons leven de laatste tijd. Sommige dachten dat dat een McDonalds in Schoonbroek was, andere dachten dat ze gewoon naar de andere kant van de wereld moesten reizen voor onbepaalde duur, maar gelukkig weet ik echt wat er mist. De 4 jaarlijkse editie van de ProostenKrant. Met men eigen server-infrastructuur ter Turnhout kan ik nu lekker gemakkelijk zelf de ProostenKrant website hosten en kunnen jullie allemaal nog gemakkelijker artikels indienen. \par Dus aan allen der Proosten, verdeel en rapporteer u!\par
|
||||
\end{minipage}
|
||||
\hfill
|
||||
\begin{minipage}[t]{0.34\linewidth}
|
||||
\vspace*{0pt}\setlength{\fboxsep}{3pt}\framebox{\includegraphics[max width=\dimexpr\linewidth-2\fboxsep-2\fboxrule\relax,max height=0.30\textheight,keepaspectratio]{/app/wwwroot/resources/images/articles/05568e0b_ScreenshotFrom20261009133307.png}}\par
|
||||
\end{minipage}
|
||||
\par\vspace{5pt}\centering\begin{minipage}[c]{57.0pt}\centering\begin{tikzpicture}\clip (0,0) circle (21.5pt);\node at (0,0) {\includegraphics[width=43.0pt,height=43.0pt,keepaspectratio]{/app/wwwroot/resources/images/profiles/Douwe.jpg}};\end{tikzpicture}\par\vspace{2pt}{\footnotesize Douwe}\end{minipage}\par
|
||||
}}}
|
||||
\setlength{\dimen0}{\ht\csname blk1H\endcsname}%
|
||||
\advance\dimen0 by \dp\csname blk1H\endcsname
|
||||
\immediate\write\hoogtes{H 1 H \the\dimen0}
|
||||
|
||||
\expandafter\newsavebox\csname blk1W\endcsname
|
||||
\expandafter\savebox\csname blk1W\endcsname{\parbox{356.0pt}{{\noindent{\LARGE\itshape De Proostenkrant is back baby!!!}\par
|
||||
\vspace{3pt}{\footnotesize\scshape 9 oktober 2026 --- Douwe}\par
|
||||
\vspace{6pt}\noindent
|
||||
\begin{minipage}[t]{0.63\linewidth}
|
||||
\vspace*{0pt}Whoehoe! We voelde allemaal dat er iets miste in ons leven de laatste tijd. Sommige dachten dat dat een McDonalds in Schoonbroek was, andere dachten dat ze gewoon naar de andere kant van de wereld moesten reizen voor onbepaalde duur, maar gelukkig weet ik echt wat er mist. De 4 jaarlijkse editie van de ProostenKrant. Met men eigen server-infrastructuur ter Turnhout kan ik nu lekker gemakkelijk zelf de ProostenKrant website hosten en kunnen jullie allemaal nog gemakkelijker artikels indienen. \par Dus aan allen der Proosten, verdeel en rapporteer u!\par
|
||||
\end{minipage}
|
||||
\hfill
|
||||
\begin{minipage}[t]{0.34\linewidth}
|
||||
\vspace*{0pt}\setlength{\fboxsep}{3pt}\framebox{\includegraphics[max width=\dimexpr\linewidth-2\fboxsep-2\fboxrule\relax,max height=0.26\textheight,keepaspectratio]{/app/wwwroot/resources/images/articles/05568e0b_ScreenshotFrom20261009133307.png}}\par
|
||||
\end{minipage}
|
||||
\par\vspace{6pt}\centering\begin{minipage}[c]{103.0pt}\centering\begin{tikzpicture}\clip (0,0) circle (44.5pt);\node at (0,0) {\includegraphics[width=89.0pt,height=89.0pt,keepaspectratio]{/app/wwwroot/resources/images/profiles/Douwe.jpg}};\end{tikzpicture}\par\vspace{2pt}{\footnotesize Douwe}\end{minipage}\par
|
||||
}}}
|
||||
\setlength{\dimen0}{\ht\csname blk1W\endcsname}%
|
||||
\advance\dimen0 by \dp\csname blk1W\endcsname
|
||||
\immediate\write\hoogtes{H 1 W \the\dimen0}
|
||||
\end{document}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="icon" href="images/logo.jpg" type="image/jpeg" />
|
||||
<link rel="stylesheet" href="css/site.css" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
<header class="masthead">
|
||||
<div class="masthead-meta">
|
||||
<span>@Krant.Location</span>
|
||||
<span class="masthead-date">@Krant.DutchDate(DateTime.Now)</span>
|
||||
<span>@(_isEditor ? "Redactie: " + _editorName : "NUL EURO")</span>
|
||||
</div>
|
||||
<p class="masthead-title"><a href="/">Proo<span class="long-s">s</span>tenKrant</a></p>
|
||||
<p class="masthead-slogan">„@Krant.Slogan“</p>
|
||||
<nav class="main-nav">
|
||||
<NavLink href="/" Match="NavLinkMatch.All">Voorpagina</NavLink>
|
||||
<NavLink href="/indienen">Krant indienen</NavLink>
|
||||
<NavLink href="/archief">Archief</NavLink>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<NavLink href="/dashboard">Dashboard</NavLink>
|
||||
<NavLink href="/artikelen">Artikelen</NavLink>
|
||||
<NavLink href="/seizoenen">Seizoenen</NavLink>
|
||||
<NavLink href="/uitloggen" class="nav-uitloggen">Uitloggen</NavLink>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<NavLink href="/login">Inloggen</NavLink>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="sheet">
|
||||
@Body
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer-inner">
|
||||
<span>© @DateTime.Now.Year @Krant.AppName — Alle rechten voorbehouden</span>
|
||||
<span class="footer-eu">
|
||||
Made and hosted in
|
||||
<img src="images/european-flag.svg" class="eu-flag" alt="Europese vlag" width="24" height="16" />
|
||||
Europe
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@code {
|
||||
private bool _isEditor;
|
||||
private string? _editorName;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
_isEditor = KrantAuth.IsEditor(state.User);
|
||||
_editorName = KrantAuth.EditorName(state.User);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
@page "/archief"
|
||||
@inject LatexService Latex
|
||||
@inject ArticleService Artikelen
|
||||
@inject SeasonService Seizoenen
|
||||
|
||||
<PageTitle>Archief — ProostenKrant</PageTitle>
|
||||
|
||||
<h1 class="page-heading">Archief</h1>
|
||||
<p class="page-intro">Alle gedrukte edities van de ProostenKrant en de gepubliceerde artikelen per seizoen.</p>
|
||||
|
||||
<h2 class="section-heading">Gedrukte edities</h2>
|
||||
@if (_edities.Count > 0)
|
||||
{
|
||||
<table class="krant-tabel">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Editie</th>
|
||||
<th>Datum</th>
|
||||
<th>Grootte</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var editie in _edities)
|
||||
{
|
||||
<tr>
|
||||
<td>@editie.DisplayName</td>
|
||||
<td>@Krant.DutchShortDate(editie.Date)</td>
|
||||
<td>@(editie.SizeBytes / 1024) kB</td>
|
||||
<td><a class="btn" href="/edities/@editie.FileName">Download PDF</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted">Er zijn nog geen gedrukte edities beschikbaar.</p>
|
||||
}
|
||||
|
||||
<h2 class="section-heading">Gepubliceerde artikelen</h2>
|
||||
@foreach (var seizoen in _seizoenenMetArtikelen)
|
||||
{
|
||||
<section class="season-archive">
|
||||
<h3 class="season-heading seizoen-@seizoen.Season?.CssKey">
|
||||
@(seizoen.Season?.Name ?? "Zonder seizoen")
|
||||
@if (seizoen.Season is not null && seizoen.Season.StartDate <= DateTime.Now && seizoen.Season.EndDate >= DateTime.Now)
|
||||
{
|
||||
<span class="tag">huidige editie</span>
|
||||
}
|
||||
</h3>
|
||||
@if (seizoen.Articles.Count == 0)
|
||||
{
|
||||
<p class="muted">Nog geen gepubliceerde artikelen.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="archive-article-list">
|
||||
@foreach (var artikel in seizoen.Articles)
|
||||
{
|
||||
<li>
|
||||
<a href="/artikelen/@artikel.Id" class="archive-article-link">@artikel.Title</a>
|
||||
<span class="byline-inline">— @artikel.AuthorName, @Krant.DutchShortDate(artikel.EventDate)</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<Edition> _edities = new();
|
||||
private List<(Season? Season, List<Article> Articles)> _seizoenenMetArtikelen = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_edities = Latex.ListEditions();
|
||||
var seasons = await Seizoenen.GetAllAsync();
|
||||
var published = await Artikelen.GetAllAsync("published");
|
||||
_seizoenenMetArtikelen = seasons
|
||||
.Select(s => (s, published.Where(a => a.SeasonId == s.Id).ToList()))
|
||||
.Select(x => (Season: (Season?)x.Item1, Articles: x.Item2))
|
||||
.ToList();
|
||||
var overige = published.Where(a => a.SeasonId is null || seasons.All(s => s.Id != a.SeasonId)).ToList();
|
||||
if (overige.Count > 0)
|
||||
{
|
||||
_seizoenenMetArtikelen.Add((null, overige));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
@page "/artikelen/{Id:int}"
|
||||
@inject ArticleService Artikelen
|
||||
@inject UserService Gebruikers
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject IWebHostEnvironment Env
|
||||
|
||||
<PageTitle>@(_artikel?.Title ?? "Artikel") — ProostenKrant</PageTitle>
|
||||
|
||||
@if (_artikel is null)
|
||||
{
|
||||
<section class="card">
|
||||
<h1 class="headline">Artikel niet gevonden</h1>
|
||||
<p>Dit artikel bestaat niet (meer). <a href="/artikelen">Naar het overzicht</a>.</p>
|
||||
</section>
|
||||
return;
|
||||
}
|
||||
|
||||
<article class="article-page">
|
||||
<span class="kicker">@(string.IsNullOrEmpty(_artikel.SeasonName) ? "ProostenKrant" : _artikel.SeasonName)</span>
|
||||
<h1 class="article-headline">@_artikel.Title</h1>
|
||||
<p class="byline">
|
||||
Door @_artikel.AuthorName
|
||||
@if (_artikel.EventDate is not null)
|
||||
{
|
||||
<text>— @Krant.DutchShortDate(_artikel.EventDate)</text>
|
||||
}
|
||||
@if (_isEditor)
|
||||
{
|
||||
<span class="status status-@_artikel.Status">@(_artikel.Status == "published" ? "gepubliceerd" : "klad")</span>
|
||||
}
|
||||
</p>
|
||||
|
||||
@if (_deelnemers.Count > 0)
|
||||
{
|
||||
<div class="participant-row">
|
||||
@foreach (var deelnemer in _deelnemers)
|
||||
{
|
||||
<span class="participant">
|
||||
<img class="participant-avatar" src="@ProfileWebPath(deelnemer)" alt="@deelnemer.DisplayName" />
|
||||
<span>@deelnemer.DisplayName</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_artikel.WebImage))
|
||||
{
|
||||
<figure class="article-figure">
|
||||
<img src="@_artikel.WebImage" alt="@_artikel.Title" />
|
||||
</figure>
|
||||
}
|
||||
|
||||
<div class="article-body">
|
||||
@foreach (var alinea in _artikel.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
<p>@alinea</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_isEditor)
|
||||
{
|
||||
<div class="editor-actions">
|
||||
@if (_artikel.Status == "draft")
|
||||
{
|
||||
<EditForm FormName="publiceer" Method="post" Model="PubliceerInput" OnValidSubmit="Publiceer" class="inline-form">
|
||||
<button type="submit" class="btn btn-primary">Publiceren</button>
|
||||
</EditForm>
|
||||
}
|
||||
else
|
||||
{
|
||||
<EditForm FormName="depubliceer" Method="post" Model="DepubliceerInput" OnValidSubmit="Depubliceer" class="inline-form">
|
||||
<button type="submit" class="btn">Terug naar klad</button>
|
||||
</EditForm>
|
||||
}
|
||||
<a class="btn" href="/indienen/bewerken/@_artikel.Id">Bewerken</a>
|
||||
<EditForm FormName="verwijder" Method="post" Model="VerwijderInput" OnValidSubmit="Verwijder" class="inline-form">
|
||||
<button type="submit" class="btn btn-danger">Verwijderen</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private Article? _artikel;
|
||||
private List<User> _deelnemers = new();
|
||||
private bool _isEditor;
|
||||
[SupplyParameterFromForm(FormName = "publiceer")]
|
||||
private ActieInput PubliceerInput { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromForm(FormName = "depubliceer")]
|
||||
private ActieInput DepubliceerInput { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromForm(FormName = "verwijder")]
|
||||
private ActieInput VerwijderInput { get; set; } = new();
|
||||
|
||||
private sealed class ActieInput { }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
PubliceerInput ??= new();
|
||||
DepubliceerInput ??= new();
|
||||
VerwijderInput ??= new();
|
||||
_artikel = await Artikelen.FindAsync(Id);
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
_isEditor = KrantAuth.IsEditor(state.User);
|
||||
if (_artikel is not null && (_artikel.Status == "published" || _isEditor))
|
||||
{
|
||||
_deelnemers = await Gebruikers.GetByIdsAsync(_artikel.ParticipantIds);
|
||||
}
|
||||
}
|
||||
|
||||
private string ProfileWebPath(User user) => Gebruikers.GetProfileWebPath(user);
|
||||
|
||||
private async Task Publiceer()
|
||||
{
|
||||
if (_artikel is not null)
|
||||
{
|
||||
await Artikelen.SetStatusAsync(_artikel.Id, "published");
|
||||
Nav.NavigateTo($"/artikelen/{Id}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Depubliceer()
|
||||
{
|
||||
if (_artikel is not null)
|
||||
{
|
||||
await Artikelen.SetStatusAsync(_artikel.Id, "draft");
|
||||
Nav.NavigateTo($"/artikelen/{Id}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Verwijder()
|
||||
{
|
||||
if (_artikel is not null)
|
||||
{
|
||||
await Artikelen.DeleteAsync(_artikel.Id);
|
||||
Nav.NavigateTo("/artikelen", forceLoad: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
@page "/artikelen/nieuw"
|
||||
@attribute [Authorize(Policy = "Editor")]
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inject ArticleService Artikelen
|
||||
@inject SeasonService Seizoenen
|
||||
@inject UserService Gebruikers
|
||||
@inject UploadService Uploads
|
||||
|
||||
<PageTitle>Nieuw artikel — ProostenKrant</PageTitle>
|
||||
|
||||
<section class="card">
|
||||
<h1 class="headline">Nieuw artikel</h1>
|
||||
<p class="muted">Artikel voor de redactie aanmaken en eventueel direct publiceren.</p>
|
||||
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<p class="alert alert-error">@_fout</p>
|
||||
}
|
||||
|
||||
<EditForm FormName="artikel-nieuw" Method="post" Model="Input" OnValidSubmit="Opslaan"
|
||||
class="krant-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="title">Titel</label>
|
||||
<InputText @bind-Value="Input.Title" id="title" class="form-control" placeholder="De kop boven het artikel" />
|
||||
<ValidationMessage For="() => Input.Title" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="content">Tekst</label>
|
||||
<InputTextArea @bind-Value="Input.Content" id="content" rows="10" class="form-control" placeholder="Schrijf hier de inhoud van je artikel…" />
|
||||
<ValidationMessage For="() => Input.Content" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="writer">Schrijver</label>
|
||||
<InputSelect @bind-Value="Input.WriterId" id="writer" class="form-control">
|
||||
<option value="0">— Kies schrijver —</option>
|
||||
@foreach (var gebruiker in _gebruikers)
|
||||
{
|
||||
<option value="@gebruiker.Id">@gebruiker.DisplayName</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="() => Input.WriterId" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="eventdate">Datum van de gebeurtenis</label>
|
||||
<InputDate @bind-Value="Input.EventDate" id="eventdate" class="form-control" />
|
||||
<ValidationMessage For="() => Input.EventDate" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="season">Seizoen</label>
|
||||
<InputSelect @bind-Value="Input.SeasonId" id="season" class="form-control">
|
||||
<option value="0">— Kies seizoen —</option>
|
||||
@foreach (var seizoen in _seizoenen)
|
||||
{
|
||||
<option value="@seizoen.Id">@seizoen.Name</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="() => Input.SeasonId" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<InputSelect @bind-Value="Input.Status" id="status" class="form-control">
|
||||
<option value="draft">Klad</option>
|
||||
<option value="published">Gepubliceerd</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Deelnemers</label>
|
||||
<div class="checkbox-group">
|
||||
@foreach (var gebruiker in _gebruikers)
|
||||
{
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="Participants" value="@gebruiker.Id"
|
||||
checked="@(_geselecteerd.Contains(gebruiker.Id))" />
|
||||
<span>@gebruiker.DisplayName</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
@if (_participantsFout is not null)
|
||||
{
|
||||
<span class="error-message">@_participantsFout</span>
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="image">Afbeelding (optioneel)</label>
|
||||
<input type="file" name="Afbeelding" id="image" class="form-control" accept="image/*" />
|
||||
<span class="help-text">JPG, PNG, GIF of WEBP, maximaal 25 MB.</span>
|
||||
@if (_afbeeldingFout is not null)
|
||||
{
|
||||
<span class="error-message">@_afbeeldingFout</span>
|
||||
}
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Artikel opslaan</button>
|
||||
<a class="btn" href="/artikelen">Annuleren</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm(FormName = "artikel-nieuw")]
|
||||
private ArtikelInput Input { get; set; } = new();
|
||||
|
||||
private List<User> _gebruikers = new();
|
||||
private List<Season> _seizoenen = new();
|
||||
private List<int> _geselecteerd = new();
|
||||
private string? _fout;
|
||||
private string? _participantsFout;
|
||||
private string? _afbeeldingFout;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Input ??= new();
|
||||
_gebruikers = await Gebruikers.GetAllAsync();
|
||||
_seizoenen = await Seizoenen.GetAllAsync();
|
||||
var huidig = await Seizoenen.GetCurrentAsync();
|
||||
Input.SeasonId ??= huidig?.Id ?? (_seizoenen.FirstOrDefault()?.Id ?? 0);
|
||||
Input.EventDate ??= DateTime.Today;
|
||||
Input.Status ??= "published";
|
||||
}
|
||||
|
||||
private async Task Opslaan()
|
||||
{
|
||||
Input ??= new();
|
||||
var form = HttpContext.Request.Form;
|
||||
_geselecteerd = form["Participants"].SelectMany(v => (v ?? "").Split(','))
|
||||
.Select(s => int.TryParse(s, out var id) ? id : 0).Where(id => id > 0).ToList();
|
||||
if (Input.WriterId <= 0)
|
||||
{
|
||||
_fout = "Kies een schrijver.";
|
||||
return;
|
||||
}
|
||||
if (!_geselecteerd.Contains(Input.WriterId))
|
||||
{
|
||||
_geselecteerd.Insert(0, Input.WriterId);
|
||||
}
|
||||
if (_geselecteerd.Count == 0)
|
||||
{
|
||||
_participantsFout = "Selecteer minstens één deelnemer.";
|
||||
return;
|
||||
}
|
||||
|
||||
var (webPad, fout) = await Uploads.SaveArticleImageAsync(HttpContext.Request.Form.Files["Afbeelding"]);
|
||||
if (fout is not null)
|
||||
{
|
||||
_afbeeldingFout = fout;
|
||||
return;
|
||||
}
|
||||
|
||||
var artikel = new Article
|
||||
{
|
||||
Title = Input.Title,
|
||||
Content = Input.Content,
|
||||
AuthorId = Input.WriterId,
|
||||
Participants = string.Join(",", _geselecteerd),
|
||||
EventDate = Input.EventDate,
|
||||
SeasonId = Input.SeasonId,
|
||||
ArticleImage = webPad,
|
||||
Status = Input.Status == "published" ? "published" : "draft",
|
||||
};
|
||||
await Artikelen.CreateAsync(artikel);
|
||||
Nav.NavigateTo("/artikelen?melding=" + Uri.EscapeDataString("Artikel succesvol opgeslagen."), forceLoad: true);
|
||||
}
|
||||
|
||||
private sealed class ArtikelInput
|
||||
{
|
||||
[Required(ErrorMessage = "Geef het artikel een titel (minstens 5 tekens).")]
|
||||
[MinLength(5, ErrorMessage = "De titel moet minstens 5 tekens bevatten.")]
|
||||
[MaxLength(255)]
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Schrijf de tekst van het artikel (minstens 10 tekens).")]
|
||||
[MinLength(10, ErrorMessage = "De tekst moet minstens 10 tekens bevatten.")]
|
||||
public string Content { get; set; } = "";
|
||||
|
||||
public int WriterId { get; set; }
|
||||
|
||||
public DateTime? EventDate { get; set; }
|
||||
|
||||
public int? SeasonId { get; set; }
|
||||
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
@page "/artikelen"
|
||||
@attribute [Authorize(Policy = "Editor")]
|
||||
@inject ArticleService ArtikelSvc
|
||||
|
||||
<PageTitle>Artikelen — ProostenKrant</PageTitle>
|
||||
|
||||
<h1 class="page-heading">Artikelen</h1>
|
||||
|
||||
<nav class="filter-bar">
|
||||
<a href="/artikelen" class="btn @(SelectedStatus is null ? "btn-active" : "")">Alle</a>
|
||||
<a href="/artikelen?status=draft" class="btn @(_status == "draft" ? "btn-active" : "")">Klad</a>
|
||||
<a href="/artikelen?status=published" class="btn @(_status == "published" ? "btn-active" : "")">Gepubliceerd</a>
|
||||
<a href="/artikelen/nieuw" class="btn btn-primary">Nieuw artikel</a>
|
||||
</nav>
|
||||
|
||||
@if (_artikelen.Count == 0)
|
||||
{
|
||||
<p class="muted">Geen artikelen gevonden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="krant-tabel">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Schrijver</th>
|
||||
<th>Gebeurtenis</th>
|
||||
<th>Seizoen</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var artikel in _artikelen)
|
||||
{
|
||||
<tr>
|
||||
<td><a href="/artikelen/@artikel.Id">@artikel.Title</a></td>
|
||||
<td>@artikel.AuthorName</td>
|
||||
<td>@Krant.DutchShortDate(artikel.EventDate)</td>
|
||||
<td>@(artikel.SeasonName ?? "—")</td>
|
||||
<td><span class="status status-@artikel.Status">@(artikel.Status == "published" ? "gepubliceerd" : "klad")</span></td>
|
||||
<td><a class="btn" href="/artikelen/@artikel.Id">Openen</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromQuery(Name = "status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
private string? _status;
|
||||
private List<Article> _artikelen = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_status = Status;
|
||||
_artikelen = await ArtikelSvc.GetAllAsync(string.IsNullOrEmpty(Status) ? null : Status);
|
||||
}
|
||||
|
||||
private string? SelectedStatus => _status;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
@page "/indienen/bewerken/{Id:int}"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inject ArticleService Artikelen
|
||||
@inject UserService Gebruikers
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
<PageTitle>Verifieer schrijver — ProostenKrant</PageTitle>
|
||||
|
||||
@if (_artikel is null)
|
||||
{
|
||||
<section class="card">
|
||||
<h1 class="headline">Artikel niet gevonden</h1>
|
||||
<p><a href="/indienen/bewerken/lijst">Terug naar de lijst</a>.</p>
|
||||
</section>
|
||||
return;
|
||||
}
|
||||
|
||||
<section class="card auth-card">
|
||||
<span class="kicker">Beveiliging</span>
|
||||
<h1 class="headline">Wie schreef dit artikel?</h1>
|
||||
<p class="muted">
|
||||
Om „@_artikel.Title“ te bewerken, bevestig je eerst dat jij de schrijver bent.
|
||||
</p>
|
||||
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<p class="alert alert-error">@_fout</p>
|
||||
}
|
||||
|
||||
<EditForm FormName="verifieer-schrijver" Method="post" Model="Input" OnValidSubmit="Verifieer" class="krant-form">
|
||||
<div class="form-group">
|
||||
<label for="writer">Schrijver</label>
|
||||
<InputSelect @bind-Value="Input.WriterId" id="writer" class="form-control">
|
||||
<option value="0">— Kies schrijver —</option>
|
||||
@foreach (var gebruiker in _gebruikers)
|
||||
{
|
||||
<option value="@gebruiker.Id">@gebruiker.DisplayName</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="() => Input.WriterId" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Doorgaan naar bewerken</button>
|
||||
<a class="btn" href="/indienen/bewerken/lijst">Annuleren</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm(FormName = "verifieer-schrijver")]
|
||||
private VerificatieInput Input { get; set; } = new();
|
||||
|
||||
private Article? _artikel;
|
||||
private List<User> _gebruikers = new();
|
||||
private string? _fout;
|
||||
|
||||
private sealed class VerificatieInput
|
||||
{
|
||||
[Required(ErrorMessage = "Kies de schrijver.")]
|
||||
public int WriterId { get; set; }
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Input ??= new();
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (!KrantAuth.HasValidPassphrase(state.User) && !KrantAuth.IsEditor(state.User))
|
||||
{
|
||||
Nav.NavigateTo("/indienen/bewerken", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
_artikel = await Artikelen.FindAsync(Id);
|
||||
_gebruikers = await Gebruikers.GetAllAsync();
|
||||
|
||||
// Een ingelogd redactielid dat zelf de schrijver is, hoeft zich niet
|
||||
// te verifiëren: direct door naar het formulier, met behoud van de
|
||||
// redactiesessie. Artikelen van een andere schrijver volgen de
|
||||
// gewone controle hieronder.
|
||||
if (KrantAuth.IsEditor(state.User) && KrantAuth.Uid(state.User) is int uid &&
|
||||
_artikel is not null && uid == _artikel.AuthorId)
|
||||
{
|
||||
var gebruiker = await Gebruikers.FindAsync(uid);
|
||||
if (gebruiker is not null)
|
||||
{
|
||||
await HttpContext.SignInAsync(KrantAuth.Scheme,
|
||||
KrantAuth.EditorPrincipal(gebruiker, editArticleId: _artikel.Id));
|
||||
Nav.NavigateTo("/indienen/formulier", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Verifieer()
|
||||
{
|
||||
if (_artikel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Input.WriterId != _artikel.AuthorId)
|
||||
{
|
||||
_fout = "Alleen de schrijver van het artikel kan het bewerken.";
|
||||
return;
|
||||
}
|
||||
await HttpContext.SignInAsync(KrantAuth.Scheme,
|
||||
KrantAuth.PassphrasePrincipal(editArticleId: _artikel.Id));
|
||||
Nav.NavigateTo("/indienen/formulier", forceLoad: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
@page "/indienen/bewerken/lijst"
|
||||
@inject ArticleService Artikelen
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
<PageTitle>Kladartikelen — ProostenKrant</PageTitle>
|
||||
|
||||
<h1 class="page-heading">Kladartikelen bewerken</h1>
|
||||
<p class="page-intro">Kies het artikel dat je wilt aanpassen. Alleen de schrijver van het artikel kan het bewerken.</p>
|
||||
|
||||
@if (_klad.Count == 0)
|
||||
{
|
||||
<p class="muted">Er zijn momenteel geen kladartikelen.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="krant-tabel">
|
||||
<thead>
|
||||
<tr><th>Titel</th><th>Schrijver</th><th>Gebeurtenis</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var artikel in _klad)
|
||||
{
|
||||
<tr>
|
||||
<td>@artikel.Title</td>
|
||||
<td>@artikel.AuthorName</td>
|
||||
<td>@Krant.DutchShortDate(artikel.EventDate)</td>
|
||||
<td><a class="btn" href="/indienen/bewerken/@artikel.Id">Bewerken</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<Article> _klad = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (!KrantAuth.HasValidPassphrase(state.User) && !KrantAuth.IsEditor(state.User))
|
||||
{
|
||||
Nav.NavigateTo("/indienen/bewerken", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
_klad = await Artikelen.GetAllAsync("draft");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
@page "/indienen/bewerken"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
<PageTitle>Artikel bewerken — ProostenKrant</PageTitle>
|
||||
|
||||
<section class="card auth-card">
|
||||
<span class="kicker">Beveiliging</span>
|
||||
<h1 class="headline">Artikel bewerken</h1>
|
||||
<p class="muted">Voer de paszin in om de lijst met kladartikelen te openen.</p>
|
||||
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<p class="alert alert-error">@_fout</p>
|
||||
}
|
||||
|
||||
<EditForm FormName="paszin-bewerken" Method="post" Model="Input" OnValidSubmit="Verifieer" class="krant-form">
|
||||
<div class="form-group">
|
||||
<label for="passphrase">Paszin</label>
|
||||
<InputText type="password" @bind-Value="Input.Paszin" id="passphrase" class="form-control" autocomplete="off" />
|
||||
<ValidationMessage For="() => Input.Paszin" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Verder</button>
|
||||
<a class="btn" href="/">Annuleren</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm(FormName = "paszin-bewerken")]
|
||||
private PaszinInput Input { get; set; } = new();
|
||||
|
||||
private string? _fout;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Input ??= new();
|
||||
|
||||
// Ingelogde redactie hoeft geen paszin in te voeren.
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (KrantAuth.IsEditor(state.User))
|
||||
{
|
||||
Nav.NavigateTo("/indienen/bewerken/lijst", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PaszinInput
|
||||
{
|
||||
[Required(ErrorMessage = "Voer de paszin in.")]
|
||||
public string Paszin { get; set; } = "";
|
||||
}
|
||||
|
||||
private async Task Verifieer()
|
||||
{
|
||||
if (Input.Paszin != Krant.UniversalPassword)
|
||||
{
|
||||
_fout = "Onjuiste paszin. Probeer het opnieuw.";
|
||||
return;
|
||||
}
|
||||
await HttpContext.SignInAsync(KrantAuth.Scheme, KrantAuth.PassphrasePrincipal());
|
||||
Nav.NavigateTo("/indienen/bewerken/lijst", forceLoad: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
@page "/dashboard"
|
||||
@attribute [Authorize(Policy = "Editor")]
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject ArticleService Artikelen
|
||||
@inject SeasonService Seizoenen
|
||||
@inject LatexService Latex
|
||||
@inject UserService Gebruikers
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Af
|
||||
|
||||
<PageTitle>Dashboard — ProostenKrant</PageTitle>
|
||||
|
||||
<h1 class="page-heading">Dashboard redactie</h1>
|
||||
<p class="page-intro">Welkom, @_editorNaam. Overzicht van de krant en sneltoetsen voor de redactie.</p>
|
||||
|
||||
@if (_melding is not null)
|
||||
{
|
||||
<p class="alert alert-success">@_melding</p>
|
||||
}
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<div class="alert alert-error">
|
||||
<p>@_fout</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">@_drafts</span>
|
||||
<span class="stat-label">Kladartikelen</span>
|
||||
<a href="/artikelen?status=draft" class="stat-link">Bekijken →</a>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">@_gepubliceerd</span>
|
||||
<span class="stat-label">Gepubliceerd</span>
|
||||
<a href="/artikelen?status=published" class="stat-link">Bekijken →</a>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">@_gebruikers.Count</span>
|
||||
<span class="stat-label">Familieleden</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">@_edities.Count</span>
|
||||
<span class="stat-label">Gedrukte edities</span>
|
||||
<a href="/archief" class="stat-link">Archief →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-columns">
|
||||
<section class="dashboard-main">
|
||||
<h2 class="section-heading">Huidige editie</h2>
|
||||
@if (_huidigSeizoen is not null)
|
||||
{
|
||||
<div class="season-card seizoen-@_huidigSeizoen.CssKey">
|
||||
<h3>@_huidigSeizoen.Name @DateTime.Now.Year</h3>
|
||||
<p class="muted">
|
||||
Van @Krant.DutchShortDate(_huidigSeizoen.StartDate) t/m @Krant.DutchShortDate(_huidigSeizoen.EndDate).
|
||||
@(_seizoenDrafts.Count) klad, @(_seizoenPublished.Count) gepubliceerd.
|
||||
</p>
|
||||
<div class="form-actions">
|
||||
<form method="post" action="/api/genereer-pdf" class="inline-form">
|
||||
<input type="hidden" name="SeizoenId" value="@_huidigSeizoen.Id" />
|
||||
<input type="hidden" name="__RequestVerificationToken" value="@_afToken" />
|
||||
<button type="submit" class="btn btn-primary">Druk de @_huidigSeizoen.Name-editie</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted">Er is geen lopend seizoen gedefinieerd.</p>
|
||||
}
|
||||
|
||||
<h2 class="section-heading">Laatste artikelen</h2>
|
||||
<table class="krant-tabel">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Schrijver</th>
|
||||
<th>Seizoen</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var artikel in _recent.Take(8))
|
||||
{
|
||||
<tr>
|
||||
<td><a href="/artikelen/@artikel.Id">@artikel.Title</a></td>
|
||||
<td>@artikel.AuthorName</td>
|
||||
<td>@(artikel.SeasonName ?? "—")</td>
|
||||
<td>
|
||||
<span class="status status-@artikel.Status">@(artikel.Status == "published" ? "gepubliceerd" : "klad")</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<aside class="dashboard-side">
|
||||
<h2 class="section-heading">Sneltoetsen</h2>
|
||||
<ul class="quick-links">
|
||||
<li><a class="btn" href="/artikelen/nieuw">Nieuw artikel</a></li>
|
||||
<li><a class="btn" href="/seizoenen">Seizoenen & drukwerk</a></li>
|
||||
<li><a class="btn" href="/artikelen?status=draft">Kladartikelen publiceren</a></li>
|
||||
</ul>
|
||||
|
||||
<h2 class="section-heading">Laatste edities</h2>
|
||||
<ul class="edition-list">
|
||||
@foreach (var editie in _edities.Take(5))
|
||||
{
|
||||
<li>
|
||||
<a href="/edities/@editie.FileName" class="edition-link">
|
||||
<span class="edition-name">@editie.DisplayName</span>
|
||||
<span class="edition-size">@(editie.SizeBytes / 1024) kB</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string? _editorNaam;
|
||||
private Season? _huidigSeizoen;
|
||||
private List<Article> _recent = new();
|
||||
private List<Article> _seizoenDrafts = new();
|
||||
private List<Article> _seizoenPublished = new();
|
||||
private List<User> _gebruikers = new();
|
||||
private List<Edition> _edities = new();
|
||||
private int _drafts;
|
||||
private int _gepubliceerd;
|
||||
private string? _melding;
|
||||
private string? _fout;
|
||||
private string? _afToken;
|
||||
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_afToken = Af.GetAndStoreTokens(HttpContext).RequestToken;
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
_editorNaam = KrantAuth.EditorName(state.User) ?? "redacteur";
|
||||
_huidigSeizoen = await Seizoenen.GetCurrentAsync();
|
||||
_recent = await Artikelen.GetAllAsync();
|
||||
_gebruikers = await Gebruikers.GetAllAsync();
|
||||
_edities = Latex.ListEditions();
|
||||
_drafts = _recent.Count(a => a.Status == "draft");
|
||||
_gepubliceerd = _recent.Count(a => a.Status == "published");
|
||||
if (_huidigSeizoen is not null)
|
||||
{
|
||||
_seizoenDrafts = _recent.Where(a => a.SeasonId == _huidigSeizoen.Id && a.Status == "draft").ToList();
|
||||
_seizoenPublished = await Artikelen.GetBySeasonAsync(_huidigSeizoen.Id);
|
||||
}
|
||||
LeesMelding();
|
||||
}
|
||||
|
||||
private void LeesMelding()
|
||||
{
|
||||
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(new Uri(Nav.Uri).Query);
|
||||
query.TryGetValue("melding", out var melding);
|
||||
query.TryGetValue("fout", out var fout);
|
||||
_melding = melding;
|
||||
_fout = fout;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@page "/fout"
|
||||
|
||||
<PageTitle>Fout — ProostenKrant</PageTitle>
|
||||
|
||||
<section class="card">
|
||||
<span class="kicker">Persstop</span>
|
||||
<h1 class="headline">Er ging iets mis</h1>
|
||||
<p>De redactie kon deze pagina niet drukken. Probeer het opnieuw of ga terug naar de voorpagina.</p>
|
||||
<div class="form-actions">
|
||||
<a class="btn btn-primary" href="/">Naar de voorpagina</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,169 @@
|
||||
@page "/"
|
||||
@inject ArticleService Artikelen
|
||||
@inject SeasonService Seizoenen
|
||||
@inject LatexService Latex
|
||||
|
||||
<PageTitle>ProostenKrant — Voorpagina</PageTitle>
|
||||
|
||||
<section class="edition-banner">
|
||||
<div class="edition-banner-left">
|
||||
@if (_huidigSeizoen is not null)
|
||||
{
|
||||
<span class="kicker seizoen-@_huidigSeizoen.CssKey">Editie @_huidigSeizoen.Name @DateTime.Now.Year</span>
|
||||
<h2 class="edition-title">In wording</h2>
|
||||
<p class="edition-sub">
|
||||
@if (_drafts.Count == 0)
|
||||
{
|
||||
<text>Er liggen nog geen artikelen klaar voor de komende editie.</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Er liggen <strong>@(_drafts.Count == 1 ? "één artikel" : $"{_drafts.Count} artikelen")</strong> klaar voor de komende editie.</text>
|
||||
}
|
||||
Sluit @Krant.DutchShortDate(_huidigSeizoen.EndDate).
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2 class="edition-title">ProostenKrant</h2>
|
||||
<p class="edition-sub">Een krant van en voor Proosten.</p>
|
||||
}
|
||||
</div>
|
||||
<div class="edition-banner-right">
|
||||
<a class="btn btn-primary" href="/indienen">Lever zelf een artikel in</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (_lead is not null)
|
||||
{
|
||||
<article class="lead-article">
|
||||
<span class="kicker">Voorpagina nieuws</span>
|
||||
<h2 class="lead-headline">@_lead.Title</h2>
|
||||
<p class="byline">
|
||||
Door @_lead.AuthorName
|
||||
@if (_lead.EventDate is not null)
|
||||
{
|
||||
<text>— @Krant.DutchShortDate(_lead.EventDate)</text>
|
||||
}
|
||||
</p>
|
||||
@if (!string.IsNullOrEmpty(_lead.WebImage))
|
||||
{
|
||||
<figure class="lead-figure">
|
||||
<img src="@_lead.WebImage" alt="@_lead.Title" loading="lazy" />
|
||||
</figure>
|
||||
}
|
||||
<p class="lead-text">@Teaser(_lead, 400)</p>
|
||||
</article>
|
||||
}
|
||||
|
||||
@if (_teasers.Count > 0)
|
||||
{
|
||||
<h3 class="section-heading">Ook in deze editie</h3>
|
||||
<div class="teaser-columns">
|
||||
@foreach (var artikel in _teasers)
|
||||
{
|
||||
<article class="teaser">
|
||||
<h4 class="teaser-headline">@artikel.Title</h4>
|
||||
<p class="byline">
|
||||
Door @artikel.AuthorName
|
||||
@if (artikel.SeasonName is not null)
|
||||
{
|
||||
<text>— @artikel.SeasonName</text>
|
||||
}
|
||||
</p>
|
||||
<p class="teaser-text">@Teaser(artikel, 220)</p>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (_lead is null && _gepubliceerd.Count == 0)
|
||||
{
|
||||
<section class="card empty-state">
|
||||
<h2 class="headline">De eerste editie wacht op jou</h2>
|
||||
<p>Er zijn nog geen artikelen ingediend. De ProostenKrant wordt pas gedrukt als de familie schrijft.</p>
|
||||
<p><a class="btn btn-primary" href="/indienen">Schrijf het eerste artikel</a></p>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (_gepubliceerd.Count > 0)
|
||||
{
|
||||
<h3 class="section-heading">Verschenen in de @_huidigSeizoen?.Name-editie</h3>
|
||||
<div class="teaser-columns">
|
||||
@foreach (var artikel in _gepubliceerd.Take(6))
|
||||
{
|
||||
<article class="teaser">
|
||||
<h4 class="teaser-headline"><a href="/artikelen/@artikel.Id">@artikel.Title</a></h4>
|
||||
<p class="byline">
|
||||
Door @artikel.AuthorName
|
||||
@if (artikel.EventDate is not null)
|
||||
{
|
||||
<text>— @Krant.DutchShortDate(artikel.EventDate)</text>
|
||||
}
|
||||
</p>
|
||||
<p class="teaser-text">@Teaser(artikel, 200)</p>
|
||||
<p class="teaser-more"><a href="/artikelen/@artikel.Id">Lees verder →</a></p>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<aside class="editions-strip">
|
||||
<h3 class="section-heading">Verschenen edities</h3>
|
||||
@if (_edities.Count > 0)
|
||||
{
|
||||
<ul class="edition-list">
|
||||
@foreach (var editie in _edities.Take(4))
|
||||
{
|
||||
<li>
|
||||
<a href="/edities/@editie.FileName" class="edition-link">
|
||||
<span class="edition-name">@editie.DisplayName</span>
|
||||
@if (editie.Date is not null)
|
||||
{
|
||||
<span class="edition-date">@Krant.DutchShortDate(editie.Date)</span>
|
||||
}
|
||||
<span class="edition-size">@Kb(editie.SizeBytes) kB — PDF</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<p class="edition-more"><a href="/archief">Bekijk het volledige archief →</a></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted">Er zijn nog geen gedrukte edities. Zodra de redactie de krant genereert, verschijnen hier de PDF’s.</p>
|
||||
}
|
||||
</aside>
|
||||
|
||||
@code {
|
||||
private Season? _huidigSeizoen;
|
||||
private List<Article> _drafts = new();
|
||||
private List<Article> _teasers = new();
|
||||
private List<Article> _gepubliceerd = new();
|
||||
private Article? _lead;
|
||||
private List<Edition> _edities = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_huidigSeizoen = await Seizoenen.GetCurrentAsync();
|
||||
_drafts = await Artikelen.GetAllAsync("draft");
|
||||
_lead = _drafts.FirstOrDefault();
|
||||
_teasers = _drafts.Skip(1).ToList();
|
||||
_gepubliceerd = _huidigSeizoen is not null
|
||||
? await Artikelen.GetBySeasonAsync(_huidigSeizoen.Id)
|
||||
: await Artikelen.GetAllAsync("published");
|
||||
_edities = Latex.ListEditions();
|
||||
}
|
||||
|
||||
private static string Teaser(Article a, int length)
|
||||
{
|
||||
var text = a.Content.Replace("\r", " ").Replace("\n", " ").Trim();
|
||||
if (text.Length <= length)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
var cut = text.LastIndexOf(' ', length);
|
||||
return text[..(cut > 0 ? cut : length)] + "…";
|
||||
}
|
||||
|
||||
private static string Kb(long bytes) => Math.Max(1, bytes / 1024).ToString("N0");
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
@page "/indienen"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inject UserService Gebruikers
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
<PageTitle>Artikel indienen — ProostenKrant</PageTitle>
|
||||
|
||||
<section class="card auth-card">
|
||||
<span class="kicker">Medewerking gevraagd</span>
|
||||
<h1 class="headline">Lever een artikel in</h1>
|
||||
<p class="muted">
|
||||
De ProostenKrant bestaat door bijdragen van de familie. Voer de paszin van de familie in
|
||||
om een artikel te schrijven (of een eerder artikel te bewerken).
|
||||
</p>
|
||||
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<p class="alert alert-error">@_fout</p>
|
||||
}
|
||||
|
||||
<EditForm FormName="paszin" Method="post" Model="Input" OnValidSubmit="Verifieer" class="krant-form">
|
||||
<div class="form-group">
|
||||
<label for="passphrase">Paszin</label>
|
||||
<InputText type="password" @bind-Value="Input.Paszin" id="passphrase" class="form-control"
|
||||
placeholder="De geheime paszin van de familie" autocomplete="off" />
|
||||
<ValidationMessage For="() => Input.Paszin" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Verder naar het formulier</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm(FormName = "paszin")]
|
||||
private PaszinInput Input { get; set; } = new();
|
||||
|
||||
private string? _fout;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Input ??= new();
|
||||
|
||||
// Een ingelogd redactielid heeft de paszin niet nodig: direct naar
|
||||
// het formulier. Daarbij een eventueel achtergebleven bewerk-grant
|
||||
// wissen zodat het formulier met een leeg artikel begint.
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (KrantAuth.IsEditor(state.User) && KrantAuth.Uid(state.User) is int uid)
|
||||
{
|
||||
var gebruiker = await Gebruikers.FindAsync(uid);
|
||||
if (gebruiker is not null)
|
||||
{
|
||||
await HttpContext.SignInAsync(KrantAuth.Scheme, KrantAuth.EditorPrincipal(gebruiker));
|
||||
}
|
||||
Nav.NavigateTo("/indienen/formulier", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PaszinInput
|
||||
{
|
||||
[Required(ErrorMessage = "Voer de paszin in.")]
|
||||
public string Paszin { get; set; } = "";
|
||||
}
|
||||
|
||||
private async Task Verifieer()
|
||||
{
|
||||
if (Input.Paszin != Krant.UniversalPassword)
|
||||
{
|
||||
_fout = "Onjuiste paszin. Probeer het opnieuw.";
|
||||
return;
|
||||
}
|
||||
var principal = KrantAuth.PassphrasePrincipal();
|
||||
await HttpContext.SignInAsync(KrantAuth.Scheme, principal);
|
||||
Nav.NavigateTo("/indienen/formulier", forceLoad: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
@page "/indienen/bevestiging"
|
||||
@inject ArticleService Artikelen
|
||||
|
||||
<PageTitle>Bevestiging — ProostenKrant</PageTitle>
|
||||
|
||||
<section class="card">
|
||||
<span class="kicker">Bedankt!</span>
|
||||
<h1 class="headline">@( _artikel is not null ? "Artikel ingediend" : "Bevestiging")</h1>
|
||||
|
||||
@if (_artikel is null)
|
||||
{
|
||||
<p>Er is geen recent ingediend artikel gevonden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Het artikel <strong>@_artikel.Title</strong> is veilig in de brievenbus van de redactie gevallen.</p>
|
||||
<dl class="bevestiging-lijst">
|
||||
<dt>Schrijver</dt>
|
||||
<dd>@_artikel.AuthorName</dd>
|
||||
<dt>Gebeurtenis</dt>
|
||||
<dd>@Krant.DutchShortDate(_artikel.EventDate)</dd>
|
||||
@if (_artikel.SeasonName is not null)
|
||||
{
|
||||
<dt>Editie</dt>
|
||||
<dd>@_artikel.SeasonName</dd>
|
||||
}
|
||||
</dl>
|
||||
<p class="muted">De redactie bekijkt alle inzendingen en publiceert ze in de komende editie.</p>
|
||||
<div class="form-actions">
|
||||
<a class="btn btn-primary" href="/indienen">Nog een artikel indienen</a>
|
||||
<a class="btn" href="/">Naar de voorpagina</a>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromQuery(Name = "artikel")]
|
||||
public int? ArtikelId { get; set; }
|
||||
|
||||
private Article? _artikel;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (ArtikelId is int id)
|
||||
{
|
||||
_artikel = await Artikelen.FindAsync(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
@page "/indienen/formulier"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inject ArticleService Artikelen
|
||||
@inject SeasonService Seizoenen
|
||||
@inject UserService Gebruikers
|
||||
@inject UploadService Uploads
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
<PageTitle>@(_bewerkArtikel is not null ? "Artikel bewerken" : "Artikel indienen") — ProostenKrant</PageTitle>
|
||||
|
||||
<section class="card">
|
||||
<span class="kicker">@(_bewerkArtikel is not null ? "Bewerken" : "Medewerking gevraagd")</span>
|
||||
<h1 class="headline">@( _bewerkArtikel is not null ? "Artikel bewerken" : "Nieuw artikel indienen")</h1>
|
||||
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<p class="alert alert-error">@_fout</p>
|
||||
}
|
||||
|
||||
<EditForm FormName="indienen" Method="post" Model="Input" OnValidSubmit="Opslaan"
|
||||
class="krant-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="title">Titel</label>
|
||||
<InputText @bind-Value="Input.Title" id="title" class="form-control" placeholder="De kop boven je artikel" />
|
||||
<ValidationMessage For="() => Input.Title" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="content">Tekst</label>
|
||||
<InputTextArea @bind-Value="Input.Content" id="content" rows="10" class="form-control"
|
||||
placeholder="Schrijf hier de inhoud van je artikel…" />
|
||||
<ValidationMessage For="() => Input.Content" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="writer">Schrijver</label>
|
||||
<InputSelect @bind-Value="Input.WriterId" id="writer" class="form-control">
|
||||
<option value="0">— Kies schrijver —</option>
|
||||
@foreach (var gebruiker in _gebruikers)
|
||||
{
|
||||
<option value="@gebruiker.Id">@gebruiker.DisplayName</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="() => Input.WriterId" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="eventdate">Datum van de gebeurtenis</label>
|
||||
<InputDate @bind-Value="Input.EventDate" id="eventdate" class="form-control" />
|
||||
<ValidationMessage For="() => Input.EventDate" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Deelnemers</label>
|
||||
<p class="help-text">Selecteer alle personages die bij dit verhaal betrokken zijn.</p>
|
||||
<div class="checkbox-group">
|
||||
@foreach (var gebruiker in _gebruikers)
|
||||
{
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="Participants" value="@gebruiker.Id"
|
||||
checked="@(_geselecteerd.Contains(gebruiker.Id))" />
|
||||
<span>@gebruiker.DisplayName</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
@if (_participantsFout is not null)
|
||||
{
|
||||
<span class="error-message">@_participantsFout</span>
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="image">Afbeelding</label>
|
||||
<input type="file" name="Afbeelding" id="image" class="form-control" accept="image/*" />
|
||||
<span class="help-text">
|
||||
JPG, PNG, GIF of WEBP, maximaal 25 MB.
|
||||
@if (_bewerkArtikel is not null && !string.IsNullOrEmpty(_bewerkArtikel.WebImage))
|
||||
{
|
||||
<text>Laat leeg om de huidige afbeelding te houden.</text>
|
||||
}
|
||||
</span>
|
||||
@if (_afbeeldingFout is not null)
|
||||
{
|
||||
<span class="error-message">@_afbeeldingFout</span>
|
||||
}
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@(_bewerkArtikel is not null ? "Wijzigingen opslaan" : "Artikel indienen")
|
||||
</button>
|
||||
<a class="btn" href="/">Annuleren</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm(FormName = "indienen")]
|
||||
private IndienInput Input { get; set; } = new();
|
||||
|
||||
private List<User> _gebruikers = new();
|
||||
private List<int> _geselecteerd = new();
|
||||
private Article? _bewerkArtikel;
|
||||
private string? _fout;
|
||||
private string? _participantsFout;
|
||||
private string? _afbeeldingFout;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Input ??= new();
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var isEditor = KrantAuth.IsEditor(state.User);
|
||||
var heeftPaszin = KrantAuth.HasValidPassphrase(state.User);
|
||||
var bewerkId = KrantAuth.EditArticleId(state.User);
|
||||
|
||||
if (!isEditor && !heeftPaszin)
|
||||
{
|
||||
Nav.NavigateTo("/indienen", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
|
||||
_gebruikers = await Gebruikers.GetAllAsync();
|
||||
Input.EventDate ??= DateTime.Today;
|
||||
|
||||
if (bewerkId is int id)
|
||||
{
|
||||
_bewerkArtikel = await Artikelen.FindAsync(id);
|
||||
if (_bewerkArtikel is null)
|
||||
{
|
||||
Nav.NavigateTo("/indienen/bewerken/lijst", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
// Alleen bij een GET-request het formulier vullen; bij een POST
|
||||
// zijn de waarden al door de formulierbinding gezet.
|
||||
if (string.Equals(HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Input.Title = _bewerkArtikel.Title;
|
||||
Input.Content = _bewerkArtikel.Content;
|
||||
Input.WriterId = _bewerkArtikel.AuthorId;
|
||||
Input.EventDate = _bewerkArtikel.EventDate;
|
||||
_geselecteerd = _bewerkArtikel.ParticipantIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Opslaan()
|
||||
{
|
||||
Input ??= new();
|
||||
var form = HttpContext.Request.Form;
|
||||
_geselecteerd = form["Participants"].SelectMany(v => (v ?? "").Split(','))
|
||||
.Select(s => int.TryParse(s, out var id) ? id : 0).Where(id => id > 0).ToList();
|
||||
|
||||
if (Input.WriterId <= 0)
|
||||
{
|
||||
_fout = "Kies een schrijver.";
|
||||
return;
|
||||
}
|
||||
if (!_geselecteerd.Contains(Input.WriterId))
|
||||
{
|
||||
_geselecteerd.Insert(0, Input.WriterId);
|
||||
}
|
||||
if (_geselecteerd.Count == 0)
|
||||
{
|
||||
_participantsFout = "Selecteer minstens één deelnemer.";
|
||||
return;
|
||||
}
|
||||
|
||||
string? webPad = _bewerkArtikel?.ArticleImage;
|
||||
var bestand = HttpContext.Request.Form.Files["Afbeelding"];
|
||||
if (bestand is { Length: > 0 } || _bewerkArtikel is null)
|
||||
{
|
||||
var (nieuwPad, fout) = await Uploads.SaveArticleImageAsync(bestand);
|
||||
if (fout is not null)
|
||||
{
|
||||
_afbeeldingFout = fout;
|
||||
return;
|
||||
}
|
||||
webPad = nieuwPad ?? webPad;
|
||||
}
|
||||
|
||||
var season = await Seizoenen.GetByDateAsync(Input.EventDate ?? DateTime.Today);
|
||||
|
||||
if (_bewerkArtikel is not null)
|
||||
{
|
||||
_bewerkArtikel.Title = Input.Title;
|
||||
_bewerkArtikel.Content = Input.Content;
|
||||
_bewerkArtikel.AuthorId = Input.WriterId;
|
||||
_bewerkArtikel.Participants = string.Join(",", _geselecteerd);
|
||||
_bewerkArtikel.EventDate = Input.EventDate;
|
||||
_bewerkArtikel.SeasonId = season?.Id ?? _bewerkArtikel.SeasonId;
|
||||
_bewerkArtikel.ArticleImage = webPad;
|
||||
await Artikelen.UpdateAsync(_bewerkArtikel);
|
||||
|
||||
var artikelId = _bewerkArtikel.Id;
|
||||
await RondAf(artikelId);
|
||||
}
|
||||
else
|
||||
{
|
||||
var artikel = new Article
|
||||
{
|
||||
Title = Input.Title,
|
||||
Content = Input.Content,
|
||||
AuthorId = Input.WriterId,
|
||||
Participants = string.Join(",", _geselecteerd),
|
||||
EventDate = Input.EventDate,
|
||||
SeasonId = season?.Id,
|
||||
ArticleImage = webPad,
|
||||
Status = "draft",
|
||||
};
|
||||
var nieuwId = await Artikelen.CreateAsync(artikel);
|
||||
await RondAf(nieuwId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sessie afronden na het opslaan. Een ingelogd redactielid blijft
|
||||
/// ingelogd (alleen de bewerk-grant wordt gewist); een paszin-sessie
|
||||
/// wordt zoals voorheen afgesloten.
|
||||
/// </summary>
|
||||
private async Task RondAf(int artikelId)
|
||||
{
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (KrantAuth.Uid(state.User) is int uid)
|
||||
{
|
||||
var gebruiker = await Gebruikers.FindAsync(uid);
|
||||
if (gebruiker is not null)
|
||||
{
|
||||
await HttpContext.SignInAsync(KrantAuth.Scheme, KrantAuth.EditorPrincipal(gebruiker));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await HttpContext.SignOutAsync(KrantAuth.Scheme);
|
||||
}
|
||||
Nav.NavigateTo($"/indienen/bevestiging?artikel={artikelId}", forceLoad: true);
|
||||
}
|
||||
|
||||
private sealed class IndienInput
|
||||
{
|
||||
[Required(ErrorMessage = "Geef het artikel een titel (minstens 5 tekens).")]
|
||||
[MinLength(5, ErrorMessage = "De titel moet minstens 5 tekens bevatten.")]
|
||||
[MaxLength(255)]
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Schrijf de tekst van het artikel (minstens 10 tekens).")]
|
||||
[MinLength(10, ErrorMessage = "De tekst moet minstens 10 tekens bevatten.")]
|
||||
public string Content { get; set; } = "";
|
||||
|
||||
public int WriterId { get; set; }
|
||||
|
||||
public DateTime? EventDate { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@page "/login"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inject UserService Gebruikers
|
||||
|
||||
<PageTitle>Inloggen — ProostenKrant</PageTitle>
|
||||
|
||||
<section class="card auth-card">
|
||||
<h1 class="headline">Inloggen redactie</h1>
|
||||
<p class="muted">Kies je naam en voer het familiewachtwoord in.</p>
|
||||
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<p class="alert alert-error">@_fout</p>
|
||||
}
|
||||
|
||||
<EditForm FormName="login" Method="post" Model="Input" OnValidSubmit="LoginUser" class="krant-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Familielid</label>
|
||||
<InputSelect @bind-Value="Input.Username" id="username" class="form-control">
|
||||
<option value="">— Kies je naam —</option>
|
||||
@foreach (var gebruiker in _gebruikers)
|
||||
{
|
||||
<option value="@gebruiker.Username">@gebruiker.DisplayName</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="() => Input.Username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Wachtwoord</label>
|
||||
<InputText type="password" @bind-Value="Input.Password" id="password" class="form-control" autocomplete="current-password" />
|
||||
<ValidationMessage For="() => Input.Password" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Inloggen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm(FormName = "login")]
|
||||
private LoginInput Input { get; set; } = new();
|
||||
|
||||
private List<User> _gebruikers = new();
|
||||
private string? _fout;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Input ??= new();
|
||||
_gebruikers = await Gebruikers.GetAllAsync();
|
||||
}
|
||||
|
||||
private async Task LoginUser()
|
||||
{
|
||||
var gebruiker = _gebruikers.FirstOrDefault(g =>
|
||||
string.Equals(g.Username, Input.Username, StringComparison.OrdinalIgnoreCase));
|
||||
if (gebruiker is null || Input.Password != Krant.UniversalPassword)
|
||||
{
|
||||
_fout = "Ongeldige gebruikersnaam of wachtwoord.";
|
||||
return;
|
||||
}
|
||||
|
||||
var principal = KrantAuth.EditorPrincipal(gebruiker);
|
||||
await HttpContext.SignInAsync(KrantAuth.Scheme, principal);
|
||||
Nav.NavigateTo("/dashboard", forceLoad: true);
|
||||
}
|
||||
|
||||
private sealed class LoginInput
|
||||
{
|
||||
[Required(ErrorMessage = "Kies je naam.")]
|
||||
public string Username { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "Voer het wachtwoord in.")]
|
||||
public string Password { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
@page "/seizoenen/{Id:int}"
|
||||
@attribute [Authorize(Policy = "Editor")]
|
||||
@inject SeasonService Seizoenen
|
||||
@inject ArticleService Artikelen
|
||||
@inject LatexService Latex
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Af
|
||||
|
||||
<PageTitle>@(_seizoen?.Name ?? "Seizoen") — ProostenKrant</PageTitle>
|
||||
|
||||
@if (_seizoen is null)
|
||||
{
|
||||
<section class="card">
|
||||
<h1 class="headline">Seizoen niet gevonden</h1>
|
||||
<p><a href="/seizoenen">Naar het seizoensoverzicht</a>.</p>
|
||||
</section>
|
||||
return;
|
||||
}
|
||||
|
||||
<section class="card">
|
||||
<span class="kicker seizoen-@_seizoen.CssKey">Editie @_seizoen.Name @_seizoen.StartDate.Year</span>
|
||||
<h1 class="headline">@_seizoen.Name @(_seizoen.StartDate.Year)</h1>
|
||||
<p class="muted">
|
||||
@Krant.DutchShortDate(_seizoen.StartDate) — @Krant.DutchShortDate(_seizoen.EndDate).
|
||||
@_artikelen.Count gepubliceerde artikelen in deze editie.
|
||||
</p>
|
||||
<div class="form-actions">
|
||||
<form method="post" action="/api/genereer-pdf" class="inline-form">
|
||||
<input type="hidden" name="SeizoenId" value="@Id" />
|
||||
<input type="hidden" name="__RequestVerificationToken" value="@_afToken" />
|
||||
<button type="submit" class="btn btn-primary">Druk de @_seizoen.Name-editie</button>
|
||||
</form>
|
||||
<a class="btn" href="/seizoenen">Terug</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<p class="alert alert-error">@_fout</p>
|
||||
}
|
||||
@if (_melding is not null)
|
||||
{
|
||||
<p class="alert alert-success">
|
||||
@_melding
|
||||
@if (_nieuwPdf is not null)
|
||||
{
|
||||
<text> <a href="/edities/@_nieuwPdf">Download →</a></text>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
|
||||
<h2 class="section-heading">Artikelen in deze editie</h2>
|
||||
@if (_artikelen.Count == 0)
|
||||
{
|
||||
<p class="muted">Er zijn nog geen gepubliceerde artikelen in deze editie.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="krant-tabel">
|
||||
<thead>
|
||||
<tr><th>Titel</th><th>Schrijver</th><th>Gebeurtenis</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var artikel in _artikelen)
|
||||
{
|
||||
<tr>
|
||||
<td><a href="/artikelen/@artikel.Id">@artikel.Title</a></td>
|
||||
<td>@artikel.AuthorName</td>
|
||||
<td>@Krant.DutchShortDate(artikel.EventDate)</td>
|
||||
<td><a class="btn" href="/artikelen/@artikel.Id">Openen</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
[SupplyParameterFromForm(FormName = "genereer-detail")]
|
||||
private ActieInput Input { get; set; } = new();
|
||||
|
||||
private sealed class ActieInput { }
|
||||
private Season? _seizoen;
|
||||
private List<Article> _artikelen = new();
|
||||
private string? _melding;
|
||||
private string? _fout;
|
||||
private string? _nieuwPdf;
|
||||
private string? _afToken;
|
||||
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_afToken = Af.GetAndStoreTokens(HttpContext).RequestToken;
|
||||
_seizoen = await Seizoenen.FindAsync(Id);
|
||||
if (_seizoen is not null)
|
||||
{
|
||||
_artikelen = await Artikelen.GetBySeasonAsync(Id);
|
||||
}
|
||||
LeesMelding();
|
||||
}
|
||||
|
||||
private void LeesMelding()
|
||||
{
|
||||
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(new Uri(Nav.Uri).Query);
|
||||
query.TryGetValue("melding", out var melding);
|
||||
query.TryGetValue("fout", out var fout);
|
||||
_melding = melding;
|
||||
_fout = fout;
|
||||
query.TryGetValue("pdf", out var pdf);
|
||||
_nieuwPdf = pdf;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
@page "/seizoenen"
|
||||
@attribute [Authorize(Policy = "Editor")]
|
||||
@inject SeasonService SeizoenSvc
|
||||
@inject ArticleService Artikelen
|
||||
@inject LatexService Latex
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Af
|
||||
|
||||
<PageTitle>Seizoenen — ProostenKrant</PageTitle>
|
||||
|
||||
<h1 class="page-heading">Seizoenen & drukwerk</h1>
|
||||
<p class="page-intro">Per seizoen wordt een editie van de krant gedrukt. Publiceer artikelen en druk daarna de editie.</p>
|
||||
|
||||
@if (_melding is not null)
|
||||
{
|
||||
<p class="alert alert-success">
|
||||
@_melding
|
||||
@if (_nieuwPdf is not null)
|
||||
{
|
||||
<text> <a href="/edities/@_nieuwPdf">Download de krant →</a></text>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
@if (_fout is not null)
|
||||
{
|
||||
<div class="alert alert-error">
|
||||
<p>@_fout</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="season-grid">
|
||||
@foreach (var seizoen in _seizoenInfo)
|
||||
{
|
||||
<section class="season-card seizoen-@seizoen.Season.CssKey">
|
||||
<div class="season-card-head">
|
||||
<h2>@seizoen.Season.Name @seizoen.Season.StartDate.Year</h2>
|
||||
@if (seizoen.IsHuidig)
|
||||
{
|
||||
<span class="tag">lopende editie</span>
|
||||
}
|
||||
</div>
|
||||
<p class="muted">
|
||||
@Krant.DutchShortDate(seizoen.Season.StartDate) — @Krant.DutchShortDate(seizoen.Season.EndDate)
|
||||
</p>
|
||||
<ul class="season-stats">
|
||||
<li><strong>@seizoen.Gepubliceerd</strong> gepubliceerd</li>
|
||||
<li><strong>@seizoen.Klad</strong> klad</li>
|
||||
</ul>
|
||||
<div class="form-actions">
|
||||
<form method="post" action="/api/genereer-pdf" class="inline-form">
|
||||
<input type="hidden" name="SeizoenId" value="@seizoen.Season.Id" />
|
||||
<input type="hidden" name="__RequestVerificationToken" value="@_afToken" />
|
||||
<button type="submit" class="btn btn-primary">Druk deze editie</button>
|
||||
</form>
|
||||
<a class="btn" href="/seizoenen/@seizoen.Season.Id">Artikelen bekijken</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h2 class="section-heading">Gedrukte edities</h2>
|
||||
@if (_edities.Count > 0)
|
||||
{
|
||||
<table class="krant-tabel">
|
||||
<thead>
|
||||
<tr><th>Editie</th><th>Datum</th><th>Grootte</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var editie in _edities)
|
||||
{
|
||||
<tr>
|
||||
<td>@editie.DisplayName</td>
|
||||
<td>@Krant.DutchShortDate(editie.Date)</td>
|
||||
<td>@(editie.SizeBytes / 1024) kB</td>
|
||||
<td><a class="btn" href="/edities/@editie.FileName">Download PDF</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted">Er werd nog geen editie gedrukt.</p>
|
||||
}
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromForm(FormName = "genereer-seizoen")]
|
||||
private GenereerInput Input { get; set; } = new();
|
||||
|
||||
private sealed class GenereerInput
|
||||
{
|
||||
public int SeasonId { get; set; }
|
||||
}
|
||||
private List<(Season Season, int Gepubliceerd, int Klad, bool IsHuidig)> _seizoenInfo = new();
|
||||
private List<Edition> _edities = new();
|
||||
private string? _melding;
|
||||
private string? _fout;
|
||||
private string? _nieuwPdf;
|
||||
|
||||
private string? _afToken;
|
||||
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_afToken = Af.GetAndStoreTokens(HttpContext).RequestToken;
|
||||
var seasons = await SeizoenSvc.GetAllAsync();
|
||||
var alle = await Artikelen.GetAllAsync();
|
||||
_seizoenInfo = seasons.Select(s =>
|
||||
{
|
||||
var vanS = alle.Where(a => a.SeasonId == s.Id).ToList();
|
||||
return (s, vanS.Count(a => a.Status == "published"), vanS.Count(a => a.Status == "draft"),
|
||||
s.StartDate <= DateTime.Now && s.EndDate >= DateTime.Now);
|
||||
}).ToList();
|
||||
_edities = Latex.ListEditions();
|
||||
LeesMelding();
|
||||
}
|
||||
|
||||
private void LeesMelding()
|
||||
{
|
||||
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(new Uri(Nav.Uri).Query);
|
||||
query.TryGetValue("melding", out var melding);
|
||||
query.TryGetValue("fout", out var fout);
|
||||
_melding = melding;
|
||||
_fout = fout;
|
||||
query.TryGetValue("pdf", out var pdf);
|
||||
_nieuwPdf = pdf;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@page "/uitloggen"
|
||||
|
||||
<PageTitle>Uitloggen — ProostenKrant</PageTitle>
|
||||
|
||||
<section class="card auth-card">
|
||||
<h1 class="headline">Uitloggen</h1>
|
||||
<p class="muted">Bevestig dat je wilt uitloggen bij de redactie.</p>
|
||||
<EditForm FormName="uitloggen" Method="post" Model="Input" OnValidSubmit="UitloggenUitvoeren" class="krant-form">
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Uitloggen</button>
|
||||
<a class="btn" href="/">Annuleren</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm(FormName = "uitloggen")]
|
||||
private ActieInput Input { get; set; } = new();
|
||||
|
||||
private sealed class ActieInput { }
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
Input ??= new();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task UitloggenUitvoeren()
|
||||
{
|
||||
await HttpContext.SignOutAsync(KrantAuth.Scheme);
|
||||
Nav.NavigateTo("/", forceLoad: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1, .masthead-title" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Pagina niet gevonden</PageTitle>
|
||||
<LayoutView Layout="typeof(Layout.MainLayout)">
|
||||
<section class="card">
|
||||
<h2 class="headline">404 — Pagina niet gevonden</h2>
|
||||
<p>Deze pagina bestaat niet (meer). Keer terug naar de <a href="/">voorpagina</a>.</p>
|
||||
</section>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -0,0 +1,17 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using ProostenKrant
|
||||
@using ProostenKrant.Components
|
||||
@using ProostenKrant.Components.Layout
|
||||
@using ProostenKrant.Models
|
||||
@using ProostenKrant.Services
|
||||
@inject KrantInfo Krant
|
||||
@inject NavigationManager Nav
|
||||