First commit

This commit is contained in:
julien
2026-03-27 14:43:08 +01:00
commit ced7dbfbf7
54 changed files with 3680 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
<section class="stack-lg" aria-labelledby="dashboard-title">
<header class="page-header">
<h1 class="page-title" id="dashboard-title">Tableau de bord</h1>
<div class="page-actions">
<a class="button" href="{{ @BASE }}/dashboard/posts/create">Nouvel article</a>
<a class="button button--ghost" href="{{ @BASE }}/dashboard/media">Médiathèque</a>
</div>
</header>
<check if="{{ @posts }}">
<true>
<div class="card-grid">
<repeat group="{{ @posts }}" value="{{ @post }}">
<include href="partials/post_card_admin.html" />
</repeat>
</div>
<include href="partials/pagination.html" />
</true>
<false>
<section class="empty-state" aria-labelledby="dashboard-empty-title">
<h2 class="card-title" id="dashboard-empty-title">Aucun article</h2>
<p>Commence par créer un premier article.</p>
</section>
</false>
</check>
</section>

View File

@@ -0,0 +1,35 @@
<section class="stack-lg" aria-labelledby="media-title">
<header class="page-header">
<h1 class="page-title" id="media-title">Médiathèque</h1>
<div class="page-actions">
<a class="button button--ghost" href="{{ @BASE }}/dashboard">Retour</a>
</div>
</header>
<form class="panel stack" method="post" action="{{ @BASE }}/dashboard/media" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ @csrfToken }}">
<label class="field">
<span class="field-label">Nouvelle image</span>
<input class="control" type="file" name="image" accept="image/jpeg,image/png,image/webp" required>
<span class="field-help">Formats acceptés : JPG, PNG, WebP.</span>
</label>
<button class="button" type="submit">Envoyer</button>
</form>
<check if="{{ @items }}">
<true>
<div class="card-grid">
<repeat group="{{ @items }}" value="{{ @item }}">
<include href="partials/media_card.html" />
</repeat>
</div>
</true>
<false>
<section class="empty-state" aria-labelledby="media-empty-title">
<h2 class="card-title" id="media-empty-title">Aucune image</h2>
<p>Ajoute ta première image.</p>
</section>
</false>
</check>
</section>

View File

@@ -0,0 +1,107 @@
<section class="stack-lg" aria-labelledby="post-form-title">
<header class="page-header">
<h1 class="page-title" id="post-form-title">{{ @pageTitle }}</h1>
<div class="page-actions">
<a class="button button--ghost" href="{{ @BASE }}/dashboard">Retour</a>
</div>
</header>
<div class="editor-layout" data-editor-layout>
<form class="panel stack editor-form" method="post" action="{{ @formAction }}">
<input type="hidden" name="csrf_token" value="{{ @csrfToken }}">
<input type="hidden" name="cover_media_id" value="{{ @post.cover_media_id }}" data-cover-input>
<label class="field">
<span class="field-label">Titre</span>
<input class="control" type="text" name="title" value="{{ @post.title }}" maxlength="{{ @titleMax }}" required data-char-count>
<span class="char-counter"><span data-char-count-value>0</span> / {{ @titleMax }}</span>
</label>
<label class="field">
<span class="field-label">Extrait</span>
<textarea class="control" name="excerpt" rows="3" maxlength="{{ @excerptMax }}" required data-char-count>{{ @post.excerpt }}</textarea>
<span class="char-counter"><span data-char-count-value>0</span> / {{ @excerptMax }}</span>
</label>
<section class="field cover-field">
<div class="field-head">
<div>
<h2 class="field-label">Image de couverture</h2>
<p class="field-help">Choisis une image si tu veux une couverture.</p>
</div>
</div>
<div class="cover-picker">
<check if="{{ @coverPreview }}">
<true>
<img class="media-frame media-frame--large cover-preview" data-cover-preview src="{{ @coverPreview.url }}" alt="">
<div class="media-frame media-frame--large media-frame--placeholder is-hidden" data-cover-placeholder>Aucune image</div>
</true>
<false>
<div class="media-frame media-frame--large media-frame--placeholder" data-cover-placeholder>Aucune image</div>
<img class="media-frame media-frame--large cover-preview is-hidden" data-cover-preview alt="Aperçu couverture">
</false>
</check>
<div class="button-row">
<button class="button button--ghost" type="button" data-media-picker-open="cover">Choisir une image</button>
<button class="button button--ghost" type="button" data-cover-clear {{ @post.cover_media_id ? '' : 'disabled' }}>Retirer</button>
</div>
</div>
</section>
<section class="field">
<div class="field-head">
<div>
<h2 class="field-label">Contenu</h2>
<p class="field-help">Markdown simple, avec insertion dimage au curseur.</p>
</div>
</div>
<div class="toolbar" role="toolbar" aria-label="Outils Markdown">
<button class="tool-button" type="button" data-md-action="bold"><strong>Gras</strong></button>
<button class="tool-button" type="button" data-md-action="italic"><em>Italique</em></button>
<button class="tool-button" type="button" data-md-action="heading">Titre</button>
<button class="tool-button" type="button" data-md-action="list">Liste</button>
<button class="tool-button" type="button" data-md-action="quote">Citation</button>
<button class="tool-button" type="button" data-md-action="link">Lien</button>
<button class="tool-button" type="button" data-md-action="code">Code</button>
<button class="tool-button" type="button" data-media-picker-open="markdown">Image</button>
</div>
<textarea class="control editor-textarea" name="body_markdown" rows="18" required data-markdown-editor>{{ @post.body_markdown }}</textarea>
</section>
<button class="button" type="submit">Enregistrer</button>
</form>
<aside class="media-picker is-hidden" data-media-picker>
<div class="media-picker__head">
<div>
<strong data-media-picker-title>Choisir une image</strong>
<p class="field-help" data-media-picker-help>Choisis une image de la médiathèque.</p>
</div>
<button class="button button--ghost button--small" type="button" data-media-picker-close>Fermer</button>
</div>
<check if="{{ @mediaItems }}">
<true>
<div class="media-picker__grid">
<repeat group="{{ @mediaItems }}" value="{{ @item }}">
<button class="media-picker__item" type="button" data-media-picker-select data-media-id="{{ @item.id }}" data-media-url="{{ @item.url }}" data-media-markdown="{{ @item.markdown }}">
<img class="media-frame media-frame--square" src="{{ @item.url }}" alt="">
</button>
</repeat>
</div>
</true>
<false>
<section class="empty-state" aria-labelledby="media-picker-empty-title">
<h2 class="card-title" id="media-picker-empty-title">Aucune image disponible</h2>
<p>Ajoute une image depuis la médiathèque.</p>
</section>
</false>
</check>
</aside>
</div>
</section>

21
app/Views/auth/login.html Normal file
View File

@@ -0,0 +1,21 @@
<section class="auth-shell panel stack" aria-labelledby="login-title">
<header class="page-header page-header--compact">
<h1 class="page-title" id="login-title">Connexion</h1>
</header>
<form class="stack" method="post" action="{{ @BASE }}/login">
<input type="hidden" name="csrf_token" value="{{ @csrfToken }}">
<label class="field">
<span class="field-label">Nom dutilisateur</span>
<input class="control" type="text" name="username" autocomplete="username" required>
</label>
<label class="field">
<span class="field-label">Mot de passe</span>
<input class="control" type="password" name="password" autocomplete="current-password" required>
</label>
<button class="button" type="submit">Se connecter</button>
</form>
</section>

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ @errorTitle ?: 'Erreur' }}</title>
<link rel="icon" href="{{ @BASE }}/assets/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="{{ @BASE }}/min/app.css">
</head>
<body>
<main class="page error-page">
<div class="container">
<section class="error-card">
<p class="error-page__code">Erreur {{ @errorCode ?: 500 }}</p>
<h1 class="error-page__title">{{ @errorTitle ?: 'Erreur' }}</h1>
<p class="error-page__message">{{ @errorMessage ?: 'Une erreur est survenue.' }}</p>
<p class="error-page__hint">Vérifie ladresse ou reviens à laccueil.</p>
<p class="error-page__actions"><a class="button" href="{{ @BASE }}/">Retour à laccueil</a></p>
</section>
</div>
</main>
</body>
</html>

24
app/Views/layout.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ @pageTitle ? @pageTitle . ' · ' . @app.name : @app.name }}</title>
<meta name="description" content="{{ @app.tagline }}">
<link rel="icon" href="{{ @BASE }}/assets/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="{{ @BASE }}/min/app.css">
<script defer src="{{ @BASE }}/min/app.js"></script>
</head>
<body>
<include href="partials/site_navigation.html" />
<main class="page" id="main-content">
<div class="container">
<check if="{{ @flash }}">
<div class="flash flash--{{ @flash.type }}" role="status">{{ @flash.message }}</div>
</check>
<include href="{{ @view }}" />
</div>
</main>
</body>
</html>

View File

@@ -0,0 +1,23 @@
<article class="card article-card">
<img class="media-frame" src="{{ @item.url }}" alt="{{ @item.alt }}">
<div class="card-body article-card__body">
<p class="meta-text">{{ @item.width }} × {{ @item.height }}<br>{{ @item.created_at_label }}</p>
<form class="stack" method="post" action="{{ @BASE }}/dashboard/media/{{ @item.id }}/alt">
<input type="hidden" name="csrf_token" value="{{ @csrfToken }}">
<label class="field">
<span class="field-label">Texte alternatif</span>
<input class="control" type="text" name="alt" value="{{ @item.alt }}" placeholder="Description de l'image" data-alt-input>
</label>
<button class="button button--ghost button--small" type="submit">Enregistrer</button>
</form>
<div class="card-actions">
<button class="button button--ghost" type="button" data-copy-text="{{ @item.markdown }}" data-markdown-template="![](media:{{ @item.file_name }})">Copier le Markdown</button>
<form method="post" action="{{ @BASE }}/dashboard/media/{{ @item.id }}/delete" data-confirm="Supprimer cette image ?">
<input type="hidden" name="csrf_token" value="{{ @csrfToken }}">
<button class="button button--danger" type="submit">Supprimer</button>
</form>
</div>
</div>
</article>

View File

@@ -0,0 +1,20 @@
<ul class="nav-items">
<check if="{{ @currentUser }}">
<true>
<li class="nav-items__item">
<a class="nav-items__link" href="{{ @BASE }}/dashboard">Dashboard</a>
</li>
<li class="nav-items__item">
<form class="nav-items__form" method="post" action="{{ @BASE }}/logout">
<input type="hidden" name="csrf_token" value="{{ @csrfToken }}">
<button class="nav-items__button" type="submit">Déconnexion</button>
</form>
</li>
</true>
<false>
<li class="nav-items__item">
<a class="nav-items__link" href="{{ @BASE }}/login">Connexion</a>
</li>
</false>
</check>
</ul>

View File

@@ -0,0 +1,25 @@
<check if="{{ @pagination.pages > 1 }}">
<true>
<nav class="pagination" aria-label="Pagination">
<check if="{{ @pagination.page > 1 }}">
<true>
<a class="button button--ghost" href="{{ @paginationBase }}?page={{ @pagination.page - 1 }}">Précédent</a>
</true>
<false>
<span class="button button--ghost pagination__disabled">Précédent</span>
</false>
</check>
<span class="pagination__info">Page {{ @pagination.page }} sur {{ @pagination.pages }}</span>
<check if="{{ @pagination.page < @pagination.pages }}">
<true>
<a class="button button--ghost" href="{{ @paginationBase }}?page={{ @pagination.page + 1 }}">Suivant</a>
</true>
<false>
<span class="button button--ghost pagination__disabled">Suivant</span>
</false>
</check>
</nav>
</true>
</check>

View File

@@ -0,0 +1,20 @@
<article class="card article-card">
<a class="card-media-link" href="{{ @BASE }}/posts/{{ @post.slug }}">
<check if="{{ @post.cover_url }}">
<true><img class="media-frame" src="{{ @post.cover_url }}" alt="{{ @post.title }}"></true>
<false>
<div class="media-frame media-frame--placeholder">Aucune image</div>
</false>
</check>
</a>
<div class="card-body article-card__body">
<h2 class="card-title">{{ @post.title }}</h2>
<p class="meta-text">
Publié le <time datetime="{{ @post.created_at }}">{{ @post.created_at_label }}</time>
<check if="{{ @post.has_updated_at }}">
<true><br>Mis à jour le <time datetime="{{ @post.updated_at }}">{{ @post.updated_at_label }}</time></true>
</check>
</p>
<p class="card-summary">{{ @post.excerpt }}</p>
</div>
</article>

View File

@@ -0,0 +1,29 @@
<article class="card article-card">
<a class="card-media-link" href="{{ @BASE }}/dashboard/posts/{{ @post.id }}/edit">
<check if="{{ @post.cover_url }}">
<true><img class="media-frame" src="{{ @post.cover_url }}" alt="{{ @post.title }}"></true>
<false>
<div class="media-frame media-frame--placeholder">Aucune image</div>
</false>
</check>
</a>
<div class="card-body article-card__body">
<h2 class="card-title">{{ @post.title }}</h2>
<p class="meta-text">
Publié le <time datetime="{{ @post.created_at }}">{{ @post.created_at_label }}</time>
<check if="{{ @post.has_updated_at }}">
<true><br>Mis à jour le <time datetime="{{ @post.updated_at }}">{{ @post.updated_at_label }}</time></true>
</check>
</p>
<p class="card-summary">{{ @post.excerpt }}</p>
<div class="card-actions">
<a class="button button--ghost" href="{{ @BASE }}/posts/{{ @post.slug }}">Voir</a>
<a class="button button--ghost" href="{{ @BASE }}/dashboard/posts/{{ @post.id }}/edit">Modifier</a>
<form method="post" action="{{ @BASE }}/dashboard/posts/{{ @post.id }}/delete"
data-confirm="Supprimer cet article ?">
<input type="hidden" name="csrf_token" value="{{ @csrfToken }}">
<button class="button button--danger" type="submit">Supprimer</button>
</form>
</div>
</div>
</article>

View File

@@ -0,0 +1 @@
<a class="site-brand__title" href="{{ @BASE }}/">{{ @app.name }}</a>

View File

@@ -0,0 +1,44 @@
<input class="nav-toggle" type="checkbox" id="nav-toggle" aria-hidden="true">
<header class="site-header">
<div class="container site-header__inner">
<label class="nav-toggle-button" for="nav-toggle">
<span class="sr-only">Ouvrir le menu</span>
<span class="nav-toggle-button__line"></span>
<span class="nav-toggle-button__line"></span>
<span class="nav-toggle-button__line"></span>
</label>
<div class="site-brand site-brand--header">
<include href="partials/site_brand.html" />
</div>
<nav class="nav nav--desktop" aria-label="Navigation principale">
<include href="partials/nav_items.html" />
</nav>
<span class="site-header__spacer" aria-hidden="true"></span>
</div>
</header>
<div class="mobile-menu">
<label class="mobile-menu__backdrop" for="nav-toggle" aria-hidden="true"></label>
<div class="mobile-menu__panel">
<header class="mobile-menu__header">
<div class="site-brand site-brand--menu">
<include href="partials/site_brand.html" />
</div>
<label class="mobile-menu__close" for="nav-toggle">
<span class="sr-only">Fermer le menu</span>
<span class="mobile-menu__close-line"></span>
<span class="mobile-menu__close-line"></span>
</label>
</header>
<nav class="mobile-menu__nav" aria-label="Navigation principale mobile">
<include href="partials/nav_items.html" />
</nav>
</div>
</div>

22
app/Views/site/home.html Normal file
View File

@@ -0,0 +1,22 @@
<section class="stack-lg" aria-labelledby="home-title">
<header class="page-header">
<h1 class="page-title" id="home-title">Articles</h1>
</header>
<check if="{{ @posts }}">
<true>
<div class="card-grid">
<repeat group="{{ @posts }}" value="{{ @post }}">
<include href="partials/post_card.html" />
</repeat>
</div>
<include href="partials/pagination.html" />
</true>
<false>
<section class="empty-state" aria-labelledby="home-empty-title">
<h2 class="card-title" id="home-empty-title">Aucun article</h2>
<p>Le premier article arrivera bientôt.</p>
</section>
</false>
</check>
</section>

24
app/Views/site/post.html Normal file
View File

@@ -0,0 +1,24 @@
<article class="article" aria-labelledby="post-title">
<header class="article-header">
<h1 class="article-title" id="post-title">{{ @post.title }}</h1>
<p class="meta-text">
Publié le <time datetime="{{ @post.created_at }}">{{ @post.created_at_label }}</time>
<check if="{{ @post.has_updated_at }}">
<true><br>Mis à jour le <time datetime="{{ @post.updated_at }}">{{ @post.updated_at_label }}</time></true>
</check>
</p>
</header>
<check if="{{ @post.cover_url }}">
<true>
<img class="media-frame media-frame--large article-cover" src="{{ @post.cover_url }}"
alt="{{ @post.title }}">
</true>
<false>
<div class="media-frame media-frame--large media-frame--placeholder article-cover">Aucune image
</div>
</false>
</check>
<div class="prose">{{ @post.body_html | raw }}</div>
</article>