Docfile commited on
Commit
3c43094
·
verified ·
1 Parent(s): 7acb0e7

Upload 16 files

Browse files
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="table-responsive">
12
- <table class="table table-striped table-hover">
13
- <thead class="table-dark">
14
- <tr>
15
- <th>ID</th>
16
- <th>Titre</th>
17
- <th>Catégorie</th>
18
- <th>Actions</th>
19
- </tr>
20
- </thead>
21
- <tbody>
22
- {% for article in articles %}
23
- <tr>
24
- <td>{{ article.id }}</td>
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
- </td>
33
- </tr>
34
- {% endfor %}
35
- </tbody>
36
- </table>
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 l'Article</title>
6
  <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
7
  <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
8
  <style>
9
  body { font-family: Arial, sans-serif; margin: 20px; }
10
- form { max-width: 800px; }
11
  label { display: block; margin-top: 10px; }
12
- input, button { width: 100%; padding: 10px; margin-top: 5px; }
13
- trix-editor { border: 1px solid #ccc; min-height: 300px; }
 
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 l'Article</h1>
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
- {% if article.icon_data %}
72
- <p>Image actuelle: <img src="{{ url_for('get_article_image', article_id=article.id) }}" style="max-width: 100px; max-height: 100px;"></p>
73
- {% endif %}
 
 
 
74
 
75
- <label for="icon">Nouvelle image (fichier image):</label>
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">Sauvegarder</button>
83
  </form>
 
 
 
 
84
  <script>
85
- document.addEventListener("trix-attachment-add", function(event) {
86
- var file = event.attachment.file;
87
- if (file) {
88
- var formData = new FormData();
89
- formData.append("file", file);
90
- fetch("/upload", {
91
- method: "POST",
92
- body: formData
93
- }).then(response => response.json()).then(data => {
94
- event.attachment.setAttributes({url: data.url});
95
- }).catch(error => {
96
- console.error("Upload failed:", error);
97
- });
98
- }
99
- });
 
 
 
100
 
101
- document.addEventListener("trix-action-invoke", function(event) {
102
- const { actionName, target } = event;
103
- if (actionName === "x-align-left" || actionName === "x-align-right") {
104
- const editor = target.editor;
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/trix@2.0.8/dist/trix.css">
7
  <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/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/trix@2.0.8/dist/trix.umd.min.js"></script>
8
  <style>
9
  body { font-family: Arial, sans-serif; margin: 20px; }
10
- form { max-width: 800px; }
11
  label { display: block; margin-top: 10px; }
12
- input, select, button { width: 100%; padding: 10px; margin-top: 5px; }
13
- trix-editor { border: 1px solid #ccc; min-height: 300px; }
 
 
 
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
- document.addEventListener("trix-attachment-add", function(event) {
89
- var file = event.attachment.file;
90
- if (file) {
91
- var formData = new FormData();
92
- formData.append("file", file);
93
- fetch("/upload", {
94
- method: "POST",
95
- body: formData
96
- }).then(response => response.json()).then(data => {
97
- event.attachment.setAttributes({url: data.url});
98
- }).catch(error => {
99
- console.error("Upload failed:", error);
100
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
  });
103
-
104
- document.addEventListener("trix-action-invoke", function(event) {
105
- const { actionName, target } = event;
106
- if (actionName === "x-align-left" || actionName === "x-align-right") {
107
- const editor = target.editor;
108
- const selectedRange = editor.getSelectedRange();
109
- const document = editor.getDocument();
110
- const attachments = document.getAttachments();
111
- attachments.forEach(attachment => {
112
- const range = attachment.getRange();
113
- if (selectedRange[0] <= range[0] && range[1] <= selectedRange[1]) {
114
- attachment.setAttribute("class", actionName === "x-align-left" ? "float-left" : "float-right");
 
 
 
115
  }
116
  });
117
  }
 
118
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  </script>
120
  </body>
121
  </html>
 
7
  <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/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
- <div class="trix-content">
22
- {{ article.content | safe }}
23
- </div>
24
- </div>
25
-
26
- <script>
27
- document.addEventListener('DOMContentLoaded', function() {
28
- document.querySelectorAll('.trix-content img').forEach(img => {
29
- img.addEventListener('click', () => {
30
- window.open(img.src, '_blank');
31
- });
32
- });
33
- });
34
- </script>
 
 
 
 
 
 
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="row">
16
  {% for article in articles %}
17
- <div class="col-12 mb-3">
18
- <div class="card" onclick="window.location.href='/articles/{{ article.id }}'" style="cursor: pointer;">
19
- <div class="card-body">
20
- <h5 class="card-title">{{ article.title }}</h5>
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="row">
8
  {% for subject in subjects %}
9
- <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3">
10
- <div class="card h-100" onclick="window.location.href='/subjects/{{ subject.id }}/categories'" style="cursor: pointer;">
11
- <div class="card-body text-center">
12
- {% if subject.icon_url %}
13
- <img src="{{ subject.icon_url }}" alt="{{ subject.name }}" class="mb-3" style="width: 60px; height: 60px; object-fit: cover; border-radius: 50%;">
14
- {% endif %}
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 %}