Add Join The Movement form, fix login route, fix images, update copyright to 2026

- Add /api/join worker endpoint that emails membership details to party contact
- Register /api/login route (was missing — emoji login was broken)
- Add full AEC-compliant member registration form in index.html (#join section)
- Update copyright year to 2026 in index.html and login.html
- Fix dictator/target image URLs: radical.omar-c29.workers.dev → theradicalparty.com
- Add onerror handlers to all login page images for graceful degradation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
King Omar 2026-06-27 01:31:30 +10:00
parent b789b35f78
commit a6e7989a10
3 changed files with 207 additions and 10 deletions

View file

@ -150,6 +150,28 @@
.email-modal-btn:hover{background:#ff33aa}
.email-modal-skip{background:none;border:none;color:#555;font-family:'Roboto Mono',monospace;font-size:12px;cursor:pointer;width:100%;text-align:center;padding:4px}
.email-modal-skip:hover{color:#aaa}
/* ── Join section ── */
.join-section{margin:32px 0;border:2px solid #ff0099;padding:0}
.join-header{background:#ff0099;color:#000;padding:14px 20px}
.join-header h2{font-family:'Roboto Mono',monospace;font-size:18px;font-weight:700;margin:0 0 4px}
.join-desc{font-family:'Roboto Mono',monospace;font-size:11px;margin:0;line-height:1.5}
.join-form{padding:20px}
.join-row{display:flex;gap:12px;flex-wrap:wrap}
.join-field{display:flex;flex-direction:column;gap:4px;flex:1;min-width:140px;margin-bottom:12px}
.join-field--sm{flex:0 0 100px;min-width:80px}
.join-field label{font-family:'Roboto Mono',monospace;font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:.5px}
.join-input{background:#0a0a0a;border:1px solid #333;color:#fff;padding:10px 12px;font-family:'Roboto Mono',monospace;font-size:13px;width:100%;box-sizing:border-box}
.join-input:focus{outline:none;border-color:#ff0099}
.join-input option{background:#111;color:#fff}
.join-check{display:flex;align-items:flex-start;gap:10px;margin-bottom:12px}
.join-check input[type=checkbox]{margin-top:2px;accent-color:#ff0099;width:16px;height:16px;flex-shrink:0}
.join-check label{font-family:'Roboto Mono',monospace;font-size:12px;color:#aaa;line-height:1.5}
.join-submit{width:100%;background:#ff0099;color:#000;border:none;padding:14px;font-family:'Roboto Mono',monospace;font-size:15px;font-weight:700;cursor:pointer;margin-top:8px;letter-spacing:1px}
.join-submit:hover{background:#ff33aa}
.join-submit:disabled{opacity:.5;cursor:not-allowed}
.join-msg{font-family:'Roboto Mono',monospace;font-size:13px;min-height:20px;margin-bottom:8px}
.join-msg.success{color:#11cc77}
.join-msg.error{color:#ff0099}
/* ── Flag & share-image buttons ── */
.flag-btn{background:none;border:none;color:#444;font-size:12px;cursor:pointer;padding:4px 6px;font-family:'Roboto Mono',monospace;transition:color .15s}
.flag-btn:hover{color:#ff0099}
@ -281,8 +303,70 @@
</div>
</section>
<section id="join" class="join-section">
<div class="join-header">
<h2>JOIN THE MOVEMENT</h2>
<p class="join-desc">Become an official RADICAL Party member. Your details are collected in accordance with the Australian Electoral Commission requirements for registered political parties.</p>
</div>
<form id="join-form" class="join-form" onsubmit="handleJoinSubmit(event)">
<div class="join-row">
<div class="join-field">
<label for="join-fullname">Full Name (as on electoral roll) *</label>
<input type="text" id="join-fullname" class="join-input" required placeholder="Jane Smith" autocomplete="name">
</div>
<div class="join-field">
<label for="join-dob">Date of Birth *</label>
<input type="date" id="join-dob" class="join-input" required>
</div>
</div>
<div class="join-field">
<label for="join-address">Residential Address *</label>
<input type="text" id="join-address" class="join-input" required placeholder="123 Rebellion Street" autocomplete="street-address">
</div>
<div class="join-row">
<div class="join-field">
<label for="join-suburb">Suburb *</label>
<input type="text" id="join-suburb" class="join-input" required placeholder="Sydney" autocomplete="address-level2">
</div>
<div class="join-field join-field--sm">
<label for="join-state">State *</label>
<select id="join-state" class="join-input" required>
<option value=""></option>
<option>NSW</option><option>VIC</option><option>QLD</option>
<option>WA</option><option>SA</option><option>TAS</option>
<option>ACT</option><option>NT</option>
</select>
</div>
<div class="join-field join-field--sm">
<label for="join-postcode">Postcode *</label>
<input type="text" id="join-postcode" class="join-input" required placeholder="2000" maxlength="4" pattern="\d{4}" inputmode="numeric">
</div>
</div>
<div class="join-row">
<div class="join-field">
<label for="join-email">Email Address *</label>
<input type="email" id="join-email" class="join-input" required placeholder="you@example.com" autocomplete="email">
</div>
<div class="join-field">
<label for="join-phone">Phone (optional)</label>
<input type="tel" id="join-phone" class="join-input" placeholder="04xx xxx xxx" autocomplete="tel">
</div>
</div>
<div class="join-check">
<input type="checkbox" id="join-roll" required>
<label for="join-roll">I confirm I am enrolled to vote in Australian federal elections (or am eligible to enrol) *</label>
</div>
<div class="join-check">
<input type="checkbox" id="join-consent" required>
<label for="join-consent">I consent to joining the RADICAL Party as a member and agree that my details may be used for AEC registration purposes *</label>
</div>
<div id="join-msg" class="join-msg"></div>
<button type="submit" class="join-submit" id="join-btn">JOIN THE REBELLION →</button>
</form>
</section>
<footer>
<p>RADICAL - Direct Democracy Movement 🇦🇺 &copy; 2025</p>
<p>RADICAL - Direct Democracy Movement 🇦🇺 &copy; 2026</p>
<div class="footer-links">
<a href="/login" class="login-link">Login 🔐</a>
<a href="#about">About Us</a>
@ -3011,6 +3095,58 @@ async function handleFlag(proposalId, btn) {
}
}
async function handleJoinSubmit(e) {
e.preventDefault();
const btn = document.getElementById('join-btn');
const msg = document.getElementById('join-msg');
msg.textContent = '';
msg.className = 'join-msg';
btn.disabled = true;
btn.textContent = 'SUBMITTING...';
const dob = document.getElementById('join-dob').value;
const age = (Date.now() - new Date(dob)) / (365.25 * 24 * 60 * 60 * 1000);
if (age < 18) {
msg.textContent = 'You must be 18 or older to join.';
msg.className = 'join-msg error';
btn.disabled = false;
btn.textContent = 'JOIN THE REBELLION →';
return;
}
try {
const res = await fetch(`${API_BASE_URL}/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fullName: document.getElementById('join-fullname').value.trim(),
dob,
address: document.getElementById('join-address').value.trim(),
suburb: document.getElementById('join-suburb').value.trim(),
state: document.getElementById('join-state').value,
postcode: document.getElementById('join-postcode').value.trim(),
email: document.getElementById('join-email').value.trim(),
phone: document.getElementById('join-phone').value.trim(),
onRoll: document.getElementById('join-roll').checked,
consent: document.getElementById('join-consent').checked
})
});
const data = await res.json();
if (res.ok && data.success) {
msg.textContent = '✓ Welcome to the movement! We\'ll be in touch.';
msg.className = 'join-msg success';
document.getElementById('join-form').reset();
} else {
throw new Error(data.error || 'Submission failed');
}
} catch (err) {
msg.textContent = '⚠ ' + (err.message || 'Something went wrong. Try again.');
msg.className = 'join-msg error';
}
btn.disabled = false;
btn.textContent = 'JOIN THE REBELLION →';
}
</script>
<div class="music-prompt" id="music-prompt">hit me</div>

View file

@ -578,7 +578,7 @@
</div>
<div class="footer">
RADICAL - Direct Democracy Movement 🇦🇺 &copy; 2025
RADICAL - Direct Democracy Movement 🇦🇺 &copy; 2026
</div>
</div>
@ -607,15 +607,15 @@
// Dictator options
const dictators = [
{ id: 'stalin', name: 'Joseph Stalin', image: 'https://radical.omar-c29.workers.dev/memes/stalin.avif' },
{ id: 'hitler', name: 'Adolf Hitler', image: 'https://radical.omar-c29.workers.dev/memes/hitler.jpg' },
{ id: 'kim', name: 'Kim Jong-un', image: 'https://radical.omar-c29.workers.dev/memes/kim.png' },
{ id: 'mao', name: 'Mao Zedong', image: 'https://radical.omar-c29.workers.dev/memes/mao.jpg' },
{ id: 'castro', name: 'Fidel Castro', image: 'https://radical.omar-c29.workers.dev/memes/fidel.jpg' },
{ id: 'putin', name: 'Vladimir Putin', image: 'https://radical.omar-c29.workers.dev/memes/putin.jpg' },
{ id: 'mussolini', name: 'Benito Mussolini', image: 'https://radical.omar-c29.workers.dev/memes/benito.jpg' },
{ id: 'stalin', name: 'Joseph Stalin', image: 'https://theradicalparty.com/memes/stalin.avif' },
{ id: 'hitler', name: 'Adolf Hitler', image: 'https://theradicalparty.com/memes/hitler.jpg' },
{ id: 'kim', name: 'Kim Jong-un', image: 'https://theradicalparty.com/memes/kim.png' },
{ id: 'mao', name: 'Mao Zedong', image: 'https://theradicalparty.com/memes/mao.jpg' },
{ id: 'castro', name: 'Fidel Castro', image: 'https://theradicalparty.com/memes/fidel.jpg' },
{ id: 'putin', name: 'Vladimir Putin', image: 'https://theradicalparty.com/memes/putin.jpg' },
{ id: 'mussolini', name: 'Benito Mussolini', image: 'https://theradicalparty.com/memes/benito.jpg' },
{ id: 'pol', name: 'Pol Pot', image: 'https://i.imgur.com/AZRJW2n.jpg' },
{ id: 'kanye', name: 'Kanye West', image: 'https://radical.omar-c29.workers.dev/memes/ye.png' },
{ id: 'kanye', name: 'Kanye West', image: 'https://theradicalparty.com/memes/ye.png' },
{ id: 'khomeini', name: 'Ayatollah Khomeini', image: 'https://i.imgur.com/M7OPgQd.jpg' },
{ id: 'netanyahu', name: 'Benjamin Netanyahu', image: 'https://i.imgur.com/9ZKJFy8.jpg' },
{ id: 'zelensky', name: 'Volodymyr Zelensky', image: 'https://i.imgur.com/jANrPvB.jpg' },
@ -691,6 +691,7 @@
image.src = target.image;
image.alt = target.name;
image.loading = 'lazy';
image.onerror = function() { this.style.display='none'; };
const name = document.createElement('div');
name.className = 'target-name';
@ -755,6 +756,7 @@
image.src = dictator.image;
image.alt = dictator.name;
image.loading = 'lazy';
image.onerror = function() { this.style.display='none'; };
const name = document.createElement('div');
name.className = 'dictator-name';

View file

@ -143,6 +143,10 @@ export default {
return await saveUserEmail(request, env);
} else if (path === '/api/flag' && request.method === 'POST') {
return await flagProposal(request, env);
} else if (path === '/api/login' && request.method === 'POST') {
return await validateLogin(request, env);
} else if (path === '/api/join' && request.method === 'POST') {
return await handleJoinMovement(request, env);
} else if (path === '/api/petition-stats' && request.method === 'GET') {
return await getPetitionStats(request, env);
} else if (path === '/api/petition-svg' && request.method === 'GET') {
@ -2473,4 +2477,59 @@ async function sendWeeklyDigest(env) {
} catch (error) {
console.error('sendWeeklyDigest error:', error);
}
}
async function handleJoinMovement(request, env) {
try {
const data = await request.json();
const { fullName, dob, address, suburb, state, postcode, email, phone, onRoll, consent } = data;
if (!fullName || !dob || !address || !suburb || !state || !postcode || !email || !onRoll || !consent) {
return createResponse({ error: 'Missing required fields' }, 400);
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return createResponse({ error: 'Invalid email address' }, 400);
}
const timestamp = new Date().toLocaleString('en-AU', { timeZone: 'Australia/Sydney' });
if (env.RESEND_API_KEY) {
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'RADICAL <noreply@theradicalparty.com>',
to: ['kingomartheoneandonly@protonmail.com'],
subject: `New RADICAL Member: ${fullName}`,
html: `<div style="font-family:monospace;background:#000;color:#fff;padding:24px;max-width:600px">
<h2 style="color:#ff0099">NEW RADICAL PARTY MEMBER</h2>
<p style="color:#aaa">Submitted: ${timestamp}</p>
<table style="width:100%;border-collapse:collapse;margin-top:16px">
<tr><td style="padding:8px;color:#ff0099;width:140px">Full Name</td><td style="padding:8px">${escapeHTML(fullName)}</td></tr>
<tr style="background:#111"><td style="padding:8px;color:#ff0099">Date of Birth</td><td style="padding:8px">${escapeHTML(dob)}</td></tr>
<tr><td style="padding:8px;color:#ff0099">Address</td><td style="padding:8px">${escapeHTML(address)}</td></tr>
<tr style="background:#111"><td style="padding:8px;color:#ff0099">Suburb</td><td style="padding:8px">${escapeHTML(suburb)}</td></tr>
<tr><td style="padding:8px;color:#ff0099">State</td><td style="padding:8px">${escapeHTML(state)}</td></tr>
<tr style="background:#111"><td style="padding:8px;color:#ff0099">Postcode</td><td style="padding:8px">${escapeHTML(postcode)}</td></tr>
<tr><td style="padding:8px;color:#ff0099">Email</td><td style="padding:8px">${escapeHTML(email)}</td></tr>
<tr style="background:#111"><td style="padding:8px;color:#ff0099">Phone</td><td style="padding:8px">${phone ? escapeHTML(phone) : 'Not provided'}</td></tr>
<tr><td style="padding:8px;color:#ff0099">On Electoral Roll</td><td style="padding:8px">Yes </td></tr>
<tr style="background:#111"><td style="padding:8px;color:#ff0099">Consented</td><td style="padding:8px">Yes </td></tr>
</table>
<hr style="border-color:#333;margin-top:32px">
<p style="font-size:0.8em;color:#666">RADICAL Party Membership Application</p>
</div>`
})
});
}
return createResponse({ success: true }, 200, 0);
} catch (error) {
return createResponse({ error: 'Failed to process membership application', details: logError(error, { action: 'join_movement' }) }, 500);
}
}