fix(gallery): implement voting JS + fix change-vote server path
Voting was completely broken: gallery.el referenced d8251f5e5aa1.js which was never written. Buttons rendered disabled with no JS to enable them, load vote state, or call /api/vote. Fix 1 — client: inline the voting script directly in gallery.el. Initializes Supabase client from window.NEURON_CFG, calls /api/vote-state/<id> on load (with JWT if signed in) to populate scores and active states, wires vote buttons with toggle logic (same direction = undo/none), handles sign-in modal with magic-link flow, re-loads all vote states on auth state change. Fix 2 — server: replace supabase_upsert_user (upsert via user JWT) with delete-then-insert. Upsert requires both INSERT + UPDATE RLS policies; the UPDATE policy is typically absent on share_votes. Delete (user JWT, RLS-safe) + insert (service key, user already auth-validated) is reliable for both new votes and vote changes.
This commit is contained in:
+107
-1
@@ -271,7 +271,113 @@ window.NEURON_CFG=window.NEURON_CFG||{};
|
||||
window.NEURON_CFG.supabase_url=\"" + supabase_url + "\";
|
||||
window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\";
|
||||
</script>
|
||||
<script src=\"/assets/js/d8251f5e5aa1.js\" defer></script>
|
||||
<script>
|
||||
(function(){
|
||||
var cfg=window.NEURON_CFG||{};
|
||||
var sbUrl=cfg.supabase_url,sbKey=cfg.supabase_anon_key;
|
||||
if(!sbUrl||!sbKey)return;
|
||||
var sb=window.supabase.createClient(sbUrl,sbKey);
|
||||
var token=null;
|
||||
var votes={};
|
||||
|
||||
function getCtrl(sid){
|
||||
var found=null;
|
||||
document.querySelectorAll('.vote-controls').forEach(function(c){
|
||||
if(c.getAttribute('data-share-id')===sid)found=c;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
function applyState(ctrl,state){
|
||||
var sid=ctrl.getAttribute('data-share-id');
|
||||
votes[sid]=state.user_vote||'none';
|
||||
var scoreEl=ctrl.querySelector('.vote-score');
|
||||
if(scoreEl&&state.score!=null)scoreEl.textContent=state.score;
|
||||
var upBtn=ctrl.querySelector('.vote-btn.vote-up');
|
||||
var dnBtn=ctrl.querySelector('.vote-btn.vote-down');
|
||||
if(upBtn){upBtn.disabled=false;upBtn.classList.toggle('is-active',state.user_vote==='up');}
|
||||
if(dnBtn){dnBtn.disabled=false;dnBtn.classList.toggle('is-active',state.user_vote==='down');}
|
||||
}
|
||||
|
||||
function loadVoteState(sid){
|
||||
var url='/api/vote-state/'+sid;
|
||||
if(token)url+='?access_token='+encodeURIComponent(token);
|
||||
fetch(url).then(function(r){return r.json();}).then(function(d){
|
||||
var ctrl=getCtrl(sid);
|
||||
if(ctrl)applyState(ctrl,d);
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
function loadAll(){
|
||||
document.querySelectorAll('.vote-controls').forEach(function(ctrl){
|
||||
var id=ctrl.getAttribute('data-share-id');
|
||||
if(id)loadVoteState(id);
|
||||
});
|
||||
}
|
||||
|
||||
function castVote(sid,direction){
|
||||
if(!token){showSignIn();return;}
|
||||
var ctrl=getCtrl(sid);
|
||||
var btns=ctrl?ctrl.querySelectorAll('.vote-btn'):[];
|
||||
btns.forEach(function(b){b.disabled=true;b.classList.add('is-loading');});
|
||||
fetch('/api/vote',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({access_token:token,id:sid,direction:direction})
|
||||
}).then(function(r){return r.json();}).then(function(d){
|
||||
if(ctrl&&d.ok)applyState(ctrl,d);
|
||||
btns.forEach(function(b){b.disabled=false;b.classList.remove('is-loading');});
|
||||
}).catch(function(){
|
||||
btns.forEach(function(b){b.disabled=false;b.classList.remove('is-loading');});
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.vote-controls').forEach(function(ctrl){
|
||||
ctrl.querySelectorAll('.vote-btn').forEach(function(btn){
|
||||
btn.addEventListener('click',function(){
|
||||
var sid=ctrl.getAttribute('data-share-id');
|
||||
if(!token){showSignIn();return;}
|
||||
var dir=btn.getAttribute('data-direction');
|
||||
var cur=votes[sid]||'none';
|
||||
castVote(sid,cur===dir?'none':dir);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var modal=document.getElementById('signin-modal');
|
||||
var cancelEl=document.getElementById('signin-cancel');
|
||||
var sendEl=document.getElementById('signin-send');
|
||||
var emailEl=document.getElementById('signin-email');
|
||||
var msgEl=document.getElementById('signin-msg');
|
||||
|
||||
function showSignIn(){if(modal)modal.classList.add('open');if(emailEl)emailEl.focus();}
|
||||
|
||||
if(cancelEl)cancelEl.addEventListener('click',function(){modal.classList.remove('open');});
|
||||
if(modal)modal.addEventListener('click',function(e){if(e.target===modal)modal.classList.remove('open');});
|
||||
if(sendEl)sendEl.addEventListener('click',function(){
|
||||
var email=emailEl?emailEl.value.trim():'';
|
||||
if(!email){if(msgEl)msgEl.textContent='Please enter your email.';return;}
|
||||
sendEl.disabled=true;
|
||||
if(msgEl)msgEl.textContent='Sending...';
|
||||
sb.auth.signInWithOtp({email:email,options:{emailRedirectTo:window.location.href}}).then(function(r){
|
||||
sendEl.disabled=false;
|
||||
if(msgEl)msgEl.textContent=r.error?(r.error.message||'Error. Try again.'):'Check your email for a sign-in link.';
|
||||
});
|
||||
});
|
||||
if(emailEl)emailEl.addEventListener('keydown',function(e){if(e.key==='Enter'&&sendEl)sendEl.click();});
|
||||
|
||||
sb.auth.onAuthStateChange(function(event,session){
|
||||
token=session?session.access_token:null;
|
||||
if(token&&modal)modal.classList.remove('open');
|
||||
loadAll();
|
||||
});
|
||||
|
||||
sb.auth.getSession().then(function(r){
|
||||
token=r.data&&r.data.session?r.data.session.access_token:null;
|
||||
loadAll();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script src=\"/assets/js/cd30551e3c3b.js\" defer></script>
|
||||
</body>
|
||||
</html>"
|
||||
|
||||
Reference in New Issue
Block a user