Updated the proostekrant to work properly again.

This commit is contained in:
douwe
2026-10-09 10:33:21 +02:00
parent 56d143b74b
commit 256a81a770
133 changed files with 10491 additions and 837 deletions
+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,97 @@
@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();
}
private async Task Verifieer()
{
if (_artikel is null)
{
return;
}
if (Input.WriterId != _artikel.AuthorId)
{
_fout = "Alleen de schrijver van het artikel kan het bewerken.";
return;
}
await HttpContext.SignInAsync(KrantAuth.Scheme,
KrantAuth.PassphrasePrincipal(editArticleId: _artikel.Id));
Nav.NavigateTo("/indienen/formulier", forceLoad: true);
}
}
@@ -0,0 +1,47 @@
@page "/indienen/bewerken/lijst"
@inject ArticleService Artikelen
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>Kladartikelen — ProostenKrant</PageTitle>
<h1 class="page-heading">Kladartikelen bewerken</h1>
<p class="page-intro">Kies het artikel dat je wilt aanpassen. Alleen de schrijver van het artikel kan het bewerken.</p>
@if (_klad.Count == 0)
{
<p class="muted">Er zijn momenteel geen kladartikelen.</p>
}
else
{
<table class="krant-tabel">
<thead>
<tr><th>Titel</th><th>Schrijver</th><th>Gebeurtenis</th><th></th></tr>
</thead>
<tbody>
@foreach (var artikel in _klad)
{
<tr>
<td>@artikel.Title</td>
<td>@artikel.AuthorName</td>
<td>@Krant.DutchShortDate(artikel.EventDate)</td>
<td><a class="btn" href="/indienen/bewerken/@artikel.Id">Bewerken</a></td>
</tr>
}
</tbody>
</table>
}
@code {
private List<Article> _klad = new();
protected override async Task OnInitializedAsync()
{
var state = await AuthStateProvider.GetAuthenticationStateAsync();
if (!KrantAuth.HasValidPassphrase(state.User) && !KrantAuth.IsEditor(state.User))
{
Nav.NavigateTo("/indienen/bewerken", forceLoad: true);
return;
}
_klad = await Artikelen.GetAllAsync("draft");
}
}
@@ -0,0 +1,60 @@
@page "/indienen/bewerken"
@using System.ComponentModel.DataAnnotations
<PageTitle>Artikel bewerken — ProostenKrant</PageTitle>
<section class="card auth-card">
<span class="kicker">Beveiliging</span>
<h1 class="headline">Artikel bewerken</h1>
<p class="muted">Voer de paszin in om de lijst met kladartikelen te openen.</p>
@if (_fout is not null)
{
<p class="alert alert-error">@_fout</p>
}
<EditForm FormName="paszin-bewerken" Method="post" Model="Input" OnValidSubmit="Verifieer" class="krant-form">
<div class="form-group">
<label for="passphrase">Paszin</label>
<InputText type="password" @bind-Value="Input.Paszin" id="passphrase" class="form-control" autocomplete="off" />
<ValidationMessage For="() => Input.Paszin" />
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Verder</button>
<a class="btn" href="/">Annuleren</a>
</div>
</EditForm>
</section>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "paszin-bewerken")]
private PaszinInput Input { get; set; } = new();
private string? _fout;
protected override Task OnInitializedAsync()
{
Input ??= new();
return Task.CompletedTask;
}
private sealed class PaszinInput
{
[Required(ErrorMessage = "Voer de paszin in.")]
public string Paszin { get; set; } = "";
}
private async Task Verifieer()
{
if (Input.Paszin != Krant.UniversalPassword)
{
_fout = "Onjuiste paszin. Probeer het opnieuw.";
return;
}
await HttpContext.SignInAsync(KrantAuth.Scheme, KrantAuth.PassphrasePrincipal());
Nav.NavigateTo("/indienen/bewerken/lijst", forceLoad: true);
}
}
@@ -0,0 +1,166 @@
@page "/dashboard"
@attribute [Authorize(Policy = "Editor")]
@inject AuthenticationStateProvider AuthStateProvider
@inject ArticleService Artikelen
@inject SeasonService Seizoenen
@inject LatexService Latex
@inject UserService Gebruikers
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Af
<PageTitle>Dashboard — ProostenKrant</PageTitle>
<h1 class="page-heading">Dashboard redactie</h1>
<p class="page-intro">Welkom, @_editorNaam. Overzicht van de krant en sneltoetsen voor de redactie.</p>
@if (_melding is not null)
{
<p class="alert alert-success">@_melding</p>
}
@if (_fout is not null)
{
<div class="alert alert-error">
<p>@_fout</p>
</div>
}
<div class="stats-grid">
<div class="stat-card">
<span class="stat-number">@_drafts</span>
<span class="stat-label">Kladartikelen</span>
<a href="/artikelen?status=draft" class="stat-link">Bekijken &rarr;</a>
</div>
<div class="stat-card">
<span class="stat-number">@_gepubliceerd</span>
<span class="stat-label">Gepubliceerd</span>
<a href="/artikelen?status=published" class="stat-link">Bekijken &rarr;</a>
</div>
<div class="stat-card">
<span class="stat-number">@_gebruikers.Count</span>
<span class="stat-label">Familieleden</span>
</div>
<div class="stat-card">
<span class="stat-number">@_edities.Count</span>
<span class="stat-label">Gedrukte edities</span>
<a href="/archief" class="stat-link">Archief &rarr;</a>
</div>
</div>
<div class="dashboard-columns">
<section class="dashboard-main">
<h2 class="section-heading">Huidige editie</h2>
@if (_huidigSeizoen is not null)
{
<div class="season-card seizoen-@_huidigSeizoen.CssKey">
<h3>@_huidigSeizoen.Name @DateTime.Now.Year</h3>
<p class="muted">
Van @Krant.DutchShortDate(_huidigSeizoen.StartDate) t/m @Krant.DutchShortDate(_huidigSeizoen.EndDate).
@(_seizoenDrafts.Count) klad, @(_seizoenPublished.Count) gepubliceerd.
</p>
<div class="form-actions">
<form method="post" action="/api/genereer-pdf" class="inline-form">
<input type="hidden" name="SeizoenId" value="@_huidigSeizoen.Id" />
<input type="hidden" name="__RequestVerificationToken" value="@_afToken" />
<button type="submit" class="btn btn-primary">Druk de @_huidigSeizoen.Name-editie</button>
</form>
</div>
</div>
}
else
{
<p class="muted">Er is geen lopend seizoen gedefinieerd.</p>
}
<h2 class="section-heading">Laatste artikelen</h2>
<table class="krant-tabel">
<thead>
<tr>
<th>Titel</th>
<th>Schrijver</th>
<th>Seizoen</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach (var artikel in _recent.Take(8))
{
<tr>
<td><a href="/artikelen/@artikel.Id">@artikel.Title</a></td>
<td>@artikel.AuthorName</td>
<td>@(artikel.SeasonName ?? "—")</td>
<td>
<span class="status status-@artikel.Status">@(artikel.Status == "published" ? "gepubliceerd" : "klad")</span>
</td>
</tr>
}
</tbody>
</table>
</section>
<aside class="dashboard-side">
<h2 class="section-heading">Sneltoetsen</h2>
<ul class="quick-links">
<li><a class="btn" href="/artikelen/nieuw">Nieuw artikel</a></li>
<li><a class="btn" href="/seizoenen">Seizoenen &amp; drukwerk</a></li>
<li><a class="btn" href="/artikelen?status=draft">Kladartikelen publiceren</a></li>
</ul>
<h2 class="section-heading">Laatste edities</h2>
<ul class="edition-list">
@foreach (var editie in _edities.Take(5))
{
<li>
<a href="/edities/@editie.FileName" class="edition-link">
<span class="edition-name">@editie.DisplayName</span>
<span class="edition-size">@(editie.SizeBytes / 1024) kB</span>
</a>
</li>
}
</ul>
</aside>
</div>
@code {
private string? _editorNaam;
private Season? _huidigSeizoen;
private List<Article> _recent = new();
private List<Article> _seizoenDrafts = new();
private List<Article> _seizoenPublished = new();
private List<User> _gebruikers = new();
private List<Edition> _edities = new();
private int _drafts;
private int _gepubliceerd;
private string? _melding;
private string? _fout;
private string? _afToken;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
_afToken = Af.GetAndStoreTokens(HttpContext).RequestToken;
var state = await AuthStateProvider.GetAuthenticationStateAsync();
_editorNaam = KrantAuth.EditorName(state.User) ?? "redacteur";
_huidigSeizoen = await Seizoenen.GetCurrentAsync();
_recent = await Artikelen.GetAllAsync();
_gebruikers = await Gebruikers.GetAllAsync();
_edities = Latex.ListEditions();
_drafts = _recent.Count(a => a.Status == "draft");
_gepubliceerd = _recent.Count(a => a.Status == "published");
if (_huidigSeizoen is not null)
{
_seizoenDrafts = _recent.Where(a => a.SeasonId == _huidigSeizoen.Id && a.Status == "draft").ToList();
_seizoenPublished = await Artikelen.GetBySeasonAsync(_huidigSeizoen.Id);
}
LeesMelding();
}
private void LeesMelding()
{
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(new Uri(Nav.Uri).Query);
query.TryGetValue("melding", out var melding);
query.TryGetValue("fout", out var fout);
_melding = melding;
_fout = fout;
}
}
@@ -0,0 +1,12 @@
@page "/fout"
<PageTitle>Fout — ProostenKrant</PageTitle>
<section class="card">
<span class="kicker">Persstop</span>
<h1 class="headline">Er ging iets mis</h1>
<p>De redactie kon deze pagina niet drukken. Probeer het opnieuw of ga terug naar de voorpagina.</p>
<div class="form-actions">
<a class="btn btn-primary" href="/">Naar de voorpagina</a>
</div>
</section>
@@ -0,0 +1,169 @@
@page "/"
@inject ArticleService Artikelen
@inject SeasonService Seizoenen
@inject LatexService Latex
<PageTitle>ProostenKrant — Voorpagina</PageTitle>
<section class="edition-banner">
<div class="edition-banner-left">
@if (_huidigSeizoen is not null)
{
<span class="kicker seizoen-@_huidigSeizoen.CssKey">Editie @_huidigSeizoen.Name @DateTime.Now.Year</span>
<h2 class="edition-title">In wording</h2>
<p class="edition-sub">
@if (_drafts.Count == 0)
{
<text>Er liggen nog geen artikelen klaar voor de komende editie.</text>
}
else
{
<text>Er liggen <strong>@(_drafts.Count == 1 ? "één artikel" : $"{_drafts.Count} artikelen")</strong> klaar voor de komende editie.</text>
}
Sluit @Krant.DutchShortDate(_huidigSeizoen.EndDate).
</p>
}
else
{
<h2 class="edition-title">ProostenKrant</h2>
<p class="edition-sub">Een krant van en voor Proosten.</p>
}
</div>
<div class="edition-banner-right">
<a class="btn btn-primary" href="/indienen">Lever zelf een artikel in</a>
</div>
</section>
@if (_lead is not null)
{
<article class="lead-article">
<span class="kicker">Voorpagina nieuws</span>
<h2 class="lead-headline">@_lead.Title</h2>
<p class="byline">
Door @_lead.AuthorName
@if (_lead.EventDate is not null)
{
<text>&mdash; @Krant.DutchShortDate(_lead.EventDate)</text>
}
</p>
@if (!string.IsNullOrEmpty(_lead.WebImage))
{
<figure class="lead-figure">
<img src="@_lead.WebImage" alt="@_lead.Title" loading="lazy" />
</figure>
}
<p class="lead-text">@Teaser(_lead, 400)</p>
</article>
}
@if (_teasers.Count > 0)
{
<h3 class="section-heading">Ook in deze editie</h3>
<div class="teaser-columns">
@foreach (var artikel in _teasers)
{
<article class="teaser">
<h4 class="teaser-headline">@artikel.Title</h4>
<p class="byline">
Door @artikel.AuthorName
@if (artikel.SeasonName is not null)
{
<text>&mdash; @artikel.SeasonName</text>
}
</p>
<p class="teaser-text">@Teaser(artikel, 220)</p>
</article>
}
</div>
}
else if (_lead is null && _gepubliceerd.Count == 0)
{
<section class="card empty-state">
<h2 class="headline">De eerste editie wacht op jou</h2>
<p>Er zijn nog geen artikelen ingediend. De ProostenKrant wordt pas gedrukt als de familie schrijft.</p>
<p><a class="btn btn-primary" href="/indienen">Schrijf het eerste artikel</a></p>
</section>
}
@if (_gepubliceerd.Count > 0)
{
<h3 class="section-heading">Verschenen in de @_huidigSeizoen?.Name-editie</h3>
<div class="teaser-columns">
@foreach (var artikel in _gepubliceerd.Take(6))
{
<article class="teaser">
<h4 class="teaser-headline"><a href="/artikelen/@artikel.Id">@artikel.Title</a></h4>
<p class="byline">
Door @artikel.AuthorName
@if (artikel.EventDate is not null)
{
<text>&mdash; @Krant.DutchShortDate(artikel.EventDate)</text>
}
</p>
<p class="teaser-text">@Teaser(artikel, 200)</p>
<p class="teaser-more"><a href="/artikelen/@artikel.Id">Lees verder &rarr;</a></p>
</article>
}
</div>
}
<aside class="editions-strip">
<h3 class="section-heading">Verschenen edities</h3>
@if (_edities.Count > 0)
{
<ul class="edition-list">
@foreach (var editie in _edities.Take(4))
{
<li>
<a href="/edities/@editie.FileName" class="edition-link">
<span class="edition-name">@editie.DisplayName</span>
@if (editie.Date is not null)
{
<span class="edition-date">@Krant.DutchShortDate(editie.Date)</span>
}
<span class="edition-size">@Kb(editie.SizeBytes) kB &mdash; PDF</span>
</a>
</li>
}
</ul>
<p class="edition-more"><a href="/archief">Bekijk het volledige archief &rarr;</a></p>
}
else
{
<p class="muted">Er zijn nog geen gedrukte edities. Zodra de redactie de krant genereert, verschijnen hier de PDF&rsquo;s.</p>
}
</aside>
@code {
private Season? _huidigSeizoen;
private List<Article> _drafts = new();
private List<Article> _teasers = new();
private List<Article> _gepubliceerd = new();
private Article? _lead;
private List<Edition> _edities = new();
protected override async Task OnInitializedAsync()
{
_huidigSeizoen = await Seizoenen.GetCurrentAsync();
_drafts = await Artikelen.GetAllAsync("draft");
_lead = _drafts.FirstOrDefault();
_teasers = _drafts.Skip(1).ToList();
_gepubliceerd = _huidigSeizoen is not null
? await Artikelen.GetBySeasonAsync(_huidigSeizoen.Id)
: await Artikelen.GetAllAsync("published");
_edities = Latex.ListEditions();
}
private static string Teaser(Article a, int length)
{
var text = a.Content.Replace("\r", " ").Replace("\n", " ").Trim();
if (text.Length <= length)
{
return text;
}
var cut = text.LastIndexOf(' ', length);
return text[..(cut > 0 ? cut : length)] + "…";
}
private static string Kb(long bytes) => Math.Max(1, bytes / 1024).ToString("N0");
}
@@ -0,0 +1,64 @@
@page "/indienen"
@using System.ComponentModel.DataAnnotations
<PageTitle>Artikel indienen — ProostenKrant</PageTitle>
<section class="card auth-card">
<span class="kicker">Medewerking gevraagd</span>
<h1 class="headline">Lever een artikel in</h1>
<p class="muted">
De ProostenKrant bestaat door bijdragen van de familie. Voer de paszin van de familie in
om een artikel te schrijven (of een eerder artikel te bewerken).
</p>
@if (_fout is not null)
{
<p class="alert alert-error">@_fout</p>
}
<EditForm FormName="paszin" Method="post" Model="Input" OnValidSubmit="Verifieer" class="krant-form">
<div class="form-group">
<label for="passphrase">Paszin</label>
<InputText type="password" @bind-Value="Input.Paszin" id="passphrase" class="form-control"
placeholder="De geheime paszin van de familie" autocomplete="off" />
<ValidationMessage For="() => Input.Paszin" />
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Verder naar het formulier</button>
</div>
</EditForm>
</section>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "paszin")]
private PaszinInput Input { get; set; } = new();
private string? _fout;
protected override Task OnInitializedAsync()
{
Input ??= new();
return Task.CompletedTask;
}
private sealed class PaszinInput
{
[Required(ErrorMessage = "Voer de paszin in.")]
public string Paszin { get; set; } = "";
}
private async Task Verifieer()
{
if (Input.Paszin != Krant.UniversalPassword)
{
_fout = "Onjuiste paszin. Probeer het opnieuw.";
return;
}
var principal = KrantAuth.PassphrasePrincipal();
await HttpContext.SignInAsync(KrantAuth.Scheme, principal);
Nav.NavigateTo("/indienen/formulier", forceLoad: true);
}
}
@@ -0,0 +1,49 @@
@page "/indienen/bevestiging"
@inject ArticleService Artikelen
<PageTitle>Bevestiging — ProostenKrant</PageTitle>
<section class="card">
<span class="kicker">Bedankt!</span>
<h1 class="headline">@( _artikel is not null ? "Artikel ingediend" : "Bevestiging")</h1>
@if (_artikel is null)
{
<p>Er is geen recent ingediend artikel gevonden.</p>
}
else
{
<p>Het artikel <strong>@_artikel.Title</strong> is veilig in de brievenbus van de redactie gevallen.</p>
<dl class="bevestiging-lijst">
<dt>Schrijver</dt>
<dd>@_artikel.AuthorName</dd>
<dt>Gebeurtenis</dt>
<dd>@Krant.DutchShortDate(_artikel.EventDate)</dd>
@if (_artikel.SeasonName is not null)
{
<dt>Editie</dt>
<dd>@_artikel.SeasonName</dd>
}
</dl>
<p class="muted">De redactie bekijkt alle inzendingen en publiceert ze in de komende editie.</p>
<div class="form-actions">
<a class="btn btn-primary" href="/indienen">Nog een artikel indienen</a>
<a class="btn" href="/">Naar de voorpagina</a>
</div>
}
</section>
@code {
[SupplyParameterFromQuery(Name = "artikel")]
public int? ArtikelId { get; set; }
private Article? _artikel;
protected override async Task OnInitializedAsync()
{
if (ArtikelId is int id)
{
_artikel = await Artikelen.FindAsync(id);
}
}
}
@@ -0,0 +1,231 @@
@page "/indienen/formulier"
@using System.ComponentModel.DataAnnotations
@inject ArticleService Artikelen
@inject SeasonService Seizoenen
@inject UserService Gebruikers
@inject UploadService Uploads
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>@(_bewerkArtikel is not null ? "Artikel bewerken" : "Artikel indienen") — ProostenKrant</PageTitle>
<section class="card">
<span class="kicker">@(_bewerkArtikel is not null ? "Bewerken" : "Medewerking gevraagd")</span>
<h1 class="headline">@( _bewerkArtikel is not null ? "Artikel bewerken" : "Nieuw artikel indienen")</h1>
@if (_fout is not null)
{
<p class="alert alert-error">@_fout</p>
}
<EditForm FormName="indienen" Method="post" Model="Input" OnValidSubmit="Opslaan"
class="krant-form" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Titel</label>
<InputText @bind-Value="Input.Title" id="title" class="form-control" placeholder="De kop boven je artikel" />
<ValidationMessage For="() => Input.Title" />
</div>
<div class="form-group">
<label for="content">Tekst</label>
<InputTextArea @bind-Value="Input.Content" id="content" rows="10" class="form-control"
placeholder="Schrijf hier de inhoud van je artikel…" />
<ValidationMessage For="() => Input.Content" />
</div>
<div class="form-row">
<div class="form-group">
<label for="writer">Schrijver</label>
<InputSelect @bind-Value="Input.WriterId" id="writer" class="form-control">
<option value="0">— Kies schrijver —</option>
@foreach (var gebruiker in _gebruikers)
{
<option value="@gebruiker.Id">@gebruiker.DisplayName</option>
}
</InputSelect>
<ValidationMessage For="() => Input.WriterId" />
</div>
<div class="form-group">
<label for="eventdate">Datum van de gebeurtenis</label>
<InputDate @bind-Value="Input.EventDate" id="eventdate" class="form-control" />
<ValidationMessage For="() => Input.EventDate" />
</div>
</div>
<div class="form-group">
<label>Deelnemers</label>
<p class="help-text">Selecteer alle personages die bij dit verhaal betrokken zijn.</p>
<div class="checkbox-group">
@foreach (var gebruiker in _gebruikers)
{
<label class="checkbox-label">
<input type="checkbox" name="Participants" value="@gebruiker.Id"
checked="@(_geselecteerd.Contains(gebruiker.Id))" />
<span>@gebruiker.DisplayName</span>
</label>
}
</div>
@if (_participantsFout is not null)
{
<span class="error-message">@_participantsFout</span>
}
</div>
<div class="form-group">
<label for="image">Afbeelding</label>
<input type="file" name="Afbeelding" id="image" class="form-control" accept="image/*" />
<span class="help-text">
JPG, PNG, GIF of WEBP, maximaal 25 MB.
@if (_bewerkArtikel is not null && !string.IsNullOrEmpty(_bewerkArtikel.WebImage))
{
<text>Laat leeg om de huidige afbeelding te houden.</text>
}
</span>
@if (_afbeeldingFout is not null)
{
<span class="error-message">@_afbeeldingFout</span>
}
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
@(_bewerkArtikel is not null ? "Wijzigingen opslaan" : "Artikel indienen")
</button>
<a class="btn" href="/">Annuleren</a>
</div>
</EditForm>
</section>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "indienen")]
private IndienInput Input { get; set; } = new();
private List<User> _gebruikers = new();
private List<int> _geselecteerd = new();
private Article? _bewerkArtikel;
private string? _fout;
private string? _participantsFout;
private string? _afbeeldingFout;
protected override async Task OnInitializedAsync()
{
Input ??= new();
var state = await AuthStateProvider.GetAuthenticationStateAsync();
var isEditor = KrantAuth.IsEditor(state.User);
var heeftPaszin = KrantAuth.HasValidPassphrase(state.User);
var bewerkId = KrantAuth.EditArticleId(state.User);
if (!isEditor && !heeftPaszin)
{
Nav.NavigateTo("/indienen", forceLoad: true);
return;
}
_gebruikers = await Gebruikers.GetAllAsync();
Input.EventDate ??= DateTime.Today;
if (bewerkId is int id)
{
_bewerkArtikel = await Artikelen.FindAsync(id);
if (_bewerkArtikel is null)
{
Nav.NavigateTo("/indienen/bewerken/lijst", forceLoad: true);
return;
}
// Alleen bij een GET-request het formulier vullen; bij een POST
// zijn de waarden al door de formulierbinding gezet.
if (string.Equals(HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
Input.Title = _bewerkArtikel.Title;
Input.Content = _bewerkArtikel.Content;
Input.WriterId = _bewerkArtikel.AuthorId;
Input.EventDate = _bewerkArtikel.EventDate;
_geselecteerd = _bewerkArtikel.ParticipantIds;
}
}
}
private async Task Opslaan()
{
Input ??= new();
var form = HttpContext.Request.Form;
_geselecteerd = form["Participants"].SelectMany(v => (v ?? "").Split(','))
.Select(s => int.TryParse(s, out var id) ? id : 0).Where(id => id > 0).ToList();
if (Input.WriterId <= 0)
{
_fout = "Kies een schrijver.";
return;
}
if (!_geselecteerd.Contains(Input.WriterId))
{
_geselecteerd.Insert(0, Input.WriterId);
}
if (_geselecteerd.Count == 0)
{
_participantsFout = "Selecteer minstens één deelnemer.";
return;
}
string? webPad = _bewerkArtikel?.ArticleImage;
var bestand = HttpContext.Request.Form.Files["Afbeelding"];
if (bestand is { Length: > 0 } || _bewerkArtikel is null)
{
var (nieuwPad, fout) = await Uploads.SaveArticleImageAsync(bestand);
if (fout is not null)
{
_afbeeldingFout = fout;
return;
}
webPad = nieuwPad ?? webPad;
}
var season = await Seizoenen.GetByDateAsync(Input.EventDate ?? DateTime.Today);
if (_bewerkArtikel is not null)
{
_bewerkArtikel.Title = Input.Title;
_bewerkArtikel.Content = Input.Content;
_bewerkArtikel.AuthorId = Input.WriterId;
_bewerkArtikel.Participants = string.Join(",", _geselecteerd);
_bewerkArtikel.EventDate = Input.EventDate;
_bewerkArtikel.SeasonId = season?.Id ?? _bewerkArtikel.SeasonId;
_bewerkArtikel.ArticleImage = webPad;
await Artikelen.UpdateAsync(_bewerkArtikel);
var artikelId = _bewerkArtikel.Id;
await HttpContext.SignOutAsync(KrantAuth.Scheme);
Nav.NavigateTo($"/indienen/bevestiging?artikel={artikelId}", forceLoad: true);
}
else
{
var artikel = new Article
{
Title = Input.Title,
Content = Input.Content,
AuthorId = Input.WriterId,
Participants = string.Join(",", _geselecteerd),
EventDate = Input.EventDate,
SeasonId = season?.Id,
ArticleImage = webPad,
Status = "draft",
};
var nieuwId = await Artikelen.CreateAsync(artikel);
await HttpContext.SignOutAsync(KrantAuth.Scheme);
Nav.NavigateTo($"/indienen/bevestiging?artikel={nieuwId}", forceLoad: true);
}
}
private sealed class IndienInput
{
[Required(ErrorMessage = "Geef het artikel een titel (minstens 5 tekens).")]
[MinLength(5, ErrorMessage = "De titel moet minstens 5 tekens bevatten.")]
[MaxLength(255)]
public string Title { get; set; } = "";
[Required(ErrorMessage = "Schrijf de tekst van het artikel (minstens 10 tekens).")]
[MinLength(10, ErrorMessage = "De tekst moet minstens 10 tekens bevatten.")]
public string Content { get; set; } = "";
public int WriterId { get; set; }
public DateTime? EventDate { get; set; }
}
}
@@ -0,0 +1,78 @@
@page "/login"
@using System.ComponentModel.DataAnnotations
@inject UserService Gebruikers
<PageTitle>Inloggen — ProostenKrant</PageTitle>
<section class="card auth-card">
<h1 class="headline">Inloggen redactie</h1>
<p class="muted">Kies je naam en voer het familiewachtwoord in.</p>
@if (_fout is not null)
{
<p class="alert alert-error">@_fout</p>
}
<EditForm FormName="login" Method="post" Model="Input" OnValidSubmit="LoginUser" class="krant-form">
<div class="form-group">
<label for="username">Familielid</label>
<InputSelect @bind-Value="Input.Username" id="username" class="form-control">
<option value="">— Kies je naam —</option>
@foreach (var gebruiker in _gebruikers)
{
<option value="@gebruiker.Username">@gebruiker.DisplayName</option>
}
</InputSelect>
<ValidationMessage For="() => Input.Username" />
</div>
<div class="form-group">
<label for="password">Wachtwoord</label>
<InputText type="password" @bind-Value="Input.Password" id="password" class="form-control" autocomplete="current-password" />
<ValidationMessage For="() => Input.Password" />
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Inloggen</button>
</div>
</EditForm>
</section>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "login")]
private LoginInput Input { get; set; } = new();
private List<User> _gebruikers = new();
private string? _fout;
protected override async Task OnInitializedAsync()
{
Input ??= new();
_gebruikers = await Gebruikers.GetAllAsync();
}
private async Task LoginUser()
{
var gebruiker = _gebruikers.FirstOrDefault(g =>
string.Equals(g.Username, Input.Username, StringComparison.OrdinalIgnoreCase));
if (gebruiker is null || Input.Password != Krant.UniversalPassword)
{
_fout = "Ongeldige gebruikersnaam of wachtwoord.";
return;
}
var principal = KrantAuth.EditorPrincipal(gebruiker);
await HttpContext.SignInAsync(KrantAuth.Scheme, principal);
Nav.NavigateTo("/dashboard", forceLoad: true);
}
private sealed class LoginInput
{
[Required(ErrorMessage = "Kies je naam.")]
public string Username { get; set; } = "";
[Required(ErrorMessage = "Voer het wachtwoord in.")]
public string Password { get; set; } = "";
}
}
@@ -0,0 +1,115 @@
@page "/seizoenen/{Id:int}"
@attribute [Authorize(Policy = "Editor")]
@inject SeasonService Seizoenen
@inject ArticleService Artikelen
@inject LatexService Latex
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Af
<PageTitle>@(_seizoen?.Name ?? "Seizoen") — ProostenKrant</PageTitle>
@if (_seizoen is null)
{
<section class="card">
<h1 class="headline">Seizoen niet gevonden</h1>
<p><a href="/seizoenen">Naar het seizoensoverzicht</a>.</p>
</section>
return;
}
<section class="card">
<span class="kicker seizoen-@_seizoen.CssKey">Editie @_seizoen.Name @_seizoen.StartDate.Year</span>
<h1 class="headline">@_seizoen.Name @(_seizoen.StartDate.Year)</h1>
<p class="muted">
@Krant.DutchShortDate(_seizoen.StartDate) &mdash; @Krant.DutchShortDate(_seizoen.EndDate).
@_artikelen.Count gepubliceerde artikelen in deze editie.
</p>
<div class="form-actions">
<form method="post" action="/api/genereer-pdf" class="inline-form">
<input type="hidden" name="SeizoenId" value="@Id" />
<input type="hidden" name="__RequestVerificationToken" value="@_afToken" />
<button type="submit" class="btn btn-primary">Druk de @_seizoen.Name-editie</button>
</form>
<a class="btn" href="/seizoenen">Terug</a>
</div>
</section>
@if (_fout is not null)
{
<p class="alert alert-error">@_fout</p>
}
@if (_melding is not null)
{
<p class="alert alert-success">
@_melding
@if (_nieuwPdf is not null)
{
<text>&nbsp;<a href="/edities/@_nieuwPdf">Download &rarr;</a></text>
}
</p>
}
<h2 class="section-heading">Artikelen in deze editie</h2>
@if (_artikelen.Count == 0)
{
<p class="muted">Er zijn nog geen gepubliceerde artikelen in deze editie.</p>
}
else
{
<table class="krant-tabel">
<thead>
<tr><th>Titel</th><th>Schrijver</th><th>Gebeurtenis</th><th></th></tr>
</thead>
<tbody>
@foreach (var artikel in _artikelen)
{
<tr>
<td><a href="/artikelen/@artikel.Id">@artikel.Title</a></td>
<td>@artikel.AuthorName</td>
<td>@Krant.DutchShortDate(artikel.EventDate)</td>
<td><a class="btn" href="/artikelen/@artikel.Id">Openen</a></td>
</tr>
}
</tbody>
</table>
}
@code {
[Parameter]
public int Id { get; set; }
[SupplyParameterFromForm(FormName = "genereer-detail")]
private ActieInput Input { get; set; } = new();
private sealed class ActieInput { }
private Season? _seizoen;
private List<Article> _artikelen = new();
private string? _melding;
private string? _fout;
private string? _nieuwPdf;
private string? _afToken;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
_afToken = Af.GetAndStoreTokens(HttpContext).RequestToken;
_seizoen = await Seizoenen.FindAsync(Id);
if (_seizoen is not null)
{
_artikelen = await Artikelen.GetBySeasonAsync(Id);
}
LeesMelding();
}
private void LeesMelding()
{
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(new Uri(Nav.Uri).Query);
query.TryGetValue("melding", out var melding);
query.TryGetValue("fout", out var fout);
_melding = melding;
_fout = fout;
query.TryGetValue("pdf", out var pdf);
_nieuwPdf = pdf;
}
}
@@ -0,0 +1,129 @@
@page "/seizoenen"
@attribute [Authorize(Policy = "Editor")]
@inject SeasonService SeizoenSvc
@inject ArticleService Artikelen
@inject LatexService Latex
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Af
<PageTitle>Seizoenen — ProostenKrant</PageTitle>
<h1 class="page-heading">Seizoenen &amp; drukwerk</h1>
<p class="page-intro">Per seizoen wordt een editie van de krant gedrukt. Publiceer artikelen en druk daarna de editie.</p>
@if (_melding is not null)
{
<p class="alert alert-success">
@_melding
@if (_nieuwPdf is not null)
{
<text>&nbsp;<a href="/edities/@_nieuwPdf">Download de krant &rarr;</a></text>
}
</p>
}
@if (_fout is not null)
{
<div class="alert alert-error">
<p>@_fout</p>
</div>
}
<div class="season-grid">
@foreach (var seizoen in _seizoenInfo)
{
<section class="season-card seizoen-@seizoen.Season.CssKey">
<div class="season-card-head">
<h2>@seizoen.Season.Name @seizoen.Season.StartDate.Year</h2>
@if (seizoen.IsHuidig)
{
<span class="tag">lopende editie</span>
}
</div>
<p class="muted">
@Krant.DutchShortDate(seizoen.Season.StartDate) &mdash; @Krant.DutchShortDate(seizoen.Season.EndDate)
</p>
<ul class="season-stats">
<li><strong>@seizoen.Gepubliceerd</strong> gepubliceerd</li>
<li><strong>@seizoen.Klad</strong> klad</li>
</ul>
<div class="form-actions">
<form method="post" action="/api/genereer-pdf" class="inline-form">
<input type="hidden" name="SeizoenId" value="@seizoen.Season.Id" />
<input type="hidden" name="__RequestVerificationToken" value="@_afToken" />
<button type="submit" class="btn btn-primary">Druk deze editie</button>
</form>
<a class="btn" href="/seizoenen/@seizoen.Season.Id">Artikelen bekijken</a>
</div>
</section>
}
</div>
<h2 class="section-heading">Gedrukte edities</h2>
@if (_edities.Count > 0)
{
<table class="krant-tabel">
<thead>
<tr><th>Editie</th><th>Datum</th><th>Grootte</th><th></th></tr>
</thead>
<tbody>
@foreach (var editie in _edities)
{
<tr>
<td>@editie.DisplayName</td>
<td>@Krant.DutchShortDate(editie.Date)</td>
<td>@(editie.SizeBytes / 1024) kB</td>
<td><a class="btn" href="/edities/@editie.FileName">Download PDF</a></td>
</tr>
}
</tbody>
</table>
}
else
{
<p class="muted">Er werd nog geen editie gedrukt.</p>
}
@code {
[SupplyParameterFromForm(FormName = "genereer-seizoen")]
private GenereerInput Input { get; set; } = new();
private sealed class GenereerInput
{
public int SeasonId { get; set; }
}
private List<(Season Season, int Gepubliceerd, int Klad, bool IsHuidig)> _seizoenInfo = new();
private List<Edition> _edities = new();
private string? _melding;
private string? _fout;
private string? _nieuwPdf;
private string? _afToken;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
_afToken = Af.GetAndStoreTokens(HttpContext).RequestToken;
var seasons = await SeizoenSvc.GetAllAsync();
var alle = await Artikelen.GetAllAsync();
_seizoenInfo = seasons.Select(s =>
{
var vanS = alle.Where(a => a.SeasonId == s.Id).ToList();
return (s, vanS.Count(a => a.Status == "published"), vanS.Count(a => a.Status == "draft"),
s.StartDate <= DateTime.Now && s.EndDate >= DateTime.Now);
}).ToList();
_edities = Latex.ListEditions();
LeesMelding();
}
private void LeesMelding()
{
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(new Uri(Nav.Uri).Query);
query.TryGetValue("melding", out var melding);
query.TryGetValue("fout", out var fout);
_melding = melding;
_fout = fout;
query.TryGetValue("pdf", out var pdf);
_nieuwPdf = pdf;
}
}
@@ -0,0 +1,36 @@
@page "/uitloggen"
<PageTitle>Uitloggen — ProostenKrant</PageTitle>
<section class="card auth-card">
<h1 class="headline">Uitloggen</h1>
<p class="muted">Bevestig dat je wilt uitloggen bij de redactie.</p>
<EditForm FormName="uitloggen" Method="post" Model="Input" OnValidSubmit="UitloggenUitvoeren" class="krant-form">
<div class="form-actions">
<button type="submit" class="btn btn-primary">Uitloggen</button>
<a class="btn" href="/">Annuleren</a>
</div>
</EditForm>
</section>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "uitloggen")]
private ActieInput Input { get; set; } = new();
private sealed class ActieInput { }
protected override Task OnInitializedAsync()
{
Input ??= new();
return Task.CompletedTask;
}
private async Task UitloggenUitvoeren()
{
await HttpContext.SignOutAsync(KrantAuth.Scheme);
Nav.NavigateTo("/", forceLoad: true);
}
}
+15
View File
@@ -0,0 +1,15 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1, .masthead-title" />
</Found>
<NotFound>
<PageTitle>Pagina niet gevonden</PageTitle>
<LayoutView Layout="typeof(Layout.MainLayout)">
<section class="card">
<h2 class="headline">404 — Pagina niet gevonden</h2>
<p>Deze pagina bestaat niet (meer). Keer terug naar de <a href="/">voorpagina</a>.</p>
</section>
</LayoutView>
</NotFound>
</Router>
@@ -0,0 +1,17 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using ProostenKrant
@using ProostenKrant.Components
@using ProostenKrant.Components.Layout
@using ProostenKrant.Models
@using ProostenKrant.Services
@inject KrantInfo Krant
@inject NavigationManager Nav