Spaces:
Sleeping
Sleeping
Upload 16 files
Browse files- templates/admin_articles.html +19 -25
- templates/admin_edit_article.html +68 -89
- templates/admin_new_article.html +151 -76
- templates/article.html +22 -16
- templates/articles.html +5 -7
- templates/base.html +1 -1
- templates/home.html +7 -9
templates/admin_articles.html
CHANGED
|
@@ -8,31 +8,25 @@
|
|
| 8 |
<a href="{{ url_for('admin_home') }}" class="btn btn-secondary me-2">Retour à l'admin</a>
|
| 9 |
<a href="{{ url_for('admin_new_article') }}" class="btn btn-primary">Nouvel Article</a>
|
| 10 |
</div>
|
| 11 |
-
<div class="
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
<td>{{ article.title }}</td>
|
| 26 |
-
<td>{{ article.category.name }}</td>
|
| 27 |
-
<td>
|
| 28 |
-
<a href="{{ url_for('admin_edit_article', article_id=article.id) }}" class="btn btn-sm btn-outline-primary me-2">Éditer</a>
|
| 29 |
-
<form method="post" action="{{ url_for('admin_delete_article', article_id=article.id) }}" style="display:inline;">
|
| 30 |
-
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet article ?')">Supprimer</button>
|
| 31 |
</form>
|
| 32 |
-
</
|
| 33 |
-
</
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
</div>
|
| 38 |
{% endblock %}
|
|
|
|
| 8 |
<a href="{{ url_for('admin_home') }}" class="btn btn-secondary me-2">Retour à l'admin</a>
|
| 9 |
<a href="{{ url_for('admin_new_article') }}" class="btn btn-primary">Nouvel Article</a>
|
| 10 |
</div>
|
| 11 |
+
<div class="cards-grid">
|
| 12 |
+
{% for article in articles %}
|
| 13 |
+
<div class="card">
|
| 14 |
+
<div class="card-body d-flex flex-column">
|
| 15 |
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:0.5rem;">
|
| 16 |
+
<div>
|
| 17 |
+
<strong>#{{ article.id }}</strong>
|
| 18 |
+
<h5 class="card-title" style="display:inline;margin-left:0.5rem">{{ article.title }}</h5>
|
| 19 |
+
<div class="text-muted">{{ article.category.name }}</div>
|
| 20 |
+
</div>
|
| 21 |
+
<div style="display:flex;flex-direction:column;gap:0.5rem;align-items:flex-end;">
|
| 22 |
+
<a href="{{ url_for('admin_edit_article', article_id=article.id) }}" class="btn btn-primary">Éditer</a>
|
| 23 |
+
<form method="post" action="{{ url_for('admin_delete_article', article_id=article.id) }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet article ?')">
|
| 24 |
+
<button type="submit" class="btn btn-secondary">Supprimer</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</form>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
{% endfor %}
|
| 31 |
</div>
|
| 32 |
{% endblock %}
|
templates/admin_edit_article.html
CHANGED
|
@@ -2,117 +2,96 @@
|
|
| 2 |
<html lang="fr">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
-
<title>Éditer
|
| 6 |
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/dist/trix.css">
|
| 7 |
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/trix.umd.min.js"></script>
|
| 8 |
<style>
|
| 9 |
body { font-family: Arial, sans-serif; margin: 20px; }
|
| 10 |
-
form { max-width:
|
| 11 |
label { display: block; margin-top: 10px; }
|
| 12 |
-
input, button { width: 100%; padding: 10px; margin-top: 5px; }
|
| 13 |
-
trix-editor { border: 1px solid #
|
|
|
|
| 14 |
</style>
|
| 15 |
-
<script>
|
| 16 |
-
document.addEventListener("trix-before-initialize", () => {
|
| 17 |
-
// Allow more tags in sanitization
|
| 18 |
-
Trix.config.dompurify.ADD_TAGS = ["iframe", "video", "source", "table", "tr", "td", "th"];
|
| 19 |
-
Trix.config.dompurify.ADD_ATTR = ["src", "controls", "width", "height"];
|
| 20 |
-
|
| 21 |
-
// Custom toolbar with all options
|
| 22 |
-
Trix.config.toolbar.getDefaultHTML = () => `
|
| 23 |
-
<div class="trix-button-row">
|
| 24 |
-
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
|
| 25 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" tabindex="-1" title="Bold">Bold</button>
|
| 26 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" tabindex="-1" title="Italic">Italic</button>
|
| 27 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" tabindex="-1" title="Strikethrough">Strikethrough</button>
|
| 28 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" tabindex="-1" title="Link">Link</button>
|
| 29 |
-
</span>
|
| 30 |
-
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
|
| 31 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-heading-1" data-trix-attribute="heading1" tabindex="-1" title="Heading">Heading</button>
|
| 32 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-quote" data-trix-attribute="quote" tabindex="-1" title="Quote">Quote</button>
|
| 33 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-code" data-trix-attribute="code" tabindex="-1" title="Code">Code</button>
|
| 34 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" tabindex="-1" title="Bullets">Bullets</button>
|
| 35 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" tabindex="-1" title="Numbers">Numbers</button>
|
| 36 |
-
</span>
|
| 37 |
-
<span class="trix-button-group trix-button-group--file-tools" data-trix-button-group="file-tools">
|
| 38 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-attach" data-trix-action="attachFiles" tabindex="-1" title="Attach Files">Attach Files</button>
|
| 39 |
-
</span>
|
| 40 |
-
<span class="trix-button-group trix-button-group--image-tools" data-trix-button-group="image-tools">
|
| 41 |
-
<button type="button" class="trix-button" data-trix-action="x-align-left" tabindex="-1" title="Align Left">Align Left</button>
|
| 42 |
-
<button type="button" class="trix-button" data-trix-action="x-align-right" tabindex="-1" title="Align Right">Align Right</button>
|
| 43 |
-
</span>
|
| 44 |
-
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
|
| 45 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" tabindex="-1" title="Undo">Undo</button>
|
| 46 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" tabindex="-1" title="Redo">Redo</button>
|
| 47 |
-
</span>
|
| 48 |
-
</div>
|
| 49 |
-
<div class="trix-dialogs" data-trix-dialogs>
|
| 50 |
-
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
|
| 51 |
-
<div class="trix-dialog__link-fields">
|
| 52 |
-
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="Enter a URL..." aria-label="URL" required data-trix-input>
|
| 53 |
-
<div class="trix-button-group">
|
| 54 |
-
<input type="button" class="trix-button trix-button--dialog" value="Link" data-trix-method="setAttribute">
|
| 55 |
-
<input type="button" class="trix-button trix-button--dialog" value="Unlink" data-trix-method="removeAttribute">
|
| 56 |
-
</div>
|
| 57 |
-
</div>
|
| 58 |
-
</div>
|
| 59 |
-
</div>
|
| 60 |
-
`;
|
| 61 |
-
});
|
| 62 |
-
</script>
|
| 63 |
</head>
|
| 64 |
<body>
|
| 65 |
-
<h1>Éditer
|
| 66 |
<a href="{{ url_for('admin_articles') }}">Retour aux articles</a>
|
| 67 |
<form method="post" enctype="multipart/form-data">
|
| 68 |
<label for="title">Titre:</label>
|
| 69 |
<input type="text" id="title" name="title" value="{{ article.title }}" required>
|
| 70 |
|
| 71 |
-
|
| 72 |
-
<
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
<label for="icon">
|
| 76 |
<input type="file" id="icon" name="icon" accept="image/*">
|
| 77 |
|
|
|
|
|
|
|
|
|
|
| 78 |
<label for="content">Contenu:</label>
|
| 79 |
-
<input id="content" type="hidden" name="content" value="{{ article.content }}">
|
| 80 |
<trix-editor input="content"></trix-editor>
|
| 81 |
|
| 82 |
-
<button type="submit">
|
| 83 |
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
<script>
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
const selectedRange = editor.getSelectedRange();
|
| 106 |
-
const document = editor.getDocument();
|
| 107 |
-
const attachments = document.getAttachments();
|
| 108 |
-
attachments.forEach(attachment => {
|
| 109 |
-
const range = attachment.getRange();
|
| 110 |
-
if (selectedRange[0] <= range[0] && range[1] <= selectedRange[1]) {
|
| 111 |
-
attachment.setAttribute("class", actionName === "x-align-left" ? "float-left" : "float-right");
|
| 112 |
-
}
|
| 113 |
-
});
|
| 114 |
-
}
|
| 115 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
</script>
|
| 117 |
</body>
|
| 118 |
-
</html>
|
|
|
|
| 2 |
<html lang="fr">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
+
<title>Éditer Article</title>
|
| 6 |
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/dist/trix.css">
|
| 7 |
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/trix.umd.min.js"></script>
|
| 8 |
<style>
|
| 9 |
body { font-family: Arial, sans-serif; margin: 20px; }
|
| 10 |
+
form { max-width: 900px; margin:0 auto }
|
| 11 |
label { display: block; margin-top: 10px; }
|
| 12 |
+
input, select, button { width: 100%; padding: 10px; margin-top: 5px; box-sizing: border-box }
|
| 13 |
+
trix-editor { border: 1px solid #e9ecef; min-height: 300px; border-radius:8px }
|
| 14 |
+
.article-preview { background:#fff;padding:1rem;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.06);margin-top:1rem }
|
| 15 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
</head>
|
| 17 |
<body>
|
| 18 |
+
<h1>Éditer Article</h1>
|
| 19 |
<a href="{{ url_for('admin_articles') }}">Retour aux articles</a>
|
| 20 |
<form method="post" enctype="multipart/form-data">
|
| 21 |
<label for="title">Titre:</label>
|
| 22 |
<input type="text" id="title" name="title" value="{{ article.title }}" required>
|
| 23 |
|
| 24 |
+
<label for="category_id">Catégorie:</label>
|
| 25 |
+
<select id="category_id" name="category_id" required>
|
| 26 |
+
{% for category in article.category.subject.categories %}
|
| 27 |
+
<option value="{{ category.id }}" {% if category.id == article.category_id %}selected{% endif %}>{{ category.name }}</option>
|
| 28 |
+
{% endfor %}
|
| 29 |
+
</select>
|
| 30 |
|
| 31 |
+
<label for="icon">Image (fichier image):</label>
|
| 32 |
<input type="file" id="icon" name="icon" accept="image/*">
|
| 33 |
|
| 34 |
+
<label for="youtube_url">Lien YouTube (optionnel) :</label>
|
| 35 |
+
<input type="url" id="youtube_url" name="youtube_url" placeholder="https://www.youtube.com/watch?v=..." value="{{ article.youtube_url or '' }}">
|
| 36 |
+
|
| 37 |
<label for="content">Contenu:</label>
|
| 38 |
+
<input id="content" type="hidden" name="content" value="{{ article.content | safe }}">
|
| 39 |
<trix-editor input="content"></trix-editor>
|
| 40 |
|
| 41 |
+
<button type="submit">Enregistrer</button>
|
| 42 |
</form>
|
| 43 |
+
|
| 44 |
+
<h3>Aperçu</h3>
|
| 45 |
+
<div id="editor-preview" class="article-preview"></div>
|
| 46 |
+
|
| 47 |
<script>
|
| 48 |
+
// Configure Trix before init
|
| 49 |
+
document.addEventListener('trix-before-initialize', function(){
|
| 50 |
+
if (window.Trix && Trix.config && Trix.config.dompurify){
|
| 51 |
+
Trix.config.dompurify.ADD_TAGS = ["iframe","video","source","table","tr","td","th","img"];
|
| 52 |
+
Trix.config.dompurify.ADD_ATTR = ["src","controls","width","height","class","style","allowfullscreen"];
|
| 53 |
+
}
|
| 54 |
+
Trix.config.toolbar.getDefaultHTML = function(){
|
| 55 |
+
return `
|
| 56 |
+
<div class="trix-button-row">
|
| 57 |
+
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
|
| 58 |
+
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" title="Gras">Gras</button>
|
| 59 |
+
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" title="Italique">Italique</button>
|
| 60 |
+
<button type="button" class="trix-button" data-trix-action="x-heading" title="Titre">H</button>
|
| 61 |
+
<button type="button" class="trix-button" data-trix-action="x-quote" title="Citation">"</button>
|
| 62 |
+
</span>
|
| 63 |
+
</div>`;
|
| 64 |
+
};
|
| 65 |
+
});
|
| 66 |
|
| 67 |
+
function renderShortcodesForPreview(html){
|
| 68 |
+
if(!html) return '';
|
| 69 |
+
html = html.replace(/\[youtube:([^\]]+)\]/g, function(_, url){
|
| 70 |
+
try{ var parsed = new URL(url.trim()); var id=null; if(parsed.hostname.includes('youtu.be')) id = parsed.pathname.slice(1); else if(parsed.hostname.includes('youtube')) id = parsed.searchParams.get('v') || parsed.pathname.split('/').pop(); if(!id) return '<pre>'+url+'</pre>'; return '<div class="video-player"><iframe src="https://www.youtube.com/embed/'+id+'" allowfullscreen></iframe></div>'; }catch(e){return '<pre>'+url+'</pre>'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
});
|
| 72 |
+
html = html.replace(/\[image:([^\]|]+)(?:\|([^\]]+))?\]/g, function(_, url, opts){
|
| 73 |
+
var classes=''; var style=''; if(opts){ opts.split(/[|,]/).forEach(function(o){ o=o.trim(); if(/^(left|right|center)$/.test(o)){ if(o==='left') classes='float-left'; if(o==='right') classes='float-right'; if(o==='center') style='display:block;margin-left:auto;margin-right:auto'; } else if(/^width=/.test(o)){ style += (style?';':'') + 'width:' + o.split('=')[1]; } else if(/^height=/.test(o)){ style += (style?';':'') + 'height:' + o.split('=')[1]; } }); }
|
| 74 |
+
return '<img src="'+url.trim()+'" class="'+classes+'" style="'+style+'"/>';
|
| 75 |
+
});
|
| 76 |
+
return html;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function uploadFileToServer(fileBlob, filename, attachment){
|
| 80 |
+
var formData = new FormData(); formData.append('file', fileBlob, filename);
|
| 81 |
+
fetch('/upload',{method:'POST',body:formData}).then(function(r){return r.json()}).then(function(data){ if(data.url) attachment.setAttributes({url: data.url}); }).catch(function(e){console.error('Upload failed',e)});
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
document.addEventListener('trix-attachment-add', function(event){
|
| 85 |
+
var attachment = event.attachment; var file = attachment.file; if(!file) return; if(!file.type.startsWith('image/')){ uploadFileToServer(file,file.name,attachment); return; }
|
| 86 |
+
var choice = prompt("Redimensionner l'image ? Entrez largeur en px (ex:800) ou % (ex:80%). Laisser vide pour taille originale. Annuler pour ne pas uploader."); if(choice===null) return; if(!choice){ uploadFileToServer(file,file.name,attachment); return; }
|
| 87 |
+
var reader = new FileReader(); reader.onload = function(e){ var img=new Image(); img.onload=function(){ var origW=img.naturalWidth,origH=img.naturalHeight; var targetW=origW; var c=choice.toString().trim(); if(c.endsWith('%')){ var p=parseFloat(c.replace('%','')); if(!isNaN(p)) targetW=Math.round(origW*p/100);} else { var px=parseInt(c,10); if(!isNaN(px)) targetW=px;} if(targetW<=0||targetW===origW){ uploadFileToServer(file,file.name,attachment); return; } var scale=targetW/origW,targetH=Math.round(origH*scale); var canvas=document.createElement('canvas'); canvas.width=targetW; canvas.height=targetH; var ctx=canvas.getContext('2d'); ctx.drawImage(img,0,0,targetW,targetH); canvas.toBlob(function(blob){ uploadFileToServer(blob,file.name,attachment); }, file.type||'image/jpeg', 0.92); }; img.src=e.target.result; }; reader.readAsDataURL(file);
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
document.addEventListener('trix-action-invoke', function(event){ var action = event.actionName || event.detail && event.detail.actionName; if(action==='x-align-left' || action==='x-align-right'){ var target=event.target||event.detail&&event.detail.target; if(!target) return; var editor = target.editor; var selectedRange = editor.getSelectedRange(); var doc = editor.getDocument(); var attachments = doc.getAttachments(); attachments.forEach(function(att){ var range = att.getRange(); if(selectedRange[0] <= range[0] && range[1] <= selectedRange[1]) att.setAttribute('class', action==='x-align-left'?'float-left':'float-right'); }); } });
|
| 91 |
+
|
| 92 |
+
document.addEventListener('trix-change', function(){ var contentInput = document.querySelector('input[name="content"]'); var preview = document.getElementById('editor-preview'); if(preview && contentInput) preview.innerHTML = renderShortcodesForPreview(contentInput.value); });
|
| 93 |
+
|
| 94 |
+
document.addEventListener('DOMContentLoaded', function(){ var contentInput = document.querySelector('input[name="content"]'); var preview = document.getElementById('editor-preview'); if(preview && contentInput) preview.innerHTML = renderShortcodesForPreview(contentInput.value); });
|
| 95 |
</script>
|
| 96 |
</body>
|
| 97 |
+
</html>
|
templates/admin_new_article.html
CHANGED
|
@@ -7,59 +7,14 @@
|
|
| 7 |
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/trix.umd.min.js"></script>
|
| 8 |
<style>
|
| 9 |
body { font-family: Arial, sans-serif; margin: 20px; }
|
| 10 |
-
form { max-width:
|
| 11 |
label { display: block; margin-top: 10px; }
|
| 12 |
-
input, select, button { width: 100%; padding: 10px; margin-top: 5px; }
|
| 13 |
-
trix-editor { border: 1px solid #
|
|
|
|
|
|
|
|
|
|
| 14 |
</style>
|
| 15 |
-
<script>
|
| 16 |
-
document.addEventListener("trix-before-initialize", () => {
|
| 17 |
-
// Allow more tags in sanitization
|
| 18 |
-
Trix.config.dompurify.ADD_TAGS = ["iframe", "video", "source", "table", "tr", "td", "th"];
|
| 19 |
-
Trix.config.dompurify.ADD_ATTR = ["src", "controls", "width", "height"];
|
| 20 |
-
|
| 21 |
-
// Custom toolbar with all options
|
| 22 |
-
Trix.config.toolbar.getDefaultHTML = () => `
|
| 23 |
-
<div class="trix-button-row">
|
| 24 |
-
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
|
| 25 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" tabindex="-1" title="Bold">Bold</button>
|
| 26 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" tabindex="-1" title="Italic">Italic</button>
|
| 27 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" tabindex="-1" title="Strikethrough">Strikethrough</button>
|
| 28 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" tabindex="-1" title="Link">Link</button>
|
| 29 |
-
</span>
|
| 30 |
-
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
|
| 31 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-heading-1" data-trix-attribute="heading1" tabindex="-1" title="Heading">Heading</button>
|
| 32 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-quote" data-trix-attribute="quote" tabindex="-1" title="Quote">Quote</button>
|
| 33 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-code" data-trix-attribute="code" tabindex="-1" title="Code">Code</button>
|
| 34 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" tabindex="-1" title="Bullets">Bullets</button>
|
| 35 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" tabindex="-1" title="Numbers">Numbers</button>
|
| 36 |
-
</span>
|
| 37 |
-
<span class="trix-button-group trix-button-group--file-tools" data-trix-button-group="file-tools">
|
| 38 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-attach" data-trix-action="attachFiles" tabindex="-1" title="Attach Files">Attach Files</button>
|
| 39 |
-
</span>
|
| 40 |
-
<span class="trix-button-group trix-button-group--image-tools" data-trix-button-group="image-tools">
|
| 41 |
-
<button type="button" class="trix-button" data-trix-action="x-align-left" tabindex="-1" title="Align Left">Align Left</button>
|
| 42 |
-
<button type="button" class="trix-button" data-trix-action="x-align-right" tabindex="-1" title="Align Right">Align Right</button>
|
| 43 |
-
</span>
|
| 44 |
-
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
|
| 45 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" tabindex="-1" title="Undo">Undo</button>
|
| 46 |
-
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" tabindex="-1" title="Redo">Redo</button>
|
| 47 |
-
</span>
|
| 48 |
-
</div>
|
| 49 |
-
<div class="trix-dialogs" data-trix-dialogs>
|
| 50 |
-
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
|
| 51 |
-
<div class="trix-dialog__link-fields">
|
| 52 |
-
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="Enter a URL..." aria-label="URL" required data-trix-input>
|
| 53 |
-
<div class="trix-button_group">
|
| 54 |
-
<input type="button" class="trix-button trix-button--dialog" value="Link" data-trix-method="setAttribute">
|
| 55 |
-
<input type="button" class="trix-button trix-button--dialog" value="Unlink" data-trix-method="removeAttribute">
|
| 56 |
-
</div>
|
| 57 |
-
</div>
|
| 58 |
-
</div>
|
| 59 |
-
</div>
|
| 60 |
-
`;
|
| 61 |
-
});
|
| 62 |
-
</script>
|
| 63 |
</head>
|
| 64 |
<body>
|
| 65 |
<h1>Nouvel Article</h1>
|
|
@@ -78,44 +33,164 @@
|
|
| 78 |
<label for="icon">Image (fichier image):</label>
|
| 79 |
<input type="file" id="icon" name="icon" accept="image/*">
|
| 80 |
|
|
|
|
|
|
|
|
|
|
| 81 |
<label for="content">Contenu:</label>
|
| 82 |
<input id="content" type="hidden" name="content">
|
| 83 |
<trix-editor input="content"></trix-editor>
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
<button type="submit">Créer</button>
|
| 86 |
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
<script>
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
});
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
});
|
| 117 |
}
|
|
|
|
| 118 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
</script>
|
| 120 |
</body>
|
| 121 |
</html>
|
|
|
|
| 7 |
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/trix.umd.min.js"></script>
|
| 8 |
<style>
|
| 9 |
body { font-family: Arial, sans-serif; margin: 20px; }
|
| 10 |
+
form { max-width: 900px; margin: 0 auto; }
|
| 11 |
label { display: block; margin-top: 10px; }
|
| 12 |
+
input, select, button { width: 100%; padding: 10px; margin-top: 5px; box-sizing: border-box; }
|
| 13 |
+
trix-editor { border: 1px solid #e9ecef; min-height: 300px; border-radius: 8px; }
|
| 14 |
+
#editor-preview { margin-top: 1rem; }
|
| 15 |
+
.article-preview { background: #fff; padding: 1rem; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
|
| 16 |
+
.article-preview h1 { font-size: 1.4rem; margin-top: 0; }
|
| 17 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
</head>
|
| 19 |
<body>
|
| 20 |
<h1>Nouvel Article</h1>
|
|
|
|
| 33 |
<label for="icon">Image (fichier image):</label>
|
| 34 |
<input type="file" id="icon" name="icon" accept="image/*">
|
| 35 |
|
| 36 |
+
<label for="youtube_url">Lien YouTube (optionnel) :</label>
|
| 37 |
+
<input type="url" id="youtube_url" name="youtube_url" placeholder="https://www.youtube.com/watch?v=...">
|
| 38 |
+
|
| 39 |
<label for="content">Contenu:</label>
|
| 40 |
<input id="content" type="hidden" name="content">
|
| 41 |
<trix-editor input="content"></trix-editor>
|
| 42 |
|
| 43 |
+
<p style="font-size:0.9rem;color:#555;margin-top:8px;">Pour insérer une vidéo à un emplacement précis, utilisez le shortcode : <code>[youtube:URL]</code> (ex. <code>[youtube:https://www.youtube.com/watch?v=dQw4w9WgXcQ]</code>).</p>
|
| 44 |
+
<p style="font-size:0.9rem;color:#555;margin-top:6px;">Pour insérer une image à un emplacement précis, utilisez le shortcode : <code>[image:URL|opts]</code> où <code>opts</code> peut contenir <code>left</code>, <code>right</code>, <code>center</code>, <code>width=300</code>, <code>height=200</code>. Exemples :</p>
|
| 45 |
+
<ul style="color:#555;margin-top:0;margin-bottom:1rem;">
|
| 46 |
+
<li><code>[image:https://.../img.jpg|left|width=200]</code></li>
|
| 47 |
+
<li><code>[image:https://.../img.jpg|center|width=80%]</code></li>
|
| 48 |
+
</ul>
|
| 49 |
+
|
| 50 |
<button type="submit">Créer</button>
|
| 51 |
</form>
|
| 52 |
+
|
| 53 |
+
<h3>Aperçu</h3>
|
| 54 |
+
<div id="editor-preview" class="article-preview"></div>
|
| 55 |
+
|
| 56 |
<script>
|
| 57 |
+
// Configure Trix toolbar and allowed tags before initialization
|
| 58 |
+
document.addEventListener('trix-before-initialize', function(){
|
| 59 |
+
// Allow iframes and basic attributes via dompurify config
|
| 60 |
+
if (window.Trix && Trix.config && Trix.config.dompurify){
|
| 61 |
+
Trix.config.dompurify.ADD_TAGS = ["iframe","video","source","table","tr","td","th","img"];
|
| 62 |
+
Trix.config.dompurify.ADD_ATTR = ["src","controls","width","height","class","style","allowfullscreen"];
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Simpler custom toolbar (keeps Trix defaults but adds headings/quote)
|
| 66 |
+
Trix.config.toolbar.getDefaultHTML = function(){
|
| 67 |
+
return `
|
| 68 |
+
<div class="trix-button-row">
|
| 69 |
+
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
|
| 70 |
+
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" title="Gras">Gras</button>
|
| 71 |
+
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" title="Italique">Italique</button>
|
| 72 |
+
<button type="button" class="trix-button" data-trix-action="x-heading" title="Titre">H</button>
|
| 73 |
+
<button type="button" class="trix-button" data-trix-action="x-quote" title="Citation">"</button>
|
| 74 |
+
</span>
|
| 75 |
+
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
|
| 76 |
+
<button type="button" class="trix-button" data-trix-action="undo" title="Annuler">⟲</button>
|
| 77 |
+
<button type="button" class="trix-button" data-trix-action="redo" title="Refaire">⟲</button>
|
| 78 |
+
</span>
|
| 79 |
+
</div>`;
|
| 80 |
+
};
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
// Render shortcodes to HTML for preview
|
| 84 |
+
function renderShortcodesForPreview(html){
|
| 85 |
+
if(!html) return '';
|
| 86 |
+
// YouTube
|
| 87 |
+
html = html.replace(/\[youtube:([^\]]+)\]/g, function(_, url){
|
| 88 |
+
try{
|
| 89 |
+
var parsed = new URL(url.trim());
|
| 90 |
+
var id = null;
|
| 91 |
+
if(parsed.hostname.includes('youtu.be')) id = parsed.pathname.slice(1);
|
| 92 |
+
else if(parsed.hostname.includes('youtube')) id = parsed.searchParams.get('v') || parsed.pathname.split('/').pop();
|
| 93 |
+
if(!id) return '<pre>'+url+'</pre>';
|
| 94 |
+
return '<div class="video-player"><iframe src="https://www.youtube.com/embed/'+id+'" allowfullscreen></iframe></div>';
|
| 95 |
+
}catch(e){
|
| 96 |
+
return '<pre>'+url+'</pre>';
|
| 97 |
}
|
| 98 |
});
|
| 99 |
+
// Image
|
| 100 |
+
html = html.replace(/\[image:([^\]|]+)(?:\|([^\]]+))?\]/g, function(_, url, opts){
|
| 101 |
+
var classes = '';
|
| 102 |
+
var style = '';
|
| 103 |
+
if(opts){
|
| 104 |
+
opts.split(/[|,]/).forEach(function(o){
|
| 105 |
+
o = o.trim();
|
| 106 |
+
if(/^(left|right|center)$/.test(o)){
|
| 107 |
+
if(o==='left') classes='float-left';
|
| 108 |
+
if(o==='right') classes='float-right';
|
| 109 |
+
if(o==='center') style='display:block;margin-left:auto;margin-right:auto';
|
| 110 |
+
} else if(/^width=/.test(o)){
|
| 111 |
+
style += (style?';':'') + 'width:' + o.split('=')[1];
|
| 112 |
+
} else if(/^height=/.test(o)){
|
| 113 |
+
style += (style?';':'') + 'height:' + o.split('=')[1];
|
| 114 |
}
|
| 115 |
});
|
| 116 |
}
|
| 117 |
+
return '<img src="'+url.trim()+'" class="'+classes+'" style="'+style+'"/>';
|
| 118 |
});
|
| 119 |
+
return html;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Upload helper
|
| 123 |
+
function uploadFileToServer(fileBlob, filename, attachment){
|
| 124 |
+
var formData = new FormData();
|
| 125 |
+
formData.append('file', fileBlob, filename);
|
| 126 |
+
fetch('/upload', { method:'POST', body: formData }).then(function(res){ return res.json(); }).then(function(data){
|
| 127 |
+
if(data.url) attachment.setAttributes({ url: data.url });
|
| 128 |
+
}).catch(function(err){ console.error('Upload failed', err); });
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Handle attachments (drag/drop or paste into Trix)
|
| 132 |
+
document.addEventListener('trix-attachment-add', function(event){
|
| 133 |
+
var attachment = event.attachment;
|
| 134 |
+
var file = attachment.file;
|
| 135 |
+
if(!file) return;
|
| 136 |
+
if(!file.type.startsWith('image/')){ uploadFileToServer(file, file.name, attachment); return; }
|
| 137 |
+
|
| 138 |
+
var choice = prompt("Redimensionner l'image ? Entrez largeur en px (ex:800) ou % (ex:80%). Laisser vide pour taille originale. Annuler pour ne pas uploader.");
|
| 139 |
+
if(choice === null) return; // canceled
|
| 140 |
+
if(!choice){ uploadFileToServer(file, file.name, attachment); return; }
|
| 141 |
+
|
| 142 |
+
var reader = new FileReader();
|
| 143 |
+
reader.onload = function(e){
|
| 144 |
+
var img = new Image();
|
| 145 |
+
img.onload = function(){
|
| 146 |
+
var origW = img.naturalWidth, origH = img.naturalHeight;
|
| 147 |
+
var targetW = origW;
|
| 148 |
+
var c = choice.toString().trim();
|
| 149 |
+
if(c.endsWith('%')){
|
| 150 |
+
var p = parseFloat(c.replace('%',''));
|
| 151 |
+
if(!isNaN(p)) targetW = Math.round(origW * p / 100);
|
| 152 |
+
} else {
|
| 153 |
+
var px = parseInt(c,10);
|
| 154 |
+
if(!isNaN(px)) targetW = px;
|
| 155 |
+
}
|
| 156 |
+
if(targetW <= 0 || targetW === origW){ uploadFileToServer(file, file.name, attachment); return; }
|
| 157 |
+
var scale = targetW / origW; var targetH = Math.round(origH * scale);
|
| 158 |
+
var canvas = document.createElement('canvas'); canvas.width = targetW; canvas.height = targetH;
|
| 159 |
+
var ctx = canvas.getContext('2d'); ctx.drawImage(img,0,0,targetW,targetH);
|
| 160 |
+
canvas.toBlob(function(blob){ uploadFileToServer(blob, file.name, attachment); }, file.type || 'image/jpeg', 0.92);
|
| 161 |
+
};
|
| 162 |
+
img.src = e.target.result;
|
| 163 |
+
};
|
| 164 |
+
reader.readAsDataURL(file);
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
// alignment helper via custom action hooks
|
| 168 |
+
document.addEventListener('trix-action-invoke', function(event){
|
| 169 |
+
var action = event.actionName || event.detail && event.detail.actionName;
|
| 170 |
+
// some Trix builds use different event payloads; we handle common case above
|
| 171 |
+
if(action === 'x-align-left' || action === 'x-align-right'){
|
| 172 |
+
var target = event.target || event.detail && event.detail.target; if(!target) return;
|
| 173 |
+
var editor = target.editor;
|
| 174 |
+
var selectedRange = editor.getSelectedRange();
|
| 175 |
+
var doc = editor.getDocument();
|
| 176 |
+
var attachments = doc.getAttachments();
|
| 177 |
+
attachments.forEach(function(att){ var range = att.getRange(); if(selectedRange[0] <= range[0] && range[1] <= selectedRange[1]){ att.setAttribute('class', action === 'x-align-left' ? 'float-left' : 'float-right'); }});
|
| 178 |
+
}
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
// Live preview update
|
| 182 |
+
document.addEventListener('trix-change', function(){
|
| 183 |
+
var contentInput = document.querySelector('input[name="content"]');
|
| 184 |
+
var preview = document.getElementById('editor-preview');
|
| 185 |
+
if(preview && contentInput) preview.innerHTML = renderShortcodesForPreview(contentInput.value);
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
+
// initialize preview on load
|
| 189 |
+
document.addEventListener('DOMContentLoaded', function(){
|
| 190 |
+
var contentInput = document.querySelector('input[name="content"]');
|
| 191 |
+
var preview = document.getElementById('editor-preview');
|
| 192 |
+
if(preview && contentInput) preview.innerHTML = renderShortcodesForPreview(contentInput.value);
|
| 193 |
+
});
|
| 194 |
</script>
|
| 195 |
</body>
|
| 196 |
</html>
|
templates/article.html
CHANGED
|
@@ -16,20 +16,26 @@
|
|
| 16 |
</ol>
|
| 17 |
</nav>
|
| 18 |
|
| 19 |
-
<div class="article-content">
|
| 20 |
-
<h1>{{ article.title }}</h1>
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
{% endblock %}
|
|
|
|
| 16 |
</ol>
|
| 17 |
</nav>
|
| 18 |
|
| 19 |
+
<div class="article-content">
|
| 20 |
+
<h1>{{ article.title }}</h1>
|
| 21 |
+
{% set embed = article.youtube_url | youtube_embed %}
|
| 22 |
+
{% if embed %}
|
| 23 |
+
<div class="video-player" style="margin-bottom: 1rem;">
|
| 24 |
+
<iframe width="100%" height="480" src="{{ embed }}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
| 25 |
+
</div>
|
| 26 |
+
{% endif %}
|
| 27 |
+
<div class="trix-content">
|
| 28 |
+
{{ (article.content | render_embeds) | safe }}
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<script>
|
| 33 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 34 |
+
document.querySelectorAll('.trix-content img').forEach(img => {
|
| 35 |
+
img.addEventListener('click', () => {
|
| 36 |
+
window.open(img.src, '_blank');
|
| 37 |
+
});
|
| 38 |
+
});
|
| 39 |
+
});
|
| 40 |
+
</script>
|
| 41 |
{% endblock %}
|
templates/articles.html
CHANGED
|
@@ -12,14 +12,12 @@
|
|
| 12 |
</nav>
|
| 13 |
|
| 14 |
<h2 class="mb-4">Articles pour {{ category.name }}</h2>
|
| 15 |
-
<div class="
|
| 16 |
{% for article in articles %}
|
| 17 |
-
<div class="
|
| 18 |
-
<div class="card"
|
| 19 |
-
<
|
| 20 |
-
|
| 21 |
-
<p class="card-text text-muted">Cliquez pour lire l'article</p>
|
| 22 |
-
</div>
|
| 23 |
</div>
|
| 24 |
</div>
|
| 25 |
{% endfor %}
|
|
|
|
| 12 |
</nav>
|
| 13 |
|
| 14 |
<h2 class="mb-4">Articles pour {{ category.name }}</h2>
|
| 15 |
+
<div class="cards-grid">
|
| 16 |
{% for article in articles %}
|
| 17 |
+
<div class="card" onclick="window.location.href='/articles/{{ article.id }}'" style="cursor:pointer;">
|
| 18 |
+
<div class="card-body">
|
| 19 |
+
<h5 class="card-title">{{ article.title }}</h5>
|
| 20 |
+
<p class="card-text text-muted">Cliquez pour lire</p>
|
|
|
|
|
|
|
| 21 |
</div>
|
| 22 |
</div>
|
| 23 |
{% endfor %}
|
templates/base.html
CHANGED
|
@@ -19,7 +19,7 @@
|
|
| 19 |
|
| 20 |
<nav class="navbar">
|
| 21 |
<div class="container">
|
| 22 |
-
<ul class="nav">
|
| 23 |
<li class="nav-item">
|
| 24 |
<a class="nav-link" href="{{ url_for('home') }}">Accueil</a>
|
| 25 |
</li>
|
|
|
|
| 19 |
|
| 20 |
<nav class="navbar">
|
| 21 |
<div class="container">
|
| 22 |
+
<ul class="nav nav-links">
|
| 23 |
<li class="nav-item">
|
| 24 |
<a class="nav-link" href="{{ url_for('home') }}">Accueil</a>
|
| 25 |
</li>
|
templates/home.html
CHANGED
|
@@ -4,16 +4,14 @@
|
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
<h2 class="mb-4">Choisissez une matière</h2>
|
| 7 |
-
<div class="
|
| 8 |
{% for subject in subjects %}
|
| 9 |
-
<div class="
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
<h5 class="card-title">{{ subject.name }}</h5>
|
| 16 |
-
</div>
|
| 17 |
</div>
|
| 18 |
</div>
|
| 19 |
{% endfor %}
|
|
|
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
<h2 class="mb-4">Choisissez une matière</h2>
|
| 7 |
+
<div class="cards-grid">
|
| 8 |
{% for subject in subjects %}
|
| 9 |
+
<div class="card" onclick="window.location.href='/subjects/{{ subject.id }}/categories'" style="cursor:pointer;">
|
| 10 |
+
{% if subject.icon_url %}
|
| 11 |
+
<img src="{{ subject.icon_url }}" alt="{{ subject.name }}" class="card-img-top" style="width:80px;height:80px;border-radius:50%;object-fit:cover;margin:1rem auto 0;display:block;">
|
| 12 |
+
{% endif %}
|
| 13 |
+
<div class="card-body text-center">
|
| 14 |
+
<h5 class="card-title">{{ subject.name }}</h5>
|
|
|
|
|
|
|
| 15 |
</div>
|
| 16 |
</div>
|
| 17 |
{% endfor %}
|