2026-09-04 11:56:39 -05:00
|
|
|
let currentUser = null;
|
|
|
|
|
let currentChallenge = null;
|
|
|
|
|
let authMethod = null;
|
|
|
|
|
|
|
|
|
|
function escapeHtml(str) {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = str;
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Matrix auth ---
|
|
|
|
|
|
|
|
|
|
function goToMatrixLogin() {
|
|
|
|
|
const slug = window.location.pathname.split('/').filter(Boolean)[0];
|
|
|
|
|
window.location.href = '/' + slug + '/matrix-login';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function signOut() {
|
|
|
|
|
sessionStorage.removeItem('matrix_session_token');
|
|
|
|
|
currentUser = null;
|
|
|
|
|
authMethod = null;
|
|
|
|
|
window.location.reload();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showNostrAuth() {
|
|
|
|
|
document.getElementById('auth_method_select').style.display = 'none';
|
|
|
|
|
document.getElementById('nostr_auth_section').style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function checkMatrixSession() {
|
|
|
|
|
const sessionToken = sessionStorage.getItem('matrix_session_token');
|
|
|
|
|
if (!sessionToken) return false;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const slug = window.location.pathname.split('/').filter(Boolean)[0];
|
|
|
|
|
const resp = await fetch('/' + slug + '/matrixVerify', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ session_token: sessionToken })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (resp.ok) {
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
currentUser = { displayName: data.display_name, sessionToken: sessionToken };
|
|
|
|
|
authMethod = 'matrix';
|
|
|
|
|
|
|
|
|
|
document.getElementById('auth_section').style.display = 'none';
|
|
|
|
|
document.getElementById('vote_section').style.display = 'block';
|
|
|
|
|
document.getElementById('logged_in_as').textContent = 'Signed in as: ' + data.display_name;
|
|
|
|
|
|
|
|
|
|
loadSubmissions();
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
sessionStorage.removeItem('matrix_session_token');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
sessionStorage.removeItem('matrix_session_token');
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Nostr auth ---
|
|
|
|
|
|
|
|
|
|
function hasNip07() {
|
|
|
|
|
return typeof window.nostr !== 'undefined' && window.nostr !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function connectNip07() {
|
|
|
|
|
if (!hasNip07()) {
|
|
|
|
|
setAuthStatus('No Nostr browser extension detected. Enter your npub manually.', true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const hexKey = await window.nostr.getPublicKey();
|
|
|
|
|
const resp = await fetch('encode_npub', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ hex: hexKey })
|
|
|
|
|
});
|
|
|
|
|
if (resp.ok) {
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
document.getElementById('npub_input').value = data.npub;
|
|
|
|
|
document.getElementById('nip07_badge').style.display = 'block';
|
|
|
|
|
setAuthStatus('Connected! Click "Sign In to Vote" to continue.');
|
|
|
|
|
} else {
|
|
|
|
|
document.getElementById('npub_input').value = hexKey;
|
|
|
|
|
setAuthStatus('Connected (using hex key). Click "Sign In to Vote".');
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setAuthStatus('Error: ' + err.message, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function authenticate() {
|
|
|
|
|
const npub = document.getElementById('npub_input').value.trim();
|
|
|
|
|
if (!npub) {
|
|
|
|
|
setAuthStatus('Enter your npub or connect AlbyHub first.', true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAuthStatus('Requesting challenge...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch('challenge', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ npub: npub })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (resp.status === 404) {
|
|
|
|
|
setAuthStatus('Npub not linked. Please <a href="link">link your identity</a> first.', true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
|
const text = await resp.text();
|
|
|
|
|
setAuthStatus('Error: ' + text, true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
currentChallenge = data.challenge;
|
|
|
|
|
|
|
|
|
|
if (!hasNip07()) {
|
|
|
|
|
setAuthStatus('No Nostr extension. Please install AlbyHub to sign.', true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAuthStatus('Please sign the challenge in your extension...');
|
|
|
|
|
|
|
|
|
|
const signedEvent = await window.nostr.signEvent({
|
|
|
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
|
|
|
kind: 1,
|
|
|
|
|
tags: [],
|
|
|
|
|
content: currentChallenge
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
currentUser = {
|
|
|
|
|
npub: npub,
|
|
|
|
|
displayName: data.display_name,
|
|
|
|
|
signedEvent: signedEvent
|
|
|
|
|
};
|
|
|
|
|
authMethod = 'nostr';
|
|
|
|
|
|
|
|
|
|
document.getElementById('auth_section').style.display = 'none';
|
|
|
|
|
document.getElementById('vote_section').style.display = 'block';
|
|
|
|
|
document.getElementById('logged_in_as').textContent = 'Signed in as: ' + data.display_name;
|
|
|
|
|
|
|
|
|
|
loadSubmissions();
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err.message && err.message.includes('denied')) {
|
|
|
|
|
setAuthStatus('Signature request denied.', true);
|
|
|
|
|
} else {
|
|
|
|
|
setAuthStatus('Error: ' + err.message, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setAuthStatus(msg, isError) {
|
|
|
|
|
const el = document.getElementById('auth_status');
|
|
|
|
|
el.innerHTML = msg;
|
|
|
|
|
el.className = isError ? 'error' : 'success';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Submissions ---
|
|
|
|
|
|
2026-08-06 21:34:52 -05:00
|
|
|
async function loadSubmissions() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('submissions');
|
|
|
|
|
const data = await response.json();
|
2026-09-04 11:56:39 -05:00
|
|
|
|
2026-08-06 21:34:52 -05:00
|
|
|
const submissionList = document.getElementById('suggestion_list');
|
2026-09-04 11:56:39 -05:00
|
|
|
submissionList.innerHTML = '';
|
|
|
|
|
|
2026-08-06 21:34:52 -05:00
|
|
|
if (data.Submissions && data.Submissions.length > 0) {
|
2026-09-04 11:56:39 -05:00
|
|
|
data.Submissions.forEach((sub) => {
|
2026-08-06 21:34:52 -05:00
|
|
|
const li = document.createElement('li');
|
|
|
|
|
li.innerHTML = `
|
2026-09-04 11:56:39 -05:00
|
|
|
<span class="suggestor">${escapeHtml(sub.submitter)} SUGGESTED</span> :
|
|
|
|
|
<span class="suggestion">${escapeHtml(sub.submission)}</span>
|
|
|
|
|
<input type="radio" name="vote" value="${escapeHtml(sub.submission)}" data-submitter="${escapeHtml(sub.submitter)}" data-time="${sub.submission_time}">
|
2026-08-06 21:34:52 -05:00
|
|
|
`;
|
|
|
|
|
submissionList.appendChild(li);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
submissionList.innerHTML = '<li>No submissions available</li>';
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading submissions:', error);
|
|
|
|
|
document.getElementById('suggestion_list').innerHTML = '<li>Error loading submissions</li>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 11:56:39 -05:00
|
|
|
// --- Voting ---
|
|
|
|
|
|
|
|
|
|
async function submitVote() {
|
|
|
|
|
if (!currentUser) {
|
|
|
|
|
alert('Please sign in first');
|
2026-08-06 21:34:52 -05:00
|
|
|
return;
|
|
|
|
|
}
|
2026-09-04 11:56:39 -05:00
|
|
|
|
2026-08-06 21:34:52 -05:00
|
|
|
const selectedRadio = document.querySelector('input[name="vote"]:checked');
|
|
|
|
|
if (!selectedRadio) {
|
|
|
|
|
alert('Please select a title to vote for');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-09-04 11:56:39 -05:00
|
|
|
|
2026-08-06 21:34:52 -05:00
|
|
|
const voteData = {
|
|
|
|
|
selected_submission: selectedRadio.value,
|
|
|
|
|
submitter: selectedRadio.dataset.submitter,
|
2026-09-04 11:56:39 -05:00
|
|
|
submission_time: parseInt(selectedRadio.dataset.time)
|
2026-08-06 21:34:52 -05:00
|
|
|
};
|
2026-09-04 11:56:39 -05:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (authMethod === 'matrix') {
|
|
|
|
|
const resp = await fetch('matrixVote', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_token: currentUser.sessionToken,
|
|
|
|
|
...voteData
|
|
|
|
|
})
|
2026-08-06 21:34:52 -05:00
|
|
|
});
|
2026-09-04 11:56:39 -05:00
|
|
|
|
|
|
|
|
if (resp.ok) {
|
|
|
|
|
alert('Vote submitted successfully!');
|
|
|
|
|
selectedRadio.checked = false;
|
|
|
|
|
} else {
|
|
|
|
|
const text = await resp.text();
|
|
|
|
|
alert('Error: ' + text);
|
|
|
|
|
}
|
|
|
|
|
} else if (authMethod === 'nostr') {
|
|
|
|
|
const challengeResp = await fetch('challenge', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ npub: currentUser.npub })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!challengeResp.ok) {
|
|
|
|
|
const text = await challengeResp.text();
|
|
|
|
|
alert('Error getting challenge: ' + text);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const challengeData = await challengeResp.json();
|
|
|
|
|
const challengeMsg = challengeData.challenge;
|
|
|
|
|
|
|
|
|
|
const signedEvent = await window.nostr.signEvent({
|
|
|
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
|
|
|
kind: 1,
|
|
|
|
|
tags: [],
|
|
|
|
|
content: challengeMsg
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const voteResp = await fetch('verifyVote', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
npub: currentUser.npub,
|
|
|
|
|
challenge: challengeMsg,
|
|
|
|
|
...voteData,
|
|
|
|
|
signed_event: signedEvent
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (voteResp.ok) {
|
|
|
|
|
alert('Vote submitted successfully!');
|
|
|
|
|
selectedRadio.checked = false;
|
|
|
|
|
} else {
|
|
|
|
|
const text = await voteResp.text();
|
|
|
|
|
alert('Error: ' + text);
|
|
|
|
|
}
|
2026-08-06 21:34:52 -05:00
|
|
|
}
|
2026-09-04 11:56:39 -05:00
|
|
|
} catch (error) {
|
2026-08-06 21:34:52 -05:00
|
|
|
console.error('Error submitting vote:', error);
|
|
|
|
|
alert('Error submitting vote: ' + error.message);
|
2026-09-04 11:56:39 -05:00
|
|
|
}
|
2026-08-06 21:34:52 -05:00
|
|
|
}
|
|
|
|
|
|
2026-09-04 11:56:39 -05:00
|
|
|
// --- Init ---
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
if (currentUser) loadSubmissions();
|
|
|
|
|
}, 30000);
|
|
|
|
|
|
|
|
|
|
const matrixAuthed = await checkMatrixSession();
|
|
|
|
|
if (matrixAuthed) return;
|
|
|
|
|
|
|
|
|
|
if (hasNip07()) {
|
|
|
|
|
document.getElementById('nip07_badge').style.display = 'block';
|
2026-08-06 21:34:52 -05:00
|
|
|
}
|
|
|
|
|
});
|