mirror of
https://github.com/spchcap/speech.capital.git
synced 2026-01-13 16:18:06 +00:00
Refactor: Use a single Turnstile widget for all comments
This commit is contained in:
@@ -95,7 +95,7 @@
|
||||
<form @submit.prevent="comment(null,$event)" class="mb-6" x-show="user"><textarea x-model="txt" rows="4" class="w-full border border-gray-200 rounded px-3 py-2 text-sm" placeholder="Add a comment..."></textarea><div class="cf-turnstile mt-2" data-sitekey="0x4AAAAAAB4klN__r6wwJXs4"></div><button type="submit" :disabled="commenting" class="bg-gray-900 text-white px-4 py-2 rounded text-sm hover:bg-gray-800 mt-2" x-text="commenting?'Moderating...':'Comment'"></button><span x-show="commentSuccess" class="text-green-600 text-sm ml-2">Approved!</span></form>
|
||||
<div x-show="!user" class="text-sm text-gray-500 mb-6"><a href="https://speech.capital/login" class="text-blue-600 hover:underline">Log in</a> to comment.</div>
|
||||
<div class="space-y-4">
|
||||
<template x-for="c in comments" :key="c.id"><div class="comment"><div class="text-xs text-gray-500 mb-1"><a :href="'https://speech.capital/'+c.username" class="font-medium hover:underline" x-text="c.username"></a> · <span x-text="ago(c.created_at)"></span><template x-if="isMod()"><span class="text-gray-400"> · <button @click="moderate('comment',c)" class="hover:text-red-600">delete</button> · <button @click="banUser(c.user_id,c.username)" class="hover:text-red-600">ban</button></span></template></div><p class="text-sm whitespace-pre-wrap mb-2" x-text="c.content"></p><div class="flex items-center gap-3 text-xs"><button @click="vote(c,1,true)" :class="c.voted===1?'text-gray-900':'text-gray-400'" class="hover:text-gray-900">▲</button><span x-text="c.score"></span><button @click="reply=c.id" class="text-blue-600 hover:underline" x-show="user">reply</button></div><form x-show="reply===c.id" @submit.prevent="comment(c.id,$event)" class="mt-2"><textarea x-model="txt" rows="3" class="w-full border border-gray-200 rounded px-3 py-2 text-sm"></textarea><div class="cf-turnstile mt-2" data-sitekey="0x4AAAAAAB4klN__r6wwJXs4"></div><button :disabled="commenting" type="submit" class="bg-gray-900 text-white px-3 py-1 rounded text-xs hover:bg-gray-800 mt-1" x-text="commenting?'Moderating...':'Reply'"></button><button type="button" @click="reply=null;txt=''" class="text-gray-600 px-3 py-1 text-xs">Cancel</button></form><div x-show="c.replies?.length" class="mt-3 space-y-3"><template x-for="r in c.replies" :key="r.id"><div class="comment"><div class="text-xs text-gray-500 mb-1"><a :href="'https://speech.capital/'+r.username" class="font-medium hover:underline" x-text="r.username"></a> · <span x-text="ago(r.created_at)"></span><template x-if="isMod()"><span class="text-gray-400"> · <button @click="moderate('comment',r)" class="hover:text-red-600">delete</button> · <button @click="banUser(r.user_id,r.username)" class="hover:text-red-600">ban</button></span></template></div><p class="text-sm whitespace-pre-wrap mb-2" x-text="r.content"></p></div></template></div></div></template>
|
||||
<template x-for="c in comments" :key="c.id"><div class="comment"><div class="text-xs text-gray-500 mb-1"><a :href="'https://speech.capital/'+c.username" class="font-medium hover:underline" x-text="c.username"></a> · <span x-text="ago(c.created_at)"></span><template x-if="isMod()"><span class="text-gray-400"> · <button @click="moderate('comment',c)" class="hover:text-red-600">delete</button> · <button @click="banUser(c.user_id,c.username)" class="hover:text-red-600">ban</button></span></template></div><p class="text-sm whitespace-pre-wrap mb-2" x-text="c.content"></p><div class="flex items-center gap-3 text-xs"><button @click="vote(c,1,true)" :class="c.voted===1?'text-gray-900':'text-gray-400'" class="hover:text-gray-900">▲</button><span x-text="c.score"></span><button @click="reply=c.id" class="text-blue-600 hover:underline" x-show="user">reply</button></div><form x-show="reply===c.id" @submit.prevent="comment(c.id,$event)" class="mt-2"><textarea x-model="txt" rows="3" class="w-full border border-gray-200 rounded px-3 py-2 text-sm"></textarea><button :disabled="commenting" type="submit" class="bg-gray-900 text-white px-3 py-1 rounded text-xs hover:bg-gray-800 mt-1" x-text="commenting?'Moderating...':'Reply'"></button><button type="button" @click="reply=null;txt=''" class="text-gray-600 px-3 py-1 text-xs">Cancel</button></form><div x-show="c.replies?.length" class="mt-3 space-y-3"><template x-for="r in c.replies" :key="r.id"><div class="comment"><div class="text-xs text-gray-500 mb-1"><a :href="'https://speech.capital/'+r.username" class="font-medium hover:underline" x-text="r.username"></a> · <span x-text="ago(r.created_at)"></span><template x-if="isMod()"><span class="text-gray-400"> · <button @click="moderate('comment',r)" class="hover:text-red-600">delete</button> · <button @click="banUser(r.user_id,r.username)" class="hover:text-red-600">ban</button></span></template></div><p class="text-sm whitespace-pre-wrap mb-2" x-text="r.content"></p></div></template></div></div></template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
f=()=>({view:'list',user:null,posts:[],post:null,comments:[],txt:'',reply:null,md:null,commenting:false,commentSuccess:false,profile:{user:null,comments:[],posts:[]},loading:false,init(){this.md=window.markdownit({highlight:(s,l)=>{if(l&&hljs.getLanguage(l))try{return hljs.highlight(s,{language:l,ignoreIllegals:!0}).value}catch(e){}return''}});fetch('https://speech.capital/api/user',{credentials:'include'}).then(r=>r.json()).then(d=>{if(d.user){this.user=d.user;const s=document.getElementById('signup-link'),a=document.getElementById('auth-links'),u=document.getElementById('username-link');s.style.display='none';u.textContent=d.user.username;u.href=`https://speech.capital/${d.user.username}`;a.style.display='flex'}});const host=window.location.hostname,parts=host.split('.'),path=location.pathname;if(parts.length<=2){const p=path.replace(/\/$/,''),uMatch=p.match(/^\/([a-zA-Z0-9_-]+)$/);if(uMatch&&!['submit','login','signup','admin'].includes(uMatch[1])){this.view='user';this.loadUser(uMatch[1])}else{this.view='home'}}else{document.getElementById('subdomain').textContent=parts[0]+'.';const pMatch=path.match(/^\/(\d+)$/);if(pMatch){this.view='post';this.loadPost(pMatch[1])}else{this.view='list';this.loadList()}}},isMod(){return this.user&&['mod','admin','owner'].includes(this.user.role)},isImageLink(l){return l&&/\.(jpg|jpeg|png|gif|webp)$/i.test(l)},async moderate(type,item){if(!confirm(`Are you sure you want to delete this ${type}?`))return;const isPost=type==='post',body={action:isPost?'delete_post':'delete_comment',[isPost?'post_id':'comment_id']:item.id};try{const r=await fetch('https://speech.capital/api/moderate',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(!r.ok)throw new Error('Failed to delete');if(isPost){item.title='[Deleted]';item.content='[Deleted]';item.link=null}else{item.content='[Deleted]'}}catch(e){alert(e.message)}},async banUser(user_id,username){const days=prompt(`How many days to ban ${username}?`);if(!days||isNaN(parseInt(days)))return;const body={action:'ban_user',user_id,days:parseInt(days)};try{const r=await fetch('https://speech.capital/api/moderate',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(!r.ok){const d=await r.json();throw new Error(d.error||'Failed to ban user')}alert(`${username} banned for ${days} days.`)}catch(e){alert(e.message)}},async loadList(){const parts=window.location.hostname.split('.'),sub=parts.length>2?parts[0]:'free',sort=new URLSearchParams(location.search).get('sort')||'hot';const r=await fetch(`https://speech.capital/api/posts?sub=${sub}&sort=${sort}`,{credentials:'include'});this.posts=((await r.json()).posts||[]).map(p=>({...p,showImage:!1}))},async loadPost(id){const r=await fetch(`https://speech.capital/api/posts/${id}`,{credentials:'include'});const d=await r.json();this.post=d.post;this.comments=d.comments||[]},async loadUser(u){this.loading=!0;try{const r=await fetch(`/api/users/${u}`);if(!r.ok)throw'';this.profile=await r.json()}catch(e){this.profile={user:null,comments:[],posts:[]}}finally{this.loading=!1}},formatDate(d){return d?new Date(d.replace(' ','T')+'Z').toLocaleDateString():''},renderMarkdown(t){return this.md.render(t||'')},async vote(obj,dir,isComment=false){try{const body=isComment?{comment_id:obj.id,direction:dir}:{post_id:obj.id,direction:dir};const r=await fetch('https://speech.capital/api/vote',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(!r.ok)throw new Error();const{score,voted}=await r.json();obj.score=score;obj.voted=voted}catch(e){}},async comment(parent_id,ev){if(!this.txt.trim()||this.commenting)return;this.commenting=!0;const form=ev.target,token=form.querySelector('[name="cf-turnstile-response"]')?.value;if(!token){alert('Please complete the CAPTCHA.');this.commenting=!1;return}try{const r=await fetch('https://speech.capital/api/comments',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({post_id:this.post.id,parent_id,content:this.txt,'cf-turnstile-response':token})});if(!r.ok){const d=await r.json();throw new Error(d.error?.message||'Failed to comment')}this.txt='';this.reply=null;this.commentSuccess=!0;setTimeout(()=>this.commentSuccess=!1,3000);this.loadPost(this.post.id)}catch(e){alert(e.message)}finally{this.commenting=!1;turnstile.reset(form.querySelector('.cf-turnstile'))}},domain(p){if(p.link){try{return new URL(p.link).hostname}catch{}}return p.link?'link':'self'},ago(d){if(!d)return'';const s=Math.floor((new Date()-new Date(d.replace(' ','T')+'Z'))/1000);if(s<60)return s+'s ago';if(s<3600)return Math.floor(s/60)+'m ago';if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago'}})
|
||||
f=()=>({view:'list',user:null,posts:[],post:null,comments:[],txt:'',reply:null,md:null,commenting:false,commentSuccess:false,profile:{user:null,comments:[],posts:[]},loading:false,init(){this.md=window.markdownit({highlight:(s,l)=>{if(l&&hljs.getLanguage(l))try{return hljs.highlight(s,{language:l,ignoreIllegals:!0}).value}catch(e){}return''}});fetch('https://speech.capital/api/user',{credentials:'include'}).then(r=>r.json()).then(d=>{if(d.user){this.user=d.user;const s=document.getElementById('signup-link'),a=document.getElementById('auth-links'),u=document.getElementById('username-link');s.style.display='none';u.textContent=d.user.username;u.href=`https://speech.capital/${d.user.username}`;a.style.display='flex'}});const host=window.location.hostname,parts=host.split('.'),path=location.pathname;if(parts.length<=2){const p=path.replace(/\/$/,''),uMatch=p.match(/^\/([a-zA-Z0-9_-]+)$/);if(uMatch&&!['submit','login','signup','admin'].includes(uMatch[1])){this.view='user';this.loadUser(uMatch[1])}else{this.view='home'}}else{document.getElementById('subdomain').textContent=parts[0]+'.';const pMatch=path.match(/^\/(\d+)$/);if(pMatch){this.view='post';this.loadPost(pMatch[1])}else{this.view='list';this.loadList()}}},isMod(){return this.user&&['mod','admin','owner'].includes(this.user.role)},isImageLink(l){return l&&/\.(jpg|jpeg|png|gif|webp)$/i.test(l)},async moderate(type,item){if(!confirm(`Are you sure you want to delete this ${type}?`))return;const isPost=type==='post',body={action:isPost?'delete_post':'delete_comment',[isPost?'post_id':'comment_id']:item.id};try{const r=await fetch('https://speech.capital/api/moderate',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(!r.ok)throw new Error('Failed to delete');if(isPost){item.title='[Deleted]';item.content='[Deleted]';item.link=null}else{item.content='[Deleted]'}}catch(e){alert(e.message)}},async banUser(user_id,username){const days=prompt(`How many days to ban ${username}?`);if(!days||isNaN(parseInt(days)))return;const body={action:'ban_user',user_id,days:parseInt(days)};try{const r=await fetch('https://speech.capital/api/moderate',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(!r.ok){const d=await r.json();throw new Error(d.error||'Failed to ban user')}alert(`${username} banned for ${days} days.`)}catch(e){alert(e.message)}},async loadList(){const parts=window.location.hostname.split('.'),sub=parts.length>2?parts[0]:'free',sort=new URLSearchParams(location.search).get('sort')||'hot';const r=await fetch(`https://speech.capital/api/posts?sub=${sub}&sort=${sort}`,{credentials:'include'});this.posts=((await r.json()).posts||[]).map(p=>({...p,showImage:!1}))},async loadPost(id){const r=await fetch(`https://speech.capital/api/posts/${id}`,{credentials:'include'});const d=await r.json();this.post=d.post;this.comments=d.comments||[]},async loadUser(u){this.loading=!0;try{const r=await fetch(`/api/users/${u}`);if(!r.ok)throw'';this.profile=await r.json()}catch(e){this.profile={user:null,comments:[],posts:[]}}finally{this.loading=!1}},formatDate(d){return d?new Date(d.replace(' ','T')+'Z').toLocaleDateString():''},renderMarkdown(t){return this.md.render(t||'')},async vote(obj,dir,isComment=false){try{const body=isComment?{comment_id:obj.id,direction:dir}:{post_id:obj.id,direction:dir};const r=await fetch('https://speech.capital/api/vote',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(!r.ok)throw new Error();const{score,voted}=await r.json();obj.score=score;obj.voted=voted}catch(e){}},async comment(parent_id,ev){if(!this.txt.trim()||this.commenting)return;this.commenting=!0;const token=document.querySelector('[name="cf-turnstile-response"]')?.value;if(!token){alert('Please complete the CAPTCHA.');this.commenting=!1;return}try{const r=await fetch('https://speech.capital/api/comments',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({post_id:this.post.id,parent_id,content:this.txt,'cf-turnstile-response':token})});if(!r.ok){const d=await r.json();throw new Error(d.error?.message||'Failed to comment')}this.txt='';this.reply=null;this.commentSuccess=!0;setTimeout(()=>this.commentSuccess=!1,3000);this.loadPost(this.post.id)}catch(e){alert(e.message)}finally{this.commenting=!1;turnstile.reset(document.querySelector('.cf-turnstile'))}},domain(p){if(p.link){try{return new URL(p.link).hostname}catch{}}return p.link?'link':'self'},ago(d){if(!d)return'';const s=Math.floor((new Date()-new Date(d.replace(' ','T')+'Z'))/1000);if(s<60)return s+'s ago';if(s<3600)return Math.floor(s/60)+'m ago';if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago'}})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user