8 Commits
Author SHA1 Message Date
douwe 6e634a960b Changed output port. 2026-10-09 12:32:48 +00:00
douwe 19ec4b9520 new herfst edition. 2026-10-09 14:30:06 +02:00
douwe fa81b64558 Persist login sessions, seed default users, and let editors skip the paszin
- compose: keep DataProtection keys in a named volume so redeploying no
  longer logs everybody out
- seed default family members at startup (init.sql only runs on a fresh
  MySQL volume, existing databases never got new members like Mattis)
- editors no longer need the paszin to submit an article or edit their
  own article; the editor session now survives submitting/editing
  (previously the paszin flow replaced or signed out the editor cookie)
2026-10-09 14:21:51 +02:00
douwe a2a9a773f4 Cleaned up the repo and fixed some bugs. 2026-10-09 13:53:59 +02:00
douwe 72dba8bcfa Track deploy/mysql/init.sql so fresh deployments initialize the database
The blanket *.sql ignore rule (meant for database dumps) also excluded
deploy/mysql/init.sql, so it was never committed. On a fresh clone the
compose bind mount resolves to a missing file, MySQL skips schema
initialization, and every database-backed page throws.
2026-10-09 13:01:10 +02:00
douwe 256a81a770 Updated the proostekrant to work properly again. 2026-10-09 10:33:21 +02:00
Douwe Ravers 56d143b74b Removal captions images 2023-12-26 14:31:53 +01:00
Douwe Ravers fa61d9f5ae Merge pull request #1 from DouweRavers/V2
Automated the google forms to pdf generation process. Using pdflatex …
2023-01-02 10:43:23 +01:00
140 changed files with 5371 additions and 2275 deletions
+14
View File
@@ -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
+16
View File
@@ -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
+56
View File
@@ -0,0 +1,56 @@
# Dependencies
/vendor
composer.lock
# IDE
.idea
.vscode
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Docker
.docker
# Environment
.env
.env.local
.env.*.local
# Logs
*.log
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
Binary file not shown.
-495
View File
@@ -1,495 +0,0 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.12.23) 23 DEC 2022 20:23
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**../Latex/Main.tex
(../Latex/Main.tex
LaTeX2e <2022-11-01> patch level 1
L3 programming layer <2022-12-17>
(c:/texlive/2022/texmf-dist/tex/latex/base/article.cls
Document Class: article 2022/07/02 v1.4n Standard LaTeX document class
(c:/texlive/2022/texmf-dist/tex/latex/base/size10.clo
File: size10.clo 2022/07/02 v1.4n Standard LaTeX file (size option)
)
\c@part=\count185
\c@section=\count186
\c@subsection=\count187
\c@subsubsection=\count188
\c@paragraph=\count189
\c@subparagraph=\count190
\c@figure=\count191
\c@table=\count192
\abovecaptionskip=\skip48
\belowcaptionskip=\skip49
\bibindent=\dimen140
)
(c:/texlive/2022/texmf-dist/tex/latex/base/inputenc.sty
Package: inputenc 2021/02/14 v1.3d Input encoding file
\inpenc@prehook=\toks16
\inpenc@posthook=\toks17
)
(c:/texlive/2022/texmf-dist/tex/latex/base/fontenc.sty
Package: fontenc 2021/04/29 v2.0v Standard LaTeX package
)
(c:/texlive/2022/texmf-dist/tex/latex/microtype/microtype.sty
Package: microtype 2022/06/23 v3.0f Micro-typographical refinements (RS)
(c:/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2022/05/29 v1.15 key=value parser (DPC)
\KV@toks@=\toks18
)
(c:/texlive/2022/texmf-dist/tex/latex/etoolbox/etoolbox.sty
Package: etoolbox 2020/10/05 v2.5k e-TeX tools for LaTeX (JAW)
\etb@tempcnta=\count193
)
\MT@toks=\toks19
\MT@tempbox=\box51
\MT@count=\count194
LaTeX Info: Redefining \noprotrusionifhmode on input line 1045.
LaTeX Info: Redefining \leftprotrusion on input line 1046.
LaTeX Info: Redefining \rightprotrusion on input line 1056.
LaTeX Info: Redefining \textls on input line 1234.
\MT@outer@kern=\dimen141
LaTeX Info: Redefining \textmicrotypecontext on input line 1858.
\MT@listname@count=\count195
(c:/texlive/2022/texmf-dist/tex/latex/microtype/microtype-pdftex.def
File: microtype-pdftex.def 2022/06/23 v3.0f Definitions specific to pdftex (RS)
LaTeX Info: Redefining \lsstyle on input line 900.
LaTeX Info: Redefining \lslig on input line 900.
\MT@outer@space=\skip50
)
Package microtype Info: Loading configuration file microtype.cfg.
(c:/texlive/2022/texmf-dist/tex/latex/microtype/microtype.cfg
File: microtype.cfg 2022/06/23 v3.0f microtype main configuration file (RS)
))
(c:/texlive/2022/texmf-dist/tex/latex/newspaper/newspaper.sty
Package: newspaper
(c:/texlive/2022/texmf-dist/tex/latex/yfonts/yfonts.sty
Package: yfonts 2019/04/04 v1.4 (WaS&NP)
Now handling font encoding LY ...
... no UTF-8 mapping file for font encoding LY
Now handling font encoding LYG ...
... no UTF-8 mapping file for font encoding LYG
LaTeX Info: Composite with undeclared \" in encoding LYG on input line 58.
)
\c@volume=\count196
\c@issue=\count197
)
(c:/texlive/2022/texmf-dist/tex/generic/babel/babel.sty
Package: babel 2022/11/30 3.83 The Babel package
\babel@savecnt=\count198
\U@D=\dimen142
\l@unhyphenated=\language87
(c:/texlive/2022/texmf-dist/tex/generic/babel/txtbabel.def)
\bbl@readstream=\read2
\bbl@dirlevel=\count199
(c:/texlive/2022/texmf-dist/tex/generic/babel-dutch/dutch.ldf
Language: dutch 2021/08/29 v3.8l Dutch support from the babel system
Package babel Info: Making " an active character on input line 127.
))
(c:/texlive/2022/texmf-dist/tex/generic/babel/locale/nl/babel-dutch.tex
Package babel Info: Importing font and identification data for dutch
(babel) from babel-nl.ini. Reported on input line 11.
)
(c:/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
(c:/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
Package: graphics 2022/03/10 v1.4e Standard LaTeX Graphics (DPC,SPQR)
(c:/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
)
(c:/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
)
Package graphics Info: Driver file: pdftex.def on input line 107.
(c:/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
File: pdftex.def 2022/09/22 v1.2b Graphics/color driver for pdftex
))
\Gin@req@height=\dimen143
\Gin@req@width=\dimen144
)
(c:/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
(c:/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
(c:/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
(c:/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.tex
\pgfutil@everybye=\toks20
\pgfutil@tempdima=\dimen145
\pgfutil@tempdimb=\dimen146
(c:/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-common-lists.tex)
) (c:/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def
\pgfutil@abb=\box52
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex
(c:/texlive/2022/texmf-dist/tex/generic/pgf/pgf.revision.tex)
Package: pgfrcs 2021/05/15 v3.1.9a (3.1.9a)
))
Package: pgf 2021/05/15 v3.1.9a (3.1.9a)
(c:/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
(c:/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
(c:/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex
Package: pgfsys 2021/05/15 v3.1.9a (3.1.9a)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
\pgfkeys@pathtoks=\toks21
\pgfkeys@temptoks=\toks22
(c:/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeysfiltered.code.tex
\pgfkeys@tmptoks=\toks23
))
\pgf@x=\dimen147
\pgf@y=\dimen148
\pgf@xa=\dimen149
\pgf@ya=\dimen150
\pgf@xb=\dimen151
\pgf@yb=\dimen152
\pgf@xc=\dimen153
\pgf@yc=\dimen154
\pgf@xd=\dimen155
\pgf@yd=\dimen156
\w@pgf@writea=\write3
\r@pgf@reada=\read3
\c@pgf@counta=\count266
\c@pgf@countb=\count267
\c@pgf@countc=\count268
\c@pgf@countd=\count269
\t@pgf@toka=\toks24
\t@pgf@tokb=\toks25
\t@pgf@tokc=\toks26
\pgf@sys@id@count=\count270
(c:/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg
File: pgf.cfg 2021/05/15 v3.1.9a (3.1.9a)
)
Driver file for pgf: pgfsys-pdftex.def
(c:/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-pdftex.def
File: pgfsys-pdftex.def 2021/05/15 v3.1.9a (3.1.9a)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-pdf.def
File: pgfsys-common-pdf.def 2021/05/15 v3.1.9a (3.1.9a)
)))
(c:/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex
File: pgfsyssoftpath.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfsyssoftpath@smallbuffer@items=\count271
\pgfsyssoftpath@bigbuffer@items=\count272
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex
File: pgfsysprotocol.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)) (c:/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
Package: xcolor 2022/06/12 v2.14 LaTeX color extensions (UK)
(c:/texlive/2022/texmf-dist/tex/latex/graphics-cfg/color.cfg
File: color.cfg 2016/01/02 v1.6 sample color configuration
)
Package xcolor Info: Driver file: pdftex.def on input line 227.
(c:/texlive/2022/texmf-dist/tex/latex/graphics/mathcolor.ltx)
Package xcolor Info: Model `cmy' substituted by `cmy0' on input line 1353.
Package xcolor Info: Model `hsb' substituted by `rgb' on input line 1357.
Package xcolor Info: Model `RGB' extended on input line 1369.
Package xcolor Info: Model `HTML' substituted by `rgb' on input line 1371.
Package xcolor Info: Model `Hsb' substituted by `hsb' on input line 1372.
Package xcolor Info: Model `tHsb' substituted by `hsb' on input line 1373.
Package xcolor Info: Model `HSB' substituted by `hsb' on input line 1374.
Package xcolor Info: Model `Gray' substituted by `gray' on input line 1375.
Package xcolor Info: Model `wave' substituted by `hsb' on input line 1376.
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex
Package: pgfcore 2021/05/15 v3.1.9a (3.1.9a)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex
\pgfmath@dimen=\dimen157
\pgfmath@count=\count273
\pgfmath@box=\box53
\pgfmath@toks=\toks27
\pgfmath@stack@operand=\toks28
\pgfmath@stack@operation=\toks29
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code.tex
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic.code.te
x)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigonometric
.code.tex)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.random.code.t
ex)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.comparison.co
de.tex)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base.code.tex
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round.code.te
x)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc.code.tex
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integerarithm
etics.code.tex)))
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex
\c@pgfmathroundto@lastzeros=\count274
))
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfint.code.tex)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.code.tex
File: pgfcorepoints.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@picminx=\dimen158
\pgf@picmaxx=\dimen159
\pgf@picminy=\dimen160
\pgf@picmaxy=\dimen161
\pgf@pathminx=\dimen162
\pgf@pathmaxx=\dimen163
\pgf@pathminy=\dimen164
\pgf@pathmaxy=\dimen165
\pgf@xx=\dimen166
\pgf@xy=\dimen167
\pgf@yx=\dimen168
\pgf@yy=\dimen169
\pgf@zx=\dimen170
\pgf@zy=\dimen171
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconstruct.cod
e.tex
File: pgfcorepathconstruct.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@path@lastx=\dimen172
\pgf@path@lasty=\dimen173
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage.code.te
x
File: pgfcorepathusage.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@shorten@end@additional=\dimen174
\pgf@shorten@start@additional=\dimen175
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.code.tex
File: pgfcorescopes.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfpic=\box54
\pgf@hbox=\box55
\pgf@layerbox@main=\box56
\pgf@picture@serial@count=\count275
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicstate.code
.tex
File: pgfcoregraphicstate.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgflinewidth=\dimen176
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransformations.c
ode.tex
File: pgfcoretransformations.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@pt@x=\dimen177
\pgf@pt@y=\dimen178
\pgf@pt@temp=\dimen179
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.code.tex
File: pgfcorequick.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.code.tex
File: pgfcoreobjects.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathprocessing.co
de.tex
File: pgfcorepathprocessing.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.code.tex
File: pgfcorearrows.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfarrowsep=\dimen180
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.code.tex
File: pgfcoreshade.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@max=\dimen181
\pgf@sys@shading@range@num=\count276
\pgf@shadingcount=\count277
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.code.tex
File: pgfcoreimage.code.tex 2021/05/15 v3.1.9a (3.1.9a)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal.code.tex
File: pgfcoreexternal.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfexternal@startupbox=\box57
))
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.code.tex
File: pgfcorelayers.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransparency.code
.tex
File: pgfcoretransparency.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns.code.tex
File: pgfcorepatterns.code.tex 2021/05/15 v3.1.9a (3.1.9a)
) (c:/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code.tex
File: pgfcorerdf.code.tex 2021/05/15 v3.1.9a (3.1.9a)
))) (c:/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.code.te
x
File: pgfmoduleshapes.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfnodeparttextbox=\box58
)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code.tex
File: pgfmoduleplot.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)
(c:/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.st
y
Package: pgfcomp-version-0-65 2021/05/15 v3.1.9a (3.1.9a)
\pgf@nodesepstart=\dimen182
\pgf@nodesepend=\dimen183
)
(c:/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.st
y
Package: pgfcomp-version-1-18 2021/05/15 v3.1.9a (3.1.9a)
)) (c:/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
(c:/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
(c:/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex))
(c:/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex))
(c:/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex
Package: pgffor 2021/05/15 v3.1.9a (3.1.9a)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex)
\pgffor@iter=\dimen184
\pgffor@skip=\dimen185
\pgffor@stack=\toks30
\pgffor@toks=\toks31
))
(c:/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex
Package: tikz 2021/05/15 v3.1.9a (3.1.9a)
(c:/texlive/2022/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothandlers.co
de.tex
File: pgflibraryplothandlers.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgf@plot@mark@count=\count278
\pgfplotmarksize=\dimen186
)
\tikz@lastx=\dimen187
\tikz@lasty=\dimen188
\tikz@lastxsaved=\dimen189
\tikz@lastysaved=\dimen190
\tikz@lastmovetox=\dimen191
\tikz@lastmovetoy=\dimen192
\tikzleveldistance=\dimen193
\tikzsiblingdistance=\dimen194
\tikz@figbox=\box59
\tikz@figbox@bg=\box60
\tikz@tempbox=\box61
\tikz@tempbox@bg=\box62
\tikztreelevel=\count279
\tikznumberofchildren=\count280
\tikznumberofcurrentchild=\count281
\tikz@fig@count=\count282
(c:/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.code.tex
File: pgfmodulematrix.code.tex 2021/05/15 v3.1.9a (3.1.9a)
\pgfmatrixcurrentrow=\count283
\pgfmatrixcurrentcolumn=\count284
\pgf@matrix@numberofcolumns=\count285
)
\tikz@expandcount=\count286
(c:/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzli
brarytopaths.code.tex
File: tikzlibrarytopaths.code.tex 2021/05/15 v3.1.9a (3.1.9a)
)))
(c:/texlive/2022/texmf-dist/tex/latex/tools/multicol.sty
Package: multicol 2021/11/30 v1.9d multicolumn formatting (FMi)
\c@tracingmulticols=\count287
\mult@box=\box63
\multicol@leftmargin=\dimen195
\c@unbalance=\count288
\c@collectmore=\count289
\doublecol@number=\count290
\multicoltolerance=\count291
\multicolpretolerance=\count292
\full@width=\dimen196
\page@free=\dimen197
\premulticols=\dimen198
\postmulticols=\dimen199
\multicolsep=\skip51
\multicolbaselineskip=\skip52
\partial@page=\box64
\last@line=\box65
\maxbalancingoverflow=\dimen256
\mult@rightbox=\box66
\mult@grightbox=\box67
\mult@firstbox=\box68
\mult@gfirstbox=\box69
\@tempa=\box70
\@tempa=\box71
\@tempa=\box72
\@tempa=\box73
\@tempa=\box74
\@tempa=\box75
\@tempa=\box76
\@tempa=\box77
\@tempa=\box78
\@tempa=\box79
\@tempa=\box80
\@tempa=\box81
\@tempa=\box82
\@tempa=\box83
\@tempa=\box84
\@tempa=\box85
\@tempa=\box86
\@tempa=\box87
\@tempa=\box88
\@tempa=\box89
\@tempa=\box90
\@tempa=\box91
\@tempa=\box92
\@tempa=\box93
\@tempa=\box94
\@tempa=\box95
\@tempa=\box96
\@tempa=\box97
\@tempa=\box98
\@tempa=\box99
\@tempa=\box100
\@tempa=\box101
\@tempa=\box102
\@tempa=\box103
\@tempa=\box104
\@tempa=\box105
\c@minrows=\count293
\c@columnbadness=\count294
\c@finalcolumnbadness=\count295
\last@try=\dimen257
\multicolovershoot=\dimen258
\multicolundershoot=\dimen259
\mult@nat@firstbox=\box106
\colbreak@box=\box107
\mc@col@check@num=\count296
)
(c:/texlive/2022/texmf-dist/tex/latex/subfiles/subfiles.sty
Package: subfiles 2020/11/14 v2.2 Multi-file projects (package)
(c:/texlive/2022/texmf-dist/tex/latex/import/import.sty
Package: import 2020/04/01 v 6.2
))
! LaTeX Error: File `newspaper-mod.sty' not found.
Type X to quit or <RETURN> to proceed,
or enter new name. (Default extension: sty)
Enter file name:
! Emergency stop.
<read *>
l.35 ^^M
End of file on the terminal!
Here is how much of TeX's memory you used:
14643 strings out of 476093
293783 string characters out of 5795582
1848787 words of memory out of 5000000
34817 multiletter control sequences out of 15000+600000
513050 words of font info for 33 fonts, out of 8000000 for 9000
1141 hyphenation exceptions out of 8191
84i,0n,89p,415b,329s stack positions out of 10000i,1000n,20000p,200000b,200000s
! ==> Fatal error occurred, no output PDF file produced!
Binary file not shown.
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
Binary file not shown.
-105
View File
@@ -1,105 +0,0 @@
\headline{Naar spoed in plaats van naar de film.}
\byline{22-10-2022}{Dien}
\showOneWriter{Images/ProfilePictures/Dien.jpg}{Dien}
\par Zoals afgesproken op het feest van bomma ,zouden we met al diegenen die konden naar de film"Onze natuur" gaan op donderdag 20 oktober. Daar stak de natuur echter een stokje voor in mijn geval. Vanaf die donderdagmorgen voelde ik me niet goed en moest ik overgeven. Als ik dan wat water dronk kwam dit er direct terug uit gepaard met een ondraaglijke pijn. Een doktersbezoek bracht weinig soelaas en hij vermoede dat dit opnieuw een galcrisis was. Tien jaar geleden liet ik naar aanleiding van een galcrisis mijn galblaas verwijderen. Het was afwachten tot er weer aanval aankwam en dan moest ik onmiddellijk naar spoed. Diezelfde avond was het van dat. Weer heel hevige pijn en Michel reed met mij naar spoed. Onze naftbak was leeg en Michel wou eerst nog gaan tanken maar dat was geen optie. Het zag ondertussen zwart voor mijn ogen dus maken dat we bij de spoed waren. Daar aangekomen was de grootste pijn weer gaan liggen en hebben ze alles gechecked. Het bleek niet om galstenen maar gruis te gaan waar niet veel aan gedaan kan worden. We hebben 3 uren op spoed gezeten en werden dan naar huis gestuurd met de boodschap dat het afwachten was tot de volgende aanval zou komen en dan naar een specialist te gaan. Dat is dan ook de natuur zeker.
\showInlineImage{Images/Articles/Naar spoed in plaats van naar de film..jpg}{Naar spoed in plaats van naar de film.}
\headline{Kerstgrotten in Valkenburg}
\byline{15-12-2022}{Diede}
\showTwoWriters{Images/ProfilePictures/Diede.jpg}{Diede}{Images/ProfilePictures/Vincent.jpg}{Vincent}
\par Op woensdag 15 december gingen Vincent en ik naar de grotten in Valkenburg. Daar was een heuse 'kerststad' ingericht. Doorheen heel de stad hingen kerstlichtjes en in de grotten stonden grote lichtgevende standbeelden. Daarnaast waren er ook allemaal kraampjes waar ze verschillende dingen verkochten (waarvan het meeste jammer genoeg plastic rommel was). Om even een pauze te nemen van het slenteren tussen de oude mensjes, die trouwens met hele bussen aankwamen, hebben we een Glühwein gedronken in een barretje in de grot, hmm! 'S avonds zijn we weer teruggereden naar huis waarna we nog een kerstfilm gekeken hebben om de dag in thema af te sluiten.
\showInlineImage{Images/Articles/Kerstgrotten in Valkenburg.jpg}{Kerstgrotten in Valkenburg}
\headline{Reünie in Tübingen}
\byline{27-11-2022}{Diede}
\showOneWriter{Images/ProfilePictures/Diede.jpg}{Diede}
\par Ik ben een weekendje naar Tübingen geweest om af te spreken met enkele vrienden van Erasmus. Na een treinrit van 9 uur lang kwam ik aan op het kot van Lena die studeert in Tübingen. De volgende dag zijn ook Celine (uit Duitsland) en Roosa (uit Finland) aangekomen. Na een verkenning van de stad, zijn we allemaal samen naar de kerstmarkt in Esslingen geweest die een middeleeuws thema had. 'S avonds zijn we nog uitgeweest waarna we de volgende dag rustig aan hebben gedaan. Roosa en ik moesten de volgende ochtend heel vroeg weer weg aangezien onze busrit 12 uur ging duren. Uiteindelijk was het dat allemaal waard en was het superfijn om iedereen weer terug te zien!
\showInlineImage{Images/Articles/Reünie in Tübingen.jpg}{Reünie in Tübingen}
\headline{Mid weekje Oostende}
\byline{14-12-2022}{Michel}
\showOneWriter{Images/ProfilePictures/Michel.jpg}{Michel}
\par Afgelopen (mid)week trokken Dien en ik richting ‘t zeetje om eens even uit te waaien en de dagelijkse rush te ontlopen. Na het nodige slenterwerk langs de kustlijn, de streetart en de kerstrommel (in sommige kringen ook omschreven als ‘versiering ‘) gingen we wat cultuur, eten en drinken verorberen in het voormalig postgebouw van Oostende. Terwijl we genoten van de typische lokale gerechten (vegan Tajine en Pad Thai ;-) hoorde we dat in de foyer van het gebouw een Discjockey den boel serieus in de fik stak… ambiance en muziek uit de 80ies tot heden. Wat bleek, niemand minder dan Jackie Dewaele alias nonkel Bob alias Zaki stond er het beste van zichzelf te geven op de gezegende leeftijd van 77 , voor de jonge lezers: vader van de gebroeders DeWaele, 2manyDJ’s, Soulwax, studio Deewee. Efkes eten werd al snel een stomend dansfeest… Uustend is vree wijs wa!
\showInlineImage{Images/Articles/Mid weekje Oostende.jpg}{Mid weekje Oostende}
\headline{Er groeien vogels aan de takken. }
\byline{12-10-2022}{Chris}
\showOneWriter{Images/ProfilePictures/Chris.jpg}{Chris}
\par Er worden bomen geveld in de tuin. Het lijkt wel of zij dat weten. De vogels, die nog een laatste keer gaan zitten op de takken, die er straks niet meer zullen zijn. Zo schoon.
\showInlineImage{Images/Articles/Er groeien vogels aan de takken. .jpg}{Er groeien vogels aan de takken. }
\headline{Feest bij de familie Ravers}
\byline{18-12-2022}{Dien}
\showOneWriter{Images/ProfilePictures/Dien.jpg}{Dien}
\par Op 18 december had moeke en vake een feest georganiseerd in het Veldekeshof in Turnhout. Niet speciaal voor kerstmis maar zomaar, zodat we mekaar allemaal nog eens zagen. Moeke had een restaurant gezocht waar iedereen zijn gading kon vinden in het aanbod deden. In het Veldekeshof in Turnhout,kon men aan haar wens voldoen. Het werd een gezellig samenzijn en veel bijkletsen. Zo bleek dat de vriend van Zoe (dochter van Kristel) in Schoonbroek in de chiro te zitten. Ik had voor de verandering nog eens voor videe gekozen. Dat was zo ongeveer 3 jaar geleden dat ik dat nog had gegeten. Na mijn eerste hap werd er plots gezegd dat de videe niet meer goed was. Ikzelf proefde niks speciaal maar ik vind dat kip sowieso stinkt. Na 3 jaar wist ik niet meer wat de normale smaak van kip is. De anderen bleven overtuigd dat de videe slecht was. De kok bleef er heel rustig onder hoewel hij zelf niets abnormaals proefde aan de videe. We kregen allemaal iets anders aangeboden,iedereen bleef heel rustig en het probleem was opgelost. Ik had nog getwijfeld om toch voor de vegan menu te gaan. Had ik dat maar gedaan want die thaise curry was overheerlijk volgens de vegans. Volgens mij was dit een teken, daarom heb ik besloten om vanaf januari ook veganist te worden.
\showInlineImage{Images/Articles/Feest bij de familie Ravers.jpg}{Feest bij de familie Ravers}
\headline{Kerstwandeling bijna fataal voor Saartje}
\byline{17-12-2022}{Daan}
\showTwoWriters{Images/ProfilePictures/Daan.jpg}{Daan}{Images/ProfilePictures/Kyala.jpg}{Kyala}
\par Op 17 december organiseerden de grootouders van Kyala een kerstwandeling voor de leden van hun dansclub. Kyala en ik beheerden één van de drie standjes die als tussenstop voor de wandelaars dienden. Het standje bevond zich op de oprit van een bevriend koppel van Kyala's grootouders. Toen de laatste wandelaars weer vertrokken waren en we één en ander binnen wilden zetten, sloot ik de deur achter me. Deze sloot echter niet volledig en ging langzaam terug open. Voordat we er erg in hadden, spurtte Saartje, het hondje dat in het huis woonde, naar de tuin. Wat volgde was een achtervolging op leven en dood; de poort stond namelijk wagenwijd open en kon enkel elektrisch worden gesloten door de eigenaars, die niet thuis waren. Na enkele tevergeefse pogingen om het hondje te vangen, was het zò bang van ons dat het vanzelf terug naar binnen vluchtte. Eind goed, al goed!
\showInlineImage{Images/Articles/Kerstwandeling bijna fataal voor Saartje.jpg}{Kerstwandeling bijna fataal voor Saartje}
\headline{Eenhoornfeestje in Kasterlee}
\byline{18-12-2022}{Mathilde}
\showTwoWriters{Images/ProfilePictures/Mathilde.jpg}{Mathilde}{Images/ProfilePictures/Pieter.jpg}{Pieter}
\par Zondag hebben we de vijfde verjaardag van Lise gevierd, het nichtje van Pieter. Het thema van het feestje was 'eenhoorns'. Er was eenhoorntaart en we mochten allemaal een hoorn opzetten. Wat zagen we er goed uit!
\showInlineImage{Images/Articles/Eenhoornfeestje in Kasterlee.jpg}{Eenhoornfeestje in Kasterlee}
\headline{Overlijdensbericht}
\byline{14-12-2022}{Gunter}
\showOneWriter{Images/ProfilePictures/Gunter.jpg}{Gunter}
\par Na 18 jaar melden wij dat Mario Proost is overleden. Hij woonde in de Kloosterstraat in Retie. Hij was de oudste kat van deze straat. Hij was een vondeling die door Daan en Dries destijds werd gevonden toen hij slechts enkele weken oud was, De laatste jaren sukkelde hij met zijn gezondheid maar bleef trouw op post. Muizen vangen lukte niet meer omdat hij 1 te traag was en 2 zijn tanden verloren had. Hij is de enige die erin slaagde om 18 jaar te overleven langs een drukke baan Hij stak de baan over als geen ander. RIP Mario dit melden zijn vinders Daan Dries, zijn verzorgers Gunter Inge, zijn psycho sociaal ondersteuningsteam Lucca en Kyala
\showInlineImage{Images/Articles/Overlijdensbericht.jpg}{Overlijdensbericht}
\headline{Marathon Kasterlee}
\byline{13-11-2022}{Gunter}
\showOneWriter{Images/ProfilePictures/Gunter.jpg}{Gunter}
\par Na een relatief beperkte training nam Inge deel aan de Marathon van Kasterlee. Dit was haar 65ste Marathon ondertussen wat op zich al een opsteker is. Gunter heeft 1 ronde van de 2 meegelopen, ja idd een halve marathon dus voor de slimme lezers:) Hij verdiende geen podiumplaats, Inge daarentegen wel, zij was de snelste vrouwelijke loopster van Kasterlee, thuis mocht ze ook op het podium niet omwille van de snelste, wel omwille van de beste:) en niet alleen van Kasterlee. Voor de geïnteresseerden haar tijd lag net binnen de 4 uren, opmerkelijk want daar mee was haar tijd sneller dan de vorige Marathon ondanks minder training. Leeftijd heeft op haar geen vat
\showInlineImage{Images/Articles/Marathon Kasterlee.jpg}{Marathon Kasterlee}
\headline{Lichtjeswandeling in Brussel}
\byline{9-12-2022}{Douwe}
\showTwoWriters{Images/ProfilePictures/Douwe.jpg}{Douwe}{Images/ProfilePictures/Pauline.jpg}{Pauline}
\par Op 9 december gingen Douwe en ik naar de lichtjeswandeling in Brussel. Dien en Michel moesten die avond ook in Brussel zijn voor een concert van Intergalactic Lovers, dus we besloten om met z'n vieren naar Brussel te rijden. De rit door het Centrum van Brussel was al een activiteit op zich, maar de rijkunsten van Michel hebben ons veilig tot aan de Ancienne Belgique gebracht. Na een lekkere tapas-maaltijd in een gezellige maar ietwat chaotische Spaanse tent, begonnen we aan onze wandeling. Brussel was prachtig versierd met allerlei lichtjes en er was ook een speciale wandeling voorzien met neon-verlichte geometrische figuren. Een gezellige (maar koude) avond dus, zeker voor herhaling vatbaar!
\showInlineImage{Images/Articles/Lichtjeswandeling in Brussel.jpg}{Lichtjeswandeling in Brussel}
\headline{Cedric de ochtendwandelaar}
\byline{11-12-2022}{Cedric}
\showTwoWriters{Images/ProfilePictures/Cedric.jpg}{Cedric}{Images/ProfilePictures/Annick.jpg}{Annick}
\par Elke zondag doet Cedric met zijn leefgroep een ochtendwandeling. Hij geniet hier zo van
\showInlineImage{Images/Articles/Cedric de ochtendwandelaar.jpg}{Cedric de ochtendwandelaar}
\headline{Op stap met de Singel}
\byline{11-12-2022}{Annick}
\showOneWriter{Images/ProfilePictures/Annick.jpg}{Annick}
\par Op zo 11/12/22 met de Singel naar’t Buitenveld in Retie. ‘t Was er héél druk maar wel gezellig .
\showInlineImage{Images/Articles/Op stap met de Singel.jpg}{Op stap met de Singel}
\headline{ho.. ho.. ho...}
\byline{22-12-2022}{Annick}
\showOneWriter{Images/ProfilePictures/Annick.jpg}{Annick}
\par Heidi en ik waren gezellig aan het winkelen op de markt van Mol tot wij plots de enige en echte Kerstman tegenkwamen. Hij zei voornamelijk "Ho….ho….ho…..", maar ja... wij verstaan ook gewoon niets van dat Fins...
\showInlineImage{Images/Articles/ho.. ho.. ho....jpg}{ho.. ho.. ho...}
\headline{Middeleeuws eten en een IT job: the story of a man who did it all}
\byline{15-12-2022}{Dries}
\showTwoWriters{Images/ProfilePictures/Dries.jpg}{Dries}{Images/ProfilePictures/Lucca.jpg}{Lucca}
\par Dries 26 geworden, lekker gaan eten in herberg het wilde zwijn (zijn favoriet middeleeuws restaurant). Hij heeft van mij en de vrienden een hele dure en goede game chair gekregen waar hij heel freet op is en niemeer uit zijn kamertje komt. Dries heeft ook een nieuwe job in de IT (1ste job id IT), en hij ziet het volledig zitten! Wij plannen ondertussen een weekje Schotland voor tijdens ons verlof, haggis uitproberen en de highland games checken.
\showInlineImage{Images/Articles/Middeleeuws eten en een IT job the story of a man who did it all.jpg}{Middeleeuws eten en een IT job the story of a man who did it all}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 823 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 KiB

-6
View File
@@ -1,6 +0,0 @@
\relax
\providecommand\babel@aux[2]{}
\@nameuse{bbl@beforestart}
\catcode `"\active
\babel@aux{dutch}{}
\gdef \@abspage@last{5}
-1285
View File
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
\usepackage{newtxtext,newtxmath}
\usepackage{etoolbox}
\newcommand{\headlinestyle}{\itshape\huge}
\newcommand{\bylinestyle}{\scshape\Large}
\patchcmd{\headline}{#1}{\headlinestyle #1}{}{}
\patchcmd{\byline}{#1}{\bylinestyle #1}{}{}
\newcommand{\showInlineImage}[2]
{
$
\begin{array}{c}
\resizebox{0.8\columnwidth}{!}{\includegraphics{#1}} \\
$#2$
\end{array}
$
}
\newcommand{\showOneWriter}[2]
{
$
\hfill
\begin{array}{c}
\begin{tikzpicture}
\clip (0,0) circle (0.2\columnwidth) ;
\node[anchor=center] at (0,0) {\includegraphics[width=0.4\columnwidth]{#1}};
\end{tikzpicture} \\
$#2$
\end{array}
\hfill
$
}
\newcommand{\showTwoWriters}[4]
{
$
\begin{array}{cc}
\begin{tikzpicture}
\clip (0,0) circle (0.2\columnwidth) ;
\node[anchor=center] at (0,0) {\includegraphics[width=0.4\columnwidth]{#1}};
\end{tikzpicture}
&
\begin{tikzpicture}
\clip (0,0) circle (0.2\columnwidth) ;
\node[anchor=center] at (0,0) {\includegraphics[width=0.4\columnwidth]{#3}};
\end{tikzpicture} \\
$#2$ & $#4$
\end{array}
$
}
+48
View File
@@ -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)"
-25
View File
@@ -1,25 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33205.214
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proostenkrant.NET", "Proostenkrant.NET\Proostenkrant.NET.csproj", "{F984715F-2FCA-4A9A-B8A9-020D482FD394}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F984715F-2FCA-4A9A-B8A9-020D482FD394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F984715F-2FCA-4A9A-B8A9-020D482FD394}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F984715F-2FCA-4A9A-B8A9-020D482FD394}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F984715F-2FCA-4A9A-B8A9-020D482FD394}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {08AC80BF-ECB6-4A5C-8241-1E5F99C8524F}
EndGlobalSection
EndGlobal
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
-47
View File
@@ -1,47 +0,0 @@
using System.Linq;
namespace Proostenkrant.NET
{
internal static class ArticleGenerator
{
internal static string GenerateArticle(string[][] rows)
{
var text = string.Empty;
foreach (var row in rows)
{
var writers = row[1].Split(',').Select(w => w.Trim()).ToArray();
var title = row[2];
var date = row[3];
var mainText = row[4];
var imageName = row[5];
text += GenerateHeader(writers[0], title, date);
text += GenerateBody(writers, date, mainText, imageName);
text += "\n\n";
}
return text;
}
static string GenerateHeader(string writer, string title, string date)
{
var text = string.Empty;
text += "\\headline{" + title + "}\r\n";
text += "\\byline{" + date + "}{" + writer + "}\r\n";
return text;
}
static string GenerateBody(string[] writers, string date, string mainText, string imageName)
{
var text = string.Empty;
if (writers.Length == 2)
text += "\\showTwoWriters{Images/ProfilePictures/" + writers[0] + ".jpg}{" + writers[0] + "}{Images/ProfilePictures/" + writers[1] + ".jpg}{" + writers[1] + "}\r\n";
else
text += "\\showOneWriter{Images/ProfilePictures/" + writers[0] + ".jpg}{" + writers[0] + "}\r\n";
text += "\\par " + mainText + "\r\n";
text += "\\showInlineImage{Images/Articles/" + imageName + ".jpg}{" + imageName + "}\r\n";
return text;
}
}
}
-76
View File
@@ -1,76 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
namespace Proostenkrant.NET
{
internal static class GoogleFormsReader
{
const string tsvDataUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRfxfdT8f4MQjXWmZylnXCGl14T0x5mxNw3W1Xd8GrZfPivjcqRGIvmb2YoAf77M00PrGPSJ6Zo-hRs/pub?output=tsv";
const string downloadURL = "https://drive.google.com/uc?export=download&id=FILEID";
public static string[][] GetData()
{
var tsvData = GetTsvData();
var data = ConvertData(tsvData);
var imageNames = LoadImages(data);
return data.Select((v, i) =>
{
v[5] = imageNames[i];
return v;
}).ToArray();
}
static string GetTsvData()
{
var client = new HttpClient();
var downloadTsvTask = client.GetStringAsync(tsvDataUrl);
downloadTsvTask.Wait();
return downloadTsvTask.Result;
}
static string[][] ConvertData(string tsvData)
{
// Split rows
var tsvRows = tsvData.Split('\r').Where(v => v.Length > 0);
// Split values
var rows = tsvRows.Select(r => r.Split('\t').Where(v => v.Length > 0));
// Remove header rows
rows = rows.ToList().GetRange(2, rows.Count() - 2);
return rows.Select(r => r.ToArray()).ToArray();
}
static string[] LoadImages(string[][] data)
{
var names = new List<string>();
foreach (var row in data)
{
var name = Regex.Replace(row[2], string.Concat(Path.GetInvalidFileNameChars()), string.Empty, RegexOptions.Compiled);
name = name.Replace(":", "");
names.Add(name);
var filename = Program.c_articleImagesPath + $"/{name}.jpg";
if (File.Exists(filename)) continue;
var id = row[5].Replace("https://drive.google.com/open?id=", string.Empty);
var url = downloadURL.Replace("FILEID", id);
Console.WriteLine("Downloading images using the browser...");
var process = Process.Start(url);
process.WaitForExit();
Console.Write("Drag image here : ");
var path = Console.ReadLine().Trim('\"');
File.Copy(path, filename);
}
return names.ToArray();
}
}
}
-28
View File
@@ -1,28 +0,0 @@
using System.Diagnostics;
using System.IO;
namespace Proostenkrant.NET
{
internal static class LatexCompiler
{
internal static void Compile(string text)
{
File.WriteAllText(Program.c_articlePath, text);
GeneratePdf();
}
static void GeneratePdf()
{
var processInfo = new ProcessStartInfo("cmd.exe", "/c pdflatex " + Program.c_mainPath);
processInfo.WorkingDirectory = Program.c_latexPath;
processInfo.CreateNoWindow = true;
processInfo.UseShellExecute = true;
var process = Process.Start(processInfo);
process.WaitForExit();
}
}
}
-23
View File
@@ -1,23 +0,0 @@
using System;
namespace Proostenkrant.NET
{
public class Program
{
internal const string c_latexPath = "../Latex";
internal const string c_mainPath = c_latexPath + "/Main.tex";
internal const string c_articlePath = c_latexPath + "/Articles.tex";
internal const string c_imagesPath = c_latexPath + "/Images";
internal const string c_articleImagesPath = c_imagesPath + "/Articles";
static void Main(string[] args)
{
var rows = GoogleFormsReader.GetData();
var text = ArticleGenerator.GenerateArticle(rows);
LatexCompiler.Compile(text);
Console.WriteLine("Done!");
Console.ReadLine();
}
}
}
@@ -1,62 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{F984715F-2FCA-4A9A-B8A9-020D482FD394}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>Proostenkrant.NET</RootNamespace>
<AssemblyName>Proostenkrant.NET</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Build\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>
</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ArticleGenerator.cs" />
<Compile Include="GoogleFormsReader.cs" />
<Compile Include="LatexCompiler.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Connected Services\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -1,36 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Proostenkrant.NET")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Proostenkrant.NET")]
[assembly: AssemblyCopyright("Copyright © 2022")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("f984715f-2fca-4a9a-b8a9-020d482fd394")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
@@ -1,4 +0,0 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
@@ -1 +0,0 @@
36ee40575dbbea1fa5315cbc1554d56b6770af33
@@ -1,8 +0,0 @@
C:\Users\douwe\Projects\Proostenkrant.NET\Build\Proostenkrant.NET.exe.config
C:\Users\douwe\Projects\Proostenkrant.NET\Build\Proostenkrant.NET.exe
C:\Users\douwe\Projects\Proostenkrant.NET\Build\Proostenkrant.NET.pdb
C:\Users\douwe\Projects\Proostenkrant.NET\Proostenkrant.NET\obj\Debug\Proostenkrant.NET.csproj.AssemblyReference.cache
C:\Users\douwe\Projects\Proostenkrant.NET\Proostenkrant.NET\obj\Debug\Proostenkrant.NET.csproj.SuggestedBindingRedirects.cache
C:\Users\douwe\Projects\Proostenkrant.NET\Proostenkrant.NET\obj\Debug\Proostenkrant.NET.csproj.CoreCompileInputs.cache
C:\Users\douwe\Projects\Proostenkrant.NET\Proostenkrant.NET\obj\Debug\Proostenkrant.NET.exe
C:\Users\douwe\Projects\Proostenkrant.NET\Proostenkrant.NET\obj\Debug\Proostenkrant.NET.pdb
Binary file not shown.
Binary file not shown.
@@ -1,4 +0,0 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
+136 -2
View File
@@ -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`.
+60
View File
@@ -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:
+46
View File
@@ -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"]
+91
View File
@@ -0,0 +1,91 @@
-- 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);
-- 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);
+17
View File
@@ -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"]
+9
View File
@@ -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;"]
+18
View File
@@ -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;
}
}
+42
View File
@@ -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"]
+7
View File
@@ -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
Binary file not shown.
@@ -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}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 339 KiB

Before

Width:  |  Height:  |  Size: 53 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

Before

Width:  |  Height:  |  Size: 733 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

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

Before

Width:  |  Height:  |  Size: 71 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+9 -5
View File
@@ -30,20 +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{Articles}
% Articles will be inserted here by the PHP generator
\newpage
\end{multicols}
+119
View File
@@ -0,0 +1,119 @@
% Improved newspaper-mod.sty for ProostenKrant
% Better image handling for different formats and sizes
\newcommand{\headlinestyle}{\itshape\huge}
\newcommand{\bylinestyle}{\scshape\Large}
\patchcmd{\headline}{#1}{\headlinestyle #1}{}{}
\patchcmd{\byline}{#1}{\bylinestyle #1}{}{}
% Improved image commands with better handling of different aspect ratios
% Show inline image with caption (automatically handles landscape and portrait)
\newcommand{\showInlineImage}[2]
{
\\\n \hspace*{0pt}% Ensure proper spacing
\vspace*{10pt}% Add space before image
\begin{center}
\begin{minipage}{0.95\columnwidth}
\centering
% Use max dimensions that fit in column
\setlength{\fboxsep}{5pt}
\framebox{
% Check image dimensions and scale appropriately
\includegraphics[max width=\linewidth, max height=0.6\textheight, keepaspectratio]{#1}
}
\vspace*{5pt}
{\small #2}
\end{minipage}
\end{center}
\vspace*{10pt}% Add space after image
\par
}
% Show single writer with circular profile image
\newcommand{\showOneWriter}[2]
{
\\\n \hfill
\begin{minipage}[t]{0.4\columnwidth}
\centering
\begin{tikzpicture}
% Circular clip for profile image
\clip (0,0) circle (1.3cm);
% Use square aspect ratio and scale to fit
\node[anchor=center] at (0,0) {
\includegraphics[width=2.8cm, height=2.8cm, keepaspectratio]{#1}
};
\end{tikzpicture}
\vspace*{5pt}
{\small #2}
\end{minipage}
\hfill
\\\n}
% Show two writers with circular profile images
\newcommand{\showTwoWriters}[4]
{
\\\n \begin{center}
\begin{minipage}[t]{0.8\columnwidth}
\centering
\begin{tikzpicture}
\clip (0,0) circle (1.1cm);
\node[anchor=west] at (0,0) {
\includegraphics[width=2.4cm, height=2.4cm, keepaspectratio]{#1}
};
\end{tikzpicture}
\hspace*{15pt}
\begin{tikzpicture}
\clip (0,0) circle (1.1cm);
\node[anchor=east] at (0,0) {
\includegraphics[width=2.4cm, height=2.4cm, keepaspectratio]{#3}
};
\end{tikzpicture}
\vspace*{8pt}
{\small #2 \\quad #4}
\end{minipage}
\end{center}
\\\n}
% Alternative commands for different layouts
% For portrait images - maintains aspect ratio
\newcommand{\showPortraitImage}[2]
{
\\\n \begin{minipage}[t]{0.45\columnwidth}
\centering
\begin{tikzpicture}
% For portrait images, use vertical layout
\clip (0,-1.5cm) rectangle (2cm,1.5cm);
\node[anchor=center] at (1cm,0) {
\includegraphics[height=3cm, keepaspectratio]{#1}
};
\end{tikzpicture}
\vspace*{5pt}
{\small #2}
\end{minipage}
\\\n}
% For landscape images - maintains aspect ratio
\newcommand{\showLandscapeImage}[2]
{
\\\n \begin{minipage}[t]{\columnwidth}
\centering
% For landscape images, use full width
\includegraphics[width=\linewidth, height=0.4\textheight, keepaspectratio]{#1}
\vspace*{5pt}
{\small #2}
\end{minipage}
\\\n}
% Flexible image wrapper that automatically handles different orientations
\newcommand{\showFlexibleImage}[2]
{
% First, we need to check the image dimensions
% This requires the epstopdf package and shell-escape
\IfFileExists{#1}{
% Image exists, try to determine orientation
% For now, we'll use a balanced approach that works for both
\showInlineImage{#1}{#2}
}{}
}
+80
View File
@@ -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}
+18
View File
@@ -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">&bdquo;@Krant.Slogan&ldquo;</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>&copy; @DateTime.Now.Year @Krant.AppName &mdash; 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>&mdash; @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 &bdquo;@_artikel.Title&ldquo; 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");
}
}

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