feat(blog): in-content image carousel/slider
CI/CD / CI · dotnet build (push) Successful in 6m28s
CI/CD / Deploy · drsousan (push) Successful in 29s

Editor: new 🎠 اسلایدر toolbar button — pick multiple images (min 2),
uploads them all, inserts a <div class="post-carousel" data-carousel>
block at the cursor. Editor preview shows a tidy filmstrip with the
non-functional arrows/dots hidden.

Public post page: carousel CSS (scroll-snap track) + JS that wires up
prev/next arrows, clickable dots, and native touch swipe. Single-image
blocks auto-collapse their controls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 11:55:33 +03:30
parent 9c93b4e51a
commit 99a54be3ac
2 changed files with 79 additions and 0 deletions
+39
View File
@@ -184,6 +184,12 @@ tr:hover td{background:#FAFBFC}
.editor-toolbar button{background:none;border:none;padding:.3rem .5rem;border-radius:4px;cursor:pointer;font-size:.82rem;font-family:inherit;color:var(--mid)}
.editor-toolbar button:hover{background:var(--gold-pale);color:var(--gold)}
.editor-content{border:1.5px solid var(--border);border-radius:0 0 8px 8px;padding:.8rem 1rem;min-height:250px;outline:none;font-size:.9rem;line-height:1.8;direction:rtl}
.editor-content img{max-width:100%;height:auto;border-radius:8px;margin:.6rem 0}
/* Carousel preview inside the editor: tidy filmstrip, controls hidden */
.editor-content .post-carousel{border:1px dashed var(--gold);border-radius:10px;padding:5px;background:#faf7f0;margin:.8rem 0}
.editor-content .post-carousel .pc-track{display:flex;gap:6px;overflow-x:auto}
.editor-content .post-carousel .pc-track img{flex:0 0 46%;max-height:170px;object-fit:cover;border-radius:6px;margin:0}
.editor-content .post-carousel .pc-prev,.editor-content .post-carousel .pc-next,.editor-content .post-carousel .pc-dots{display:none}
/* ── Pages ── */
.page{display:none}
@@ -1028,6 +1034,7 @@ tr:hover td{background:#FAFBFC}
<button onclick="fmt('insertOrderedList')">۱. لیست</button>
<button onclick="insLink()">🔗 لینک</button>
<button onclick="insImage()" title="درج تصویر در متن">🖼 تصویر</button>
<button onclick="insCarousel()" title="درج اسلایدر چند تصویر">🎠 اسلایدر</button>
</div>
<div class="editor-content" id="post-content" contenteditable="true" dir="rtl"></div>
</div>
@@ -2270,6 +2277,38 @@ function insImage(){
};
fileInput.click();
}
// Insert a multi-image carousel/slider into the post content
function insCarousel(){
const editor=document.getElementById('post-content');
editor.focus();
const sel=window.getSelection();
const savedRange=sel.rangeCount?sel.getRangeAt(0):null;
const fileInput=document.createElement('input');
fileInput.type='file';
fileInput.accept='image/jpeg,image/png,image/webp,image/gif';
fileInput.multiple=true;
fileInput.onchange=async()=>{
const files=[...fileInput.files];if(!files.length)return;
if(files.length<2){toast('برای اسلایدر حداقل ۲ تصویر انتخاب کنید','error');return;}
toast(`در حال آپلود ${files.length} تصویر...`);
try{
const urls=[];
for(const file of files){
const fd=new FormData();fd.append('file',file);
const r=await fetch('/api/upload',{method:'POST',headers:{'Authorization':`Bearer ${token}`},body:fd});
if(!r.ok)throw new Error(await r.text());
const {url}=await r.json();urls.push(url);
}
const imgs=urls.map(u=>`<img src="${u}" alt=""/>`).join('');
const html=`<div class="post-carousel" data-carousel><div class="pc-track">${imgs}</div><button class="pc-prev" type="button" contenteditable="false"></button><button class="pc-next" type="button" contenteditable="false"></button><div class="pc-dots" contenteditable="false"></div></div><p><br></p>`;
editor.focus();
if(savedRange){sel.removeAllRanges();sel.addRange(savedRange);}
document.execCommand('insertHTML',false,html);
toast(`اسلایدر با ${urls.length} تصویر درج شد ✓`);
}catch(e){toast('خطا در آپلود: '+e.message,'error');}
};
fileInput.click();
}
// ── Modal helpers ─────────────────────────────────────────────────────────────
function closeModal(id){document.getElementById(id).classList.add('hidden');}