radical/index.html
King Omar 942da4b6a5 Add light/dark theme toggle to all pages
- Flash-prevention script in <head> reads radical_theme from localStorage and
  sets data-theme on <html> before first paint — no FOUC
- Fixed ☀️/🌙 button (top-right, z-index 9999) on index, admin, and login using
  CSS ::before so the icon is driven by data-theme with no JS initialization
- Light mode: #F5F3EF warm off-white body, #E8E6E2 cards, #CCC borders, #111 text
- Pink accent (#ff0099) unchanged in both modes
- Signed-in users: theme saved to radical_user localStorage object and applied
  on load so preference follows their session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-12 16:50:24 +10:00

3422 lines
No EOL
159 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Title -->
<title>The Radical Party | Direct Democracy for Australia</title>
<meta name="title" content="The Radical Party | Direct Democracy for Australia">
<meta property="og:title" content="The Radical Party | Direct Democracy for Australia">
<meta name="twitter:title" content="The Radical Party | Direct Democracy for Australia">
<meta itemprop="name" content="The Radical Party">
<meta property="og:site_name" content="The Radical Party">
<meta name="application-name" content="The Radical Party">
<meta name="twitter:domain" content="theradicalparty.com">
<!-- Description -->
<meta property="og:description"
content="The Radical Party: Australia's direct democracy movement. One policy — 100,000 signatures forces a binding referendum. Post ideas, build movements, take back control.">
<meta name="description" content="The Radical Party: Australia's direct democracy movement. One policy — 100,000 signatures forces a binding referendum. Post ideas, build movements, take back control.">
<meta name="twitter:description"
content="The Radical Party: Australia's direct democracy movement. One policy — 100,000 signatures forces a binding referendum. Post ideas, build movements, take back control.">
<meta name="abstract"
content="The Radical Party is an Australian political party promoting direct democracy through Citizens Initiated Referendums.">
<meta itemprop="description"
content="The Radical Party: Australia's direct democracy movement. One policy — 100,000 signatures forces a binding referendum. Post ideas, build movements, take back control.">
<!-- Icon -->
<meta name="twitter:image"
content="https://theradicalparty.com/memes/favicon.jpg">
<meta name="twitter:image:src"
content="https://theradicalparty.com/memes/favicon.jpg">
<meta name="twitter:image:alt" content="RADICAL Logo">
<link rel="shortcut icon" href="https://theradicalparty.com/memes/favicon.jpg" type="image/x-icon">
<link rel="shortcut icon" href="https://theradicalparty.com/memes/favicon.jpg"
type="image/jpg">
<link rel="mask-icon" href="https://theradicalparty.com/memes/favicon.jpg"
color="#ff0099">
<meta property="og:image"
content="https://theradicalparty.com/memes/mickeymeta.PNG">
<meta property="og:image:url"
content="https://theradicalparty.com/memes/mickeymeta.PNG">
<meta property="og:image:secure_url"
content="https://theradicalparty.com/memes/mickeymeta.PNG">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="256">
<meta property="og:image:height" content="256">
<meta property="og:image:alt" content="The RADICAL logo.">
<meta itemprop="image" content="https://theradicalparty.com/memes/favicon.jpg">
<link rel="icon" type="image/jpg" sizes="512x512"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="icon" type="image/jpg" sizes="256x256"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="icon" type="image/jpg" sizes="128x128"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="icon" type="image/jpg" sizes="64x64"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="icon" type="image/jpg" sizes="32x32"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="icon" type="image/jpg"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="apple-touch-icon" sizes="512x512"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="apple-touch-icon" sizes="256x256"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="apple-touch-icon" sizes="128x128"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="apple-touch-icon" sizes="64x64"
href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="apple-touch-icon" sizes="32x32"
href="https://theradicalparty.com/memes/favicon.jpg">
<!-- APPLE TOUCH ICONS -->
<link rel="apple-touch-icon" href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="apple-touch-icon" sizes="152x152" href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="apple-touch-icon" sizes="180x180" href="https://theradicalparty.com/memes/favicon.jpg">
<link rel="apple-touch-icon" sizes="167x167" href="https://theradicalparty.com/memes/favicon.jpg">
<!-- SPLASH SCREEN IMAGES -->
<!-- iPhone X (1125px x 2436px) -->
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" href="https://theradicalparty.com/memes/splash-1125x2436.png">
<!-- iPhone 8, 7, 6s, 6 (750px x 1334px) -->
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="https://theradicalparty.com/memes/splash-750x1334.png">
<!-- iPhone 8 Plus, 7 Plus, 6s Plus, 6 Plus (1242px x 2208px) -->
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3)" href="https://theradicalparty.com/memes/splash-1242x2208.png">
<!-- Author -->
<meta name="og:email" content="contact@theradicalparty.com">
<meta name="twitter:creator" content="@RadicalAustralia">
<meta name="twitter:site" content="@RadicalAustralia">
<meta name="author" content="RADICAL Party, contact@theradicalparty.com">
<link rel="author" href="https://theradicalparty.com/humans.txt">
<!-- App-like behavior for mobile -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-status-bar-style" content="black-translucent">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;600;700&display=swap"
rel="stylesheet">
<link
href="styles.css"
rel="stylesheet">
<link rel="manifest"
href="https://theradicalparty.com/manifest.webmanifest">
<!-- For iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="RADICAL">
<!-- For Android -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#ff0099">
<meta name="application-name" content="RADICAL">
<!-- For Windows -->
<meta name="msapplication-TileColor" content="#ff0099">
<meta name="msapplication-navbutton-color" content="#ff0099">
<style>
/* ── Stats bar ── */
.stats-bar{display:flex;align-items:center;justify-content:center;gap:12px;padding:8px 16px;background:#0a0a0a;border:1px solid #222;font-family:'Roboto Mono',monospace;font-size:12px;color:#aaa;flex-wrap:wrap}
.stat-number{color:#ff0099;font-weight:700;font-size:14px}
.stat-divider{color:#333}
/* ── Leaderboard ── */
.leaderboard-section{margin:16px 0 24px;border:1px solid #ff0099;padding:0}
.leaderboard-header{background:#ff0099;color:#000;padding:8px 16px;font-family:'Roboto Mono',monospace;font-weight:700;font-size:13px;display:flex;justify-content:space-between;align-items:center}
.leaderboard-sub{font-weight:400;font-size:11px;opacity:.8}
.leaderboard-item{padding:12px 16px;border-bottom:1px solid #1a1a1a;display:flex;flex-direction:column;gap:6px;cursor:pointer;transition:background .15s}
.leaderboard-item:hover{background:#0d0d0d}
.leaderboard-item:last-child{border-bottom:none}
.leaderboard-rank{font-family:'Roboto Mono',monospace;font-size:11px;color:#666;margin-right:8px}
.leaderboard-text{font-family:'Roboto Mono',monospace;font-size:13px;color:#fff;flex:1}
.leaderboard-meta{display:flex;align-items:center;gap:12px;font-size:11px;font-family:'Roboto Mono',monospace}
.leaderboard-upvotes{color:#ff0099;font-weight:700}
.leaderboard-postcode{color:#666}
.milestone-bar-wrap{display:flex;flex-direction:column;gap:3px}
.milestone-bar-track{background:#1a1a1a;height:4px;border-radius:0;overflow:hidden}
.milestone-bar-fill{background:#ff0099;height:4px;transition:width .4s ease}
.milestone-label{font-family:'Roboto Mono',monospace;font-size:10px;color:#666}
.milestone-label span{color:#ff0099}
/* ── AEC Membership modal ── */
.membership-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:3000;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto}
.membership-modal{background:#000;border:2px solid #ff0099;padding:0;max-width:520px;width:100%;font-family:'Roboto Mono',monospace;max-height:90vh;overflow-y:auto}
.membership-modal-header{background:#ff0099;color:#000;padding:12px 16px;display:flex;justify-content:space-between;align-items:center}
.membership-modal-header h3{margin:0;font-size:15px;font-weight:700}
.membership-modal-close{background:none;border:none;color:#000;font-size:18px;cursor:pointer;line-height:1;padding:0 4px}
.membership-modal-intro{font-size:11px;color:#888;padding:12px 16px 0;margin:0;line-height:1.6}
#mm-form{padding:12px 16px 0}
.mm-row{display:flex;gap:10px;flex-wrap:wrap}
.mm-field{display:flex;flex-direction:column;gap:3px;flex:1;min-width:120px;margin-bottom:10px}
.mm-field--xs{flex:0 0 80px;min-width:70px}
.mm-field label{font-size:10px;color:#666;text-transform:uppercase;letter-spacing:.5px}
.mm-input{background:#0a0a0a;border:1px solid #333;color:#fff;padding:8px 10px;font-family:'Roboto Mono',monospace;font-size:12px;width:100%;box-sizing:border-box}
.mm-input:focus{outline:none;border-color:#ff0099}
.mm-input option{background:#111}
.mm-check{display:flex;align-items:flex-start;gap:8px;margin-bottom:10px}
.mm-check input[type=checkbox]{margin-top:2px;accent-color:#ff0099;flex-shrink:0}
.mm-check label{font-size:11px;color:#888;line-height:1.5}
.mm-msg{font-size:11px;min-height:16px;margin-bottom:8px}
.mm-error{color:#ff0099}
.mm-submit{width:100%;background:#ff0099;color:#000;border:none;padding:12px;font-family:'Roboto Mono',monospace;font-size:13px;font-weight:700;cursor:pointer;letter-spacing:1px}
.mm-submit:hover{background:#ff33aa}
.mm-submit:disabled{opacity:.5;cursor:not-allowed}
.mm-later{display:block;width:100%;background:none;border:none;color:#444;font-family:'Roboto Mono',monospace;font-size:11px;cursor:pointer;padding:10px;text-align:center;margin:0}
.mm-later:hover{color:#888}
/* ── Email capture modal ── */
.email-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:2000;display:flex;align-items:center;justify-content:center;padding:16px}
.email-modal{background:#000;border:2px solid #ff0099;padding:28px;max-width:420px;width:100%;font-family:'Roboto Mono',monospace}
.email-modal h3{color:#ff0099;margin:0 0 8px;font-size:18px}
.email-modal p{color:#aaa;font-size:12px;margin:0 0 20px}
.email-modal-input{width:100%;background:#0a0a0a;border:1px solid #333;color:#fff;padding:10px 12px;font-family:'Roboto Mono',monospace;font-size:13px;box-sizing:border-box;margin-bottom:10px}
.email-modal-input:focus{outline:none;border-color:#ff0099}
.email-modal-btn{width:100%;background:#ff0099;color:#000;border:none;padding:12px;font-family:'Roboto Mono',monospace;font-size:14px;font-weight:700;cursor:pointer;margin-bottom:8px}
.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}
/* ── Constitution ── */
.constitution-section{margin:32px 0;border:1px solid #222;background:#050505}
.constitution-header{background:#0a0a0a;border-bottom:2px solid #ff0099;padding:16px 20px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;user-select:none}
.constitution-header:hover{background:#0d0d0d}
.constitution-tag{font-family:'Roboto Mono',monospace;font-size:10px;color:#ff0099;letter-spacing:3px;text-transform:uppercase;margin-bottom:6px}
.constitution-title{font-family:'Roboto Mono',monospace;font-size:20px;font-weight:700;color:#fff;margin:0 0 4px}
.constitution-meta{font-family:'Roboto Mono',monospace;font-size:11px;color:#555}
.constitution-chevron{font-size:18px;color:#ff0099;transition:transform .25s}
.constitution-chevron.open{transform:rotate(180deg)}
.constitution-body{padding:24px;display:none}
.constitution-body.open{display:block}
.constitution-draft-notice{border:1px solid #2a1a00;background:#0d0800;padding:14px 16px;margin-bottom:24px;font-family:'Roboto Mono',monospace;font-size:11px;color:#888;line-height:1.7}
.constitution-draft-notice strong{color:#c87000}
.constitution-draft-notice ol{margin:8px 0 0 16px;padding:0}
.constitution-draft-notice li{margin-bottom:6px}
.constitution-adopted{font-family:'Roboto Mono',monospace;font-size:12px;color:#555;text-align:center;margin-bottom:28px}
.const-section{margin-bottom:24px;padding-bottom:20px;border-bottom:1px solid #111}
.const-section:last-of-type{border-bottom:none}
.const-h3{font-family:'Roboto Mono',monospace;font-size:13px;font-weight:700;color:#ff0099;text-transform:uppercase;letter-spacing:1px;margin:0 0 12px;padding:6px 0 6px 10px;border-left:3px solid #ff0099}
.const-clause{font-family:'Roboto Mono',monospace;font-size:12px;color:#bbb;line-height:1.75;margin:0 0 10px;display:block;padding-left:46px;position:relative}
.const-clause strong{color:#fff}
.const-clause em{color:#aaa}
.const-num{color:#ff0099;font-weight:700;position:absolute;left:0;top:0;min-width:38px}
.const-sub{font-family:'Roboto Mono',monospace;font-size:12px;color:#999;line-height:1.7;margin:0 0 6px 48px}
.const-end{font-family:'Roboto Mono',monospace;font-size:11px;color:#444;text-align:center;margin-top:24px;padding-top:16px;border-top:1px solid #111}
/* ── About / The Only Policy ── */
.about-section{margin:32px 0;border:1px solid #222;background:#050505}
.about-header{padding:28px 24px 0;border-bottom:2px solid #ff0099;margin-bottom:0}
.about-tag{font-family:'Roboto Mono',monospace;font-size:11px;color:#ff0099;letter-spacing:3px;text-transform:uppercase;margin-bottom:8px}
.about-title{font-family:'Roboto Mono',monospace;font-size:32px;font-weight:700;color:#fff;margin:0 0 20px;line-height:1.2}
.about-body{padding:24px;font-family:'Roboto Mono',monospace;font-size:13px;color:#bbb;line-height:1.8}
.about-body p{margin:0 0 16px}
.about-body strong{color:#fff}
.about-lead{font-size:15px;color:#ddd}
.about-policy-box{border:1px solid #ff0099;padding:16px 20px;margin:20px 0;display:flex;gap:16px;align-items:flex-start;background:rgba(255,0,153,.05)}
.about-policy-icon{font-size:24px;flex-shrink:0;margin-top:2px}
.about-policy-box p{margin:0;font-size:14px;color:#ddd}
.about-divider{border:none;border-top:1px solid #1a1a1a;margin:24px 0}
.about-switzerland{display:flex;gap:20px;align-items:flex-start;background:#0a0a0a;border:1px solid #222;padding:20px}
.about-swiss-flag{font-size:36px;flex-shrink:0;margin-top:4px}
.about-swiss-heading{font-size:15px;color:#fff;font-weight:700;margin:0 0 10px}
.about-steps{display:flex;flex-direction:column;gap:12px;margin:8px 0 24px}
.about-step{display:flex;align-items:center;gap:16px;padding:12px 16px;background:#0a0a0a;border-left:3px solid #ff0099}
.about-step-num{font-size:20px;font-weight:700;color:#ff0099;font-family:'Roboto Mono',monospace;flex-shrink:0}
.about-footer-note{font-size:12px;color:#666;border-top:1px solid #1a1a1a;padding-top:16px;margin-top:8px}
/* ── 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}
/* ── Card action buttons (share / card) ── */
.card-action-btn{background:none;border:none;color:#444;font-family:'Roboto Mono',monospace;font-size:11px;cursor:pointer;padding:3px 6px;transition:color .15s;letter-spacing:.3px}
.card-action-btn:hover{color:#ff0099}
/* ── Postcode badge ── */
.postcode-badge{font-size:10px;color:#555;font-family:'Roboto Mono',monospace}
.postcode-badge span{color:#ff0099}
/* ── Inline milestone strip on proposal cards ── */
.card-milestone{margin-top:8px}
/* ── LIGHT MODE: index.html-specific overrides ── */
html[data-theme="light"] .stats-bar{background:#E8E6E2;border-color:#CCC;color:#555}
html[data-theme="light"] .stat-divider{color:#BBB}
html[data-theme="light"] .leaderboard-section{border-color:#ff0099}
html[data-theme="light"] .leaderboard-item{border-bottom-color:#DDD}
html[data-theme="light"] .leaderboard-item:hover{background:#DEDAD6}
html[data-theme="light"] .leaderboard-text{color:#111}
html[data-theme="light"] .leaderboard-rank{color:#777}
html[data-theme="light"] .leaderboard-postcode{color:#777}
html[data-theme="light"] .milestone-bar-track{background:#CCC}
html[data-theme="light"] .milestone-label{color:#666}
html[data-theme="light"] .membership-modal-overlay{background:rgba(0,0,0,.55)}
html[data-theme="light"] .membership-modal{background:#F5F3EF}
html[data-theme="light"] .membership-modal-intro{color:#555}
html[data-theme="light"] .mm-input{background:#FFF;border-color:#CCC;color:#111}
html[data-theme="light"] .mm-input option{background:#FFF;color:#111}
html[data-theme="light"] .mm-field label{color:#555}
html[data-theme="light"] .mm-check label{color:#555}
html[data-theme="light"] .mm-later{color:#777}
html[data-theme="light"] .mm-later:hover{color:#444}
html[data-theme="light"] .email-modal-overlay{background:rgba(0,0,0,.55)}
html[data-theme="light"] .email-modal{background:#F5F3EF}
html[data-theme="light"] .email-modal p{color:#555}
html[data-theme="light"] .email-modal-input{background:#FFF;border-color:#CCC;color:#111}
html[data-theme="light"] .email-modal-skip{color:#777}
html[data-theme="light"] .email-modal-skip:hover{color:#333}
html[data-theme="light"] .constitution-section{background:#EBEBEA;border-color:#CCC}
html[data-theme="light"] .constitution-header{background:#E0DED9}
html[data-theme="light"] .constitution-header:hover{background:#D8D6D2}
html[data-theme="light"] .constitution-title{color:#111}
html[data-theme="light"] .constitution-meta{color:#777}
html[data-theme="light"] .constitution-body{background:#F5F3EF}
html[data-theme="light"] .constitution-draft-notice{background:#FFF8EE;border-color:#E8CFA0;color:#555}
html[data-theme="light"] .constitution-adopted{color:#777}
html[data-theme="light"] .const-section{border-bottom-color:#DDD}
html[data-theme="light"] .const-clause{color:#333}
html[data-theme="light"] .const-clause strong{color:#111}
html[data-theme="light"] .const-clause em{color:#555}
html[data-theme="light"] .const-sub{color:#555}
html[data-theme="light"] .const-end{color:#777;border-top-color:#DDD}
html[data-theme="light"] .about-section{background:#EBEBEA;border-color:#CCC}
html[data-theme="light"] .about-body{color:#333}
html[data-theme="light"] .about-body strong{color:#111}
html[data-theme="light"] .about-lead{color:#222}
html[data-theme="light"] .about-divider{border-top-color:#DDD}
html[data-theme="light"] .about-switzerland{background:#E0DED9;border-color:#CCC}
html[data-theme="light"] .about-swiss-heading{color:#111}
html[data-theme="light"] .about-step{background:#E0DED9}
html[data-theme="light"] .about-footer-note{color:#777;border-top-color:#DDD}
html[data-theme="light"] .join-input{background:#FFF;border-color:#CCC;color:#111}
html[data-theme="light"] .join-input option{background:#FFF;color:#111}
html[data-theme="light"] .join-field label{color:#555}
html[data-theme="light"] .join-check label{color:#555}
</style>
<!-- Crawling -->
<link rel="sitemap" href="https://theradicalparty.com/sitemap.xml" type="application/xml">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
<!-- Languages -->
<meta property="og:locale" content="en">
<meta name="language" content="en">
<meta property="og:locale:alternate" content="en-gb"><meta property="og:locale:alternate" content="en-us">
<!-- Links -->
<link rel="canonical" href="https://theradicalparty.com">
<meta property="og:url" content="https://theradicalparty.com">
<meta name="twitter:url" content="https://theradicalparty.com">
<meta property="og:see_also" content="https://theradicalparty.com">
<meta property="og:see_also" content="https://theradicalparty.com/about">
<link rel="alternate" type="application/atom+xml" title="Recent Petitions"
href="https://api.theradicalparty.com/petitions.xml">
<link rel="alternate" type="application/rss+xml" title="Recent Petitions"
href="https://api.theradicalparty.com/petitions.xml">
<!-- Meta -->
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary">
<meta name="keywords"
content="The Radical Party, radical party australia, direct democracy australia, citizens initiated referendum, CIR australia, radical, democracy, petitions, policy, activism">
<meta name="rating" content="general">
<meta name="theme-color"
content="#ff0099">
<!-- JSON LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"name": "The Radical Party",
"alternateName": "RADICAL",
"description": "The Radical Party is Australia's direct democracy movement. One policy: if 100,000 Australians sign a petition, the government must hold a binding referendum. Vibe, Vote, Veto.",
"url": "https://theradicalparty.com",
"image": "https://theradicalparty.com/memes/mickeymeta.PNG",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://theradicalparty.com/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
},
{
"@type": "Organization",
"name": "The Radical Party",
"alternateName": ["RADICAL", "RAD"],
"description": "Australian political party promoting direct democracy through Citizens Initiated Referendums. One law: 100,000 signatures compels a binding national vote.",
"url": "https://theradicalparty.com",
"logo": "https://theradicalparty.com/memes/favicon.jpg",
"image": "https://theradicalparty.com/memes/mickeymeta.PNG",
"foundingLocation": {
"@type": "Country",
"name": "Australia"
},
"areaServed": {
"@type": "Country",
"name": "Australia"
},
"contactPoint": {
"@type": "ContactPoint",
"email": "contact@theradicalparty.com",
"contactType": "general"
},
"sameAs": [
"https://twitter.com/RadicalAustralia"
]
}
]
}
</script>
<script>(function(){try{if(localStorage.getItem('radical_theme')==='light')document.documentElement.setAttribute('data-theme','light')}catch(e){}})();</script>
</head>
<body>
<button id="theme-toggle" onclick="toggleTheme()" aria-label="Toggle light/dark mode"></button>
<div class="container">
<header>
<h1>RADICAL</h1>
<div class="tagline">Vibe, Vote, Veto</div>
<p>Australia's direct democracy movement. Share policy ideas,
vote on proposals, take back control.</p>
<br/>
<a href="https://cdn.mos.cms.futurecdn.net/Wh46bS2Gw8vUC6iQh2wEd6.png" style="font-size: 11px;"><i><small>i'm a boomer; click me 2 explain this website </small></i> </a>
</header>
<div class="stats-bar" id="stats-bar">
<div class="stat-item"><span class="stat-number" id="stat-votes"></span><span> votes today</span></div>
<div class="stat-divider">|</div>
<div class="stat-item"><span class="stat-number" id="stat-users"></span><span> citizens</span></div>
<div class="stat-divider">|</div>
<div class="stat-item"><span class="stat-number" id="stat-proposals"></span><span> proposals</span></div>
</div>
<section class="new-proposal">
<textarea id="proposal-input" class="proposal-input"
placeholder="Your idea to revolutionize Australia? (280 character limit)"
maxlength="280"></textarea>
<div id="char-counter" class="char-counter">280 characters
remaining</div>
<button id="post-button" class="post-button" disabled>Post
Proposal</button>
<div style="clear: both;"></div>
</section>
<section class="leaderboard-section">
<div class="leaderboard-header">
<span>🏆 TOP PROPOSALS</span>
<span class="leaderboard-sub">milestones trigger real action</span>
</div>
<div id="leaderboard-container">
<div class="loading-indicator"><div class="loading-spinner"></div><p>Loading...</p></div>
</div>
</section>
<section class="proposals">
<div class="filter-bar">
<div class="filter-title">Democracy Goes Brrr</div>
<select id="sort-select" class="sort-select">
<option value="newest">Latest Ideas</option>
<option value="popular">Most liked</option>
<option value="controversial">controversial</option>
<option value="petitions">Petition Progress</option>
</select>
</div>
<div id="proposals-container">
<div class="loading-indicator">
<div class="loading-spinner"></div>
<p>Loading democracy...</p>
</div>
</div>
</section>
<section id="about" class="about-section">
<div class="about-header">
<div class="about-tag">THE ONLY POLICY</div>
<h2 class="about-title">One Law.<br>All The Power.</h2>
</div>
<div class="about-body">
<p class="about-lead">RADICAL is not a normal party. We don't have 67 policies. We have <strong>one.</strong></p>
<div class="about-policy-box">
<div class="about-policy-icon"></div>
<p>If <strong>100,000 Australians</strong> sign a petition, the government is legally required to hold a public referendum on it. The people vote. The result becomes law. Full stop.</p>
</div>
<p>That means you — not politicians — decide if a bad law gets scrapped. Or if a good one gets added. Any law. On any issue. Housing. Tax. Climate. Corruption. Anything.</p>
<div class="about-divider"></div>
<div class="about-switzerland">
<div class="about-swiss-flag">🇨🇭</div>
<div>
<p class="about-swiss-heading">This already works. Switzerland proved it.</p>
<p>Switzerland has had this system since <strong>1891</strong>. 100,000 signatures forces a binding national vote. Swiss citizens have used it to set their own immigration rules, reject government spending, and keep nuclear power — directly overruling their parliament every time.</p>
<p>It's not radical. It's just democracy that actually works.</p>
</div>
</div>
<div class="about-divider"></div>
<div class="about-steps">
<div class="about-step">
<span class="about-step-num">01</span>
<span>RADICAL gets elected to parliament</span>
</div>
<div class="about-step">
<span class="about-step-num">02</span>
<span>We pass one law: 100k signatures = mandatory referendum</span>
</div>
<div class="about-step">
<span class="about-step-num">03</span>
<span>Every Australian gets real power over every law, forever</span>
</div>
<div class="about-step">
<span class="about-step-num">04</span>
<span>We dissolve. Our job is done.</span>
</div>
</div>
<p class="about-footer-note">Start by posting a petition above. Build 100k votes. That's the proof of concept. That's the movement.</p>
</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>
<section id="constitution" class="constitution-section">
<div class="constitution-header" onclick="toggleConstitution(this)">
<div>
<div class="constitution-tag">LEGAL DOCUMENT</div>
<h2 class="constitution-title">Constitution of The Radical Party</h2>
<span class="constitution-meta">Version 3.0 &nbsp;·&nbsp; Draft for AEC Federal Registration</span>
</div>
<span class="constitution-chevron"></span>
</div>
<div class="constitution-body" id="constitution-body">
<div class="constitution-draft-notice">
<strong>Drafting notes — delete before lodgement</strong>
<ol>
<li>This draft concentrates governing power in a small self-perpetuating <strong>Founding Fathers/Mummies (max 5)</strong> while keeping general membership open and unlimited. The AEC does not require internal democracy; it requires the constitution to cover name, aims, membership, amendment, and winding-up, and requires 1,500 enrolled members. All of that is satisfied here.</li>
<li>This control structure is robust <strong>only if the Party is incorporated as a company limited by guarantee</strong> in which the <em>company's</em> members are the Founding Fathers/Mummies (and only the Council). General "Members" under clause 4 are associational/party members with no Corporations Act standing. Do <strong>not</strong> use an incorporated association for this design — association members get statutory rights you cannot remove. Have a lawyer align this document with the company constitution before lodging.</li>
<li>Bracketed items are choices for you to set.</li>
</ol>
</div>
<p class="constitution-adopted"><em>Adopted 1 July 2026 &nbsp;·&nbsp; Version 3.0 &nbsp;·&nbsp; Draft for AEC federal registration</em></p>
<div class="const-section">
<h3 class="const-h3">1. Name</h3>
<p class="const-clause"><span class="const-num">1.1</span>The name of the party is <strong>The Radical Party</strong> (the "Party").</p>
<p class="const-clause"><span class="const-num">1.2</span>The Party may register and use the abbreviation <strong>"RAD"</strong> and an associated logo, being a depiction of Mickey Mouse making an obscene gesture, as approved by the Founding Fathers/Mummies from time to time.</p>
</div>
<div class="const-section">
<h3 class="const-h3">2. Objects</h3>
<p class="const-clause"><span class="const-num">2.1</span>The Party is an organisation that has, as one of its objects and activities, <strong>the promotion of the election to the Senate and the House of Representatives of candidates endorsed by the Party.</strong></p>
<p class="const-clause"><span class="const-num">2.2</span>The Party has a single substantive platform: <strong>direct democracy.</strong> The Party exists to establish in Australia a system of <strong>Citizens' Initiated Referendums</strong>, by which:</p>
<p class="const-sub">(a) where a law, decision, or proposal is opposed by the public, a petition reaching a defined threshold of electors compels a binding public vote on that matter; and</p>
<p class="const-sub">(b) electors may initiate new law by petition meeting that threshold.</p>
<p class="const-clause"><span class="const-num">2.3</span>The Party is, by design, a <strong>transitional vehicle.</strong> Its ultimate aim is a system in which political power rests directly with the people, such that political parties — including this one — no longer determine outcomes. The Party seeks its own eventual redundancy.</p>
<p class="const-clause"><span class="const-num">2.4</span>The Party pursues these objects by endorsing candidates committed to the platform, advocating the legislative and constitutional reform required to make Citizens' Initiated Referendums binding, and building public support for direct democracy.</p>
<p class="const-clause"><span class="const-num">2.5</span>The Party takes no binding collective position on any matter outside clause 2.2. Members and endorsed candidates are otherwise free to hold their own views.</p>
</div>
<div class="const-section">
<h3 class="const-h3">3. Powers</h3>
<p class="const-clause"><span class="const-num">3.1</span>The Party may do all things necessary or convenient to pursue its objects, including raising and holding funds, holding property, entering contracts, and engaging persons.</p>
</div>
<div class="const-section">
<h3 class="const-h3">4. Members</h3>
<p class="const-clause"><span class="const-num">4.1</span><strong>Open membership.</strong> Membership of the Party is open and unlimited. The Party actively seeks the broadest possible membership.</p>
<p class="const-clause"><span class="const-num">4.2</span><strong>Eligibility.</strong> A person is eligible if they are at least 16 years of age, are entitled to be enrolled on the Commonwealth Electoral Roll, and support the objects of the Party.</p>
<p class="const-clause"><span class="const-num">4.3</span><strong>Becoming a member.</strong> A person becomes a Member by submitting an application in the form approved by the Party (in writing or electronically) that provides their full name, residential address, and contact details, and declares that they support the objects of the Party and consent to membership. Applications are accepted on behalf of the Founding Fathers/Mummies; acceptance may be automatic under a policy set by the Council.</p>
<p class="const-clause"><span class="const-num">4.4</span><strong>Membership fee.</strong> Membership is free unless the Founding Fathers/Mummies sets a fee by resolution.</p>
<p class="const-clause"><span class="const-num">4.5</span><strong>Register of members.</strong> The Secretary maintains a current register of Members. It is confidential and disclosed only as required by law or for dealings with the Australian Electoral Commission.</p>
<p class="const-clause"><span class="const-num">4.6</span><strong>Rights of Members.</strong> A Member may participate in the activities of the Party, advocate for its platform, and vote in any non-binding plebiscite the Founding Fathers/Mummies chooses to put to the membership.</p>
<p class="const-clause"><span class="const-num">4.7</span><strong>Limits on Members' rights.</strong> For the avoidance of doubt, a Member, in that capacity, has <strong>no right to</strong>:</p>
<p class="const-sub">(a) elect, appoint, or remove any member of the Founding Fathers/Mummies;</p>
<p class="const-sub">(b) amend this constitution;</p>
<p class="const-sub">(c) direct or override the Founding Fathers/Mummies; or</p>
<p class="const-sub">(d) select or veto endorsed candidates.</p>
<p class="const-clause"><span class="const-num">4.8</span><strong>Reliance for registration.</strong> A Member may not be relied upon by the Party for the Commonwealth Electoral Act membership requirement while being relied upon by another political party for the same purpose.</p>
<p class="const-clause"><span class="const-num">4.9</span><strong>Resignation and cessation.</strong> A Member may resign at any time by notice to the Secretary. A person ceases to be a Member on resignation, death, loss of eligibility, or termination by the Founding Fathers/Mummies under clause 4.10.</p>
<p class="const-clause"><span class="const-num">4.10</span><strong>Termination.</strong> The Founding Fathers/Mummies may terminate a membership by resolution where the Member acts contrary to the objects or interests of the Party, after giving the Member a reasonable opportunity to respond.</p>
<p class="const-clause"><span class="const-num">4.11</span><strong>No personal liability.</strong> A Member is not, in that capacity, liable for the debts or obligations of the Party.</p>
</div>
<div class="const-section">
<h3 class="const-h3">5. The Founding Fathers/Mummies (Acting Dictators &amp; Supreme Leaders)</h3>
<p class="const-clause"><span class="const-num">5.1</span><strong>The governing body.</strong> The Party is governed exclusively by the <strong>Founding Fathers/Mummies</strong>, which holds all powers of the Party not reserved to it elsewhere in this constitution.</p>
<p class="const-clause"><span class="const-num">5.2</span><strong>Composition.</strong> The Founding Fathers/Mummies consists of exactly <strong>5</strong> persons, each of whom must be a Member.</p>
<p class="const-clause"><span class="const-num">5.3</span><strong>Changes to composition.</strong> A change to the membership of the Founding Fathers/Mummies — whether by appointment, removal, or filling a vacancy — requires <strong>all</strong> of the following:</p>
<p class="const-sub">(a) a vote in favour by at least 51% of the sitting Founding Fathers/Mummies; <strong>and</strong></p>
<p class="const-sub">(b) a vote in favour by at least 51% of the sitting DASSOM agents (clause 5A.2).</p>
<p class="const-clause"><span class="const-num">5.4</span><strong>Exclusive powers.</strong> The Founding Fathers/Mummies has sole authority over the strategy, finances, membership policy, candidate endorsement, officers, and constitution of the Party.</p>
<p class="const-clause"><span class="const-num">5.5</span><strong>Decisions.</strong> All decisions of the Founding Fathers/Mummies require a <strong>unanimous vote</strong> of all 5 sitting members, unless this constitution expressly provides otherwise. If DASSOM has been doxxed under clause 5A.4, all such decisions additionally require a unanimous vote of all 4 sitting DASSOM agents.</p>
</div>
<div class="const-section">
<h3 class="const-h3">5A. DASSOM — The Hidden Council</h3>
<p class="const-clause"><span class="const-num">5A.1</span><strong>Existence.</strong> The Party maintains a covert governing body known as <strong>DASSOM</strong> (the "Hidden Council"). The identities of DASSOM agents are kept strictly confidential except as provided in clause 5A.4. DASSOM consists of exactly <strong>4</strong> agents.</p>
<p class="const-clause"><span class="const-num">5A.2</span><strong>Changes to DASSOM.</strong> A change to DASSOM agentship — whether by appointment, removal, or resignation — requires a vote in favour of at least <strong>3 out of 4</strong> sitting DASSOM agents. Members of the Party and the Founding Fathers/Mummies have no role in DASSOM composition unless clause 5A.4 has been triggered.</p>
<p class="const-clause"><span class="const-num">5A.2A</span><strong>DASSOM Ambassador.</strong> DASSOM shall designate one of its agents as the <strong>DASSOM Ambassador</strong>. The Ambassador is the sole point of external contact between DASSOM and the Milk Man (clause 6). The identity of the Ambassador remains confidential except to the Milk Man. DASSOM may rotate the Ambassador at any time by internal resolution.</p>
<p class="const-clause"><span class="const-num">5A.3</span><strong>Veto over public council changes.</strong> No change to the composition of the Founding Fathers/Mummies is effective without the concurrent approval of at least 51% of sitting DASSOM agents, as required by clause 5.3(b).</p>
<p class="const-clause"><span class="const-num">5A.4</span><strong>Doxxing — compulsory disclosure.</strong> The identities of all sitting DASSOM agents must be publicly disclosed (the "Doxx") if <strong>either</strong> of the following occurs:</p>
<p class="const-sub">(a) at least 51% of the sitting DASSOM agents vote to disclose; <strong>or</strong></p>
<p class="const-sub">(b) 100% of the sitting Founding Fathers/Mummies vote to disclose.</p>
<p class="const-clause"><span class="const-num">5A.5</span><strong>Effect of doxxing.</strong> Upon a Doxx under clause 5A.4: DASSOM continues to exist and retains all its powers; and all decisions of the Founding Fathers/Mummies that would otherwise require a unanimous vote of the Founding Fathers/Mummies also require a unanimous vote of the sitting DASSOM agents.</p>
<p class="const-clause"><span class="const-num">5A.6</span><strong>Meeting restrictions.</strong> No more than <strong>2</strong> DASSOM agents may physically meet or be present in the same location at any one time. This restriction exists to preserve the anonymity of the Hidden Council. DASSOM votes and communications must otherwise occur through secure, indirect means as determined by DASSOM.</p>
<p class="const-clause"><span class="const-num">5A.7</span><strong>Confidentiality.</strong> All persons with knowledge of DASSOM agentship owe a strict duty of confidentiality. Unauthorised disclosure is grounds for termination under clause 4.10 or removal from DASSOM under clause 5A.2, as applicable.</p>
</div>
<div class="const-section">
<h3 class="const-h3">6. The Milk Man</h3>
<p class="const-clause"><span class="const-num">6.1</span><strong>Appointment.</strong> The Founding Fathers/Mummies shall appoint a person to the role of <strong>The Milk Man</strong>. The Milk Man need not be a Member of the Party.</p>
<p class="const-clause"><span class="const-num">6.2</span><strong>Sole responsibility.</strong> The Milk Man's sole and exclusive responsibility is to relay communications between the <strong>DASSOM Ambassador</strong> (being the one DASSOM agent designated by DASSOM for external contact) and the Founding Fathers/Mummies. The Milk Man performs no other function under this constitution.</p>
<p class="const-clause"><span class="const-num">6.3</span><strong>Contact restriction.</strong> The Milk Man shall have contact with exactly <strong>one</strong> DASSOM agent — the DASSOM Ambassador — and no other. The identity of any other DASSOM agent must never be sought, received, or disclosed by the Milk Man.</p>
<p class="const-clause"><span class="const-num">6.4</span><strong>Fidelity of communications.</strong> The Milk Man must relay all communications accurately, completely, and without alteration, omission, embellishment, or editorial commentary.</p>
<p class="const-clause"><span class="const-num">6.5</span><strong>Consequences of dishonesty.</strong> If DASSOM determines by resolution that the Milk Man has been dishonest in the relaying of any communication, the Milk Man shall be subject to <strong>72 lashes</strong>, to be administered by the Members as directed by the Founding Fathers/Mummies.</p>
<p class="const-clause"><span class="const-num">6.6</span><strong>Removal.</strong> The Founding Fathers/Mummies may remove and replace The Milk Man at any time by unanimous resolution.</p>
</div>
<div class="const-section">
<h3 class="const-h3">7. Officers</h3>
<p class="const-clause"><span class="const-num">7.1</span>The Founding Fathers/Mummies appoints from among its members a <strong>Registered Officer</strong>, a <strong>Secretary</strong>, and a <strong>Treasurer</strong>.</p>
<p class="const-clause"><span class="const-num">7.2</span>The <strong>Registered Officer</strong> is the Party's officer for dealings with the Australian Electoral Commission. The <strong>Secretary</strong> is responsible for correspondence, records, the register of Members, and convening meetings. The <strong>Treasurer</strong> is responsible for finances and statutory financial disclosure.</p>
<p class="const-clause"><span class="const-num">7.3</span>One person may hold more than one office where permitted by law, except that the Registered Officer and Secretary should ordinarily be different persons.</p>
</div>
<div class="const-section">
<h3 class="const-h3">8. Endorsement of Candidates</h3>
<p class="const-clause"><span class="const-num">8.1</span>The endorsement of candidates for the Senate and the House of Representatives is the <strong>exclusive</strong> power of the Founding Fathers/Mummies.</p>
<p class="const-clause"><span class="const-num">8.2</span>To be endorsed, a candidate must publicly commit to the Party's direct-democracy platform.</p>
</div>
<div class="const-section">
<h3 class="const-h3">9. Amendment of this Constitution</h3>
<p class="const-clause"><span class="const-num">9.1</span>This constitution may be amended <strong>only</strong> by unanimous resolution of the Founding Fathers/Mummies and, where DASSOM has been doxxed under clause 5A.4, by concurrent unanimous resolution of the sitting DASSOM agents.</p>
<p class="const-clause"><span class="const-num">9.2</span>Members of the Party are actively encouraged to propose amendments by written submission to the Secretary. The Founding Fathers/Mummies must consider any proposal received from a Member before passing an amendment. Members have no right to vote on amendments.</p>
<p class="const-clause"><span class="const-num">9.3</span>The Secretary must notify the Australian Electoral Commission of any amendment affecting the Party's name, objects, or any matter required for registration.</p>
</div>
<div class="const-section">
<h3 class="const-h3">10. Finances</h3>
<p class="const-clause"><span class="const-num">10.1</span>The Party's funds must be applied solely towards its objects. No income or property may be distributed to Members, except as bona fide reimbursement or payment for services.</p>
<p class="const-clause"><span class="const-num">10.2</span>The Founding Fathers/Mummies must ensure the Party complies with all financial disclosure, donation, and authorisation obligations under the <em>Commonwealth Electoral Act 1918</em>.</p>
</div>
<div class="const-section">
<h3 class="const-h3">11. Transition and Devolution</h3>
<p class="const-clause"><span class="const-num">11.1</span>The concentration of power in the Founding Fathers/Mummies is <strong>expressly temporary</strong> and exists only to deliver the Party's platform.</p>
<p class="const-clause"><span class="const-num">11.2</span>If, in the opinion of the Founding Fathers/Mummies, a binding Citizens' Initiated Referendum system has been established in Australia, the Founding Fathers/Mummies must, within a reasonable time, either:</p>
<p class="const-sub">(a) amend this constitution to devolve governance of the Party to its Members on a democratic basis; or</p>
<p class="const-sub">(b) wind up the Party under clause 12.</p>
</div>
<div class="const-section">
<h3 class="const-h3">12. Winding Up and Distribution of Assets</h3>
<p class="const-clause"><span class="const-num">12.1</span>The Party may be wound up by resolution of the Founding Fathers/Mummies passed in accordance with clause 9.1.</p>
<p class="const-clause"><span class="const-num">12.2</span>On winding up, after payment of all debts and liabilities, surplus assets <strong>must not be distributed to Members</strong>. They must be transferred to one or more organisations having objects similar to those of the Party and which prohibit distribution of assets to their own members, as the Founding Fathers/Mummies determines.</p>
</div>
<div class="const-section">
<h3 class="const-h3">13. Disputes and Discipline</h3>
<p class="const-clause"><span class="const-num">13.1</span>The Founding Fathers/Mummies may resolve internal disputes and deal with breaches of this constitution fairly, having given any affected person a reasonable opportunity to be heard. Its decision is final.</p>
</div>
<div class="const-section">
<h3 class="const-h3">14. Blackmail and Dark Forces</h3>
<p class="const-clause"><span class="const-num">14.1</span><strong>Duty to disclose.</strong> A Founding Father/Mummy who is subjected to blackmail, extortion, threats, undue pressure, or any attempt by a third party — including government agencies, intelligence services, corporations, or other dark forces — to improperly influence their conduct is <strong>strongly encouraged</strong> to disclose that fact to the remaining Founding Fathers/Mummies as soon as practicable. No liability attaches to such disclosure.</p>
</div>
<p class="const-end"><em>End of constitution.</em></p>
</div>
</section>
<footer>
<p>RADICAL - Direct Democracy Movement 🇦🇺 &copy; 2026</p>
<div class="footer-links">
<a href="/login">Login</a>
<a href="#about">The Only Policy</a>
<a href="#join">Join The Movement</a>
<a href="#constitution">Constitution</a>
<a href="#events">Events</a>
<a href="mailto:potato@theradicalparty.com">Contact</a>
</div>
<p style="margin-top:10px;font-size:12px;opacity:.4;font-family:'Roboto Mono',monospace">potato@theradicalparty.com</p>
</footer>
</div>
<script>
// Function to detect mobile devices
function isMobileDevice() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
// Function that handles both mobile and desktop sharing
function smartCopyShare(proposalId, proposalText) {
// Get the link to share
const link = getShareableLink(proposalId);
// Check if we're on a mobile device
if (isMobileDevice() && 'share' in navigator) {
// On mobile, use Web Share API
console.log("Mobile device detected - using Web Share API");
// Decode the text if it exists
const text = proposalText ? decodeURIComponent(proposalText) : '';
// Use Web Share API
navigator.share({
title: 'What do you think?',
text: text,
url: link
}).catch(err => {
console.log('Share failed:', err);
// Fall back to clipboard
copyToClipboard(link);
});
} else {
// On desktop/Windows, just copy to clipboard
console.log("Desktop device detected - copying to clipboard");
copyToClipboard(link);
}
}
function getAPIBaseURL() {
const hostname = window.location.hostname;
const protocol = window.location.protocol;
// Always use the main domain for API calls
if (hostname === 'theradicalparty.com' || hostname === 'www.theradicalparty.com') {
return `${protocol}//theradicalparty.com/api`;
} else if (hostname === 'radical-dmd.pages.dev' || hostname.endsWith('.pages.dev')) {
// For preview deployments, use the main domain's API
return `https://theradicalparty.com/api`;
} else if (hostname === 'localhost') {
return 'http://localhost:8787/api';
} else {
// Fallback
return `${protocol}//${hostname}/api`;
}
}
const API_BASE_URL = getAPIBaseURL();
function getMediaURL(path) {
// Always use the main domain for media assets
return `https://theradicalparty.com${path}`;
}
// Character limit
const CHAR_LIMIT = 280;
// Global variables
let currentUser;
let verifiedUsers = {};
let currentProposalId = null;
let currentVoteType = null;
// Catchy political slogans for placeholder text
const placeholders = [
"Drop your revolutionary idea here... (280 character limit)",
"How would you hack Australia's system? (280 character limit)",
"What dank policy should replace the current shit? (280 character limit)",
"Your move to seize democracy from the elites? (280 character limit)",
"Post the change that would make Australia actually based (280 character limit)",
"What would make Australia actually slap? (280 character limit)",
"Drop your 🔥 policy idea here (280 character limit)"
];
// Document Ready Handler
document.addEventListener('DOMContentLoaded', async () => {
// Initialize variables for DOM elements
initializeApp();
// Initialize meme upload
initMemeUpload();
});
// Initialize application
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
try { localStorage.setItem('radical_theme', t); } catch(e) {}
}
function toggleTheme() {
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
applyTheme(next);
if (typeof currentUser !== 'undefined' && currentUser?.id) {
currentUser.theme = next;
try { localStorage.setItem('radical_user', JSON.stringify(currentUser)); } catch(e) {}
}
}
async function initializeApp() {
updateUIText();
await loadUserData();
if (currentUser?.theme) applyTheme(currentUser.theme);
loadVerifiedUsers();
setupEventListeners();
await Promise.allSettled([fetchProposals(), fetchLeaderboard(), fetchStats()]);
checkForProposalInUrl();
if (window.location.hash === '#join') {
document.getElementById('join')?.scrollIntoView({ behavior: 'smooth' });
}
}
// Update UI text for petition system
function updateUIText() {
const tagline = document.querySelector('.tagline');
if (tagline) tagline.textContent = 'Radicalise, Vote, Veto';
const headerDesc = document.querySelector('header p');
if (headerDesc) {
headerDesc.textContent = 'The underground parliament. Post dank memes, build movements, hack the system. Take back control.';
}
const filterTitle = document.querySelector('.filter-title');
if (filterTitle) filterTitle.textContent = 'Active Petitions';
const sortSelect = document.getElementById('sort-select');
if (sortSelect) {
const options = sortSelect.querySelectorAll('option');
if (options[0]) options[0].textContent = 'Latest ideas';
if (options[1]) options[1].textContent = 'Most liked';
if (options[2]) options[2].textContent = 'controversial';
}
const proposalInput = document.getElementById('proposal-input');
if (proposalInput) {
proposalInput.placeholder = 'Drop your revolutionary idea here... (280 character limit)';
}
const postButton = document.getElementById('post-button');
if (postButton) postButton.innerHTML = 'Launch Rebellion';
}
// Load user data
async function loadUserData() {
try {
// User information from localStorage
currentUser = JSON.parse(localStorage.getItem('radical_user'));
// If no user exists, create a new one
if (!currentUser || !currentUser.id) {
console.log("No user found in localStorage, creating new one");
currentUser = {
id: 'citizen_' + Math.random().toString(36).substr(2, 9),
name: 'Citizen ' + Math.floor(Math.random() * 9000 + 1000)
};
// Save user to local storage
localStorage.setItem('radical_user', JSON.stringify(currentUser));
}
console.log("Current user:", currentUser);
// Create or get user in the database - with explicit error handling
const userData = await createUser(currentUser);
console.log("User registration complete:", userData);
return currentUser;
} catch (error) {
console.error("Failed to register user on page load:", error);
// Create emergency backup user if registration fails
const backupUser = {
id: 'emergency_' + Date.now(),
name: 'Emergency User'
};
try {
// Try to register the backup user
await createUser(backupUser);
// If successful, update currentUser and localStorage
currentUser = backupUser;
localStorage.setItem('radical_user', JSON.stringify(currentUser));
console.log("Created emergency backup user:", currentUser);
return currentUser;
} catch (backupError) {
console.error("Even backup user creation failed:", backupError);
showToastMessage("Connection issues - please refresh the page", "#ff0099");
throw backupError;
}
}
}
// Load verified users data
function loadVerifiedUsers() {
verifiedUsers = JSON.parse(localStorage.getItem('radical_verified_users')) || {};
}
// Setup event listeners
function setupEventListeners() {
const proposalInput = document.getElementById('proposal-input');
const charCounter = document.getElementById('char-counter');
const postButton = document.getElementById('post-button');
const sortSelect = document.getElementById('sort-select');
// Rotate placeholder text every 5 seconds
let placeholderIndex = 0;
setInterval(() => {
if (proposalInput) {
placeholderIndex = (placeholderIndex + 1) % placeholders.length;
proposalInput.placeholder = placeholders[placeholderIndex];
}
}, 5000);
// Update character counter
if (proposalInput) {
proposalInput.addEventListener('input', () => {
const remaining = CHAR_LIMIT - proposalInput.value.length;
charCounter.textContent = `${remaining} characters remaining`;
if (remaining < 20) {
charCounter.classList.add('limit');
} else {
charCounter.classList.remove('limit');
}
// Enable/disable post button
postButton.disabled = proposalInput.value.trim().length === 0;
});
// Sort proposals when sort option changes
if (sortSelect) {
sortSelect.addEventListener('change', fetchProposals);
}
}
}
// Create or get user in DB
async function createUser(user) {
try {
console.log("Creating user:", user);
if (!user || !user.id || !user.name) {
throw new Error("Invalid user object - missing id or name");
}
const response = await fetch(`${API_BASE_URL}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: user.id,
name: user.name
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`User creation failed: ${response.status} ${errorText}`);
}
const data = await response.json();
console.log("User creation successful:", data);
return data;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}
// Animation for successful post
function animatePostSuccess() {
const successEl = document.createElement('div');
successEl.textContent = '🔥 Petition created!';
successEl.style.position = 'fixed';
successEl.style.bottom = '20px';
successEl.style.left = '50%';
successEl.style.transform = 'translateX(-50%)';
successEl.style.backgroundColor = 'var(--primary)';
successEl.style.color = 'var(--dark)';
successEl.style.padding = '12px 24px';
successEl.style.borderRadius = '0';
successEl.style.zIndex = '1000';
successEl.style.boxShadow = '0 0 20px rgba(255, 0, 153, 0.5)';
successEl.style.transition = 'all 0.4s cubic-bezier(0.16, 1, 0.3, 1)';
successEl.style.opacity = '0';
successEl.style.fontSize = '14px';
successEl.style.fontWeight = '700';
successEl.style.fontFamily = "'Roboto Mono', monospace";
document.body.appendChild(successEl);
// Fade in
setTimeout(() => {
successEl.style.opacity = '1';
}, 100);
// Fade out
setTimeout(() => {
successEl.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(successEl);
}, 500);
}, 3000);
}
// Show loading indicator
function showLoadingIndicator() {
const proposalsContainer = document.getElementById('proposals-container');
if (proposalsContainer) {
proposalsContainer.innerHTML = `
<div class="loading-indicator">
<div class="loading-spinner"></div>
<p>Loading petitions...</p>
</div>
`;
}
}
// Hide loading indicator
function hideLoadingIndicator() {
const proposalsContainer = document.getElementById('proposals-container');
if (proposalsContainer) {
// Only remove if it exists
const loadingIndicator = proposalsContainer.querySelector('.loading-indicator');
if (loadingIndicator) {
loadingIndicator.remove();
}
}
}
// Show error message
function showErrorMessage(message, isRetryable = true) {
const proposalsContainer = document.getElementById('proposals-container');
if (proposalsContainer) {
proposalsContainer.innerHTML = `
<div class="error-state">
<p>${message}</p>
${isRetryable ? '<button class="retry-button" onclick="fetchProposals()">Try Again</button>' : ''}
</div>
`;
}
}
// Show success/error toast message
function showToastMessage(message, backgroundColor) {
const toastEl = document.createElement('div');
toastEl.textContent = message;
toastEl.style.position = 'fixed';
toastEl.style.bottom = '20px';
toastEl.style.left = '50%';
toastEl.style.transform = 'translateX(-50%)';
toastEl.style.backgroundColor = backgroundColor;
toastEl.style.color = 'var(--dark)';
toastEl.style.padding = '12px 24px';
toastEl.style.borderRadius = '0';
toastEl.style.zIndex = '1000';
toastEl.style.boxShadow = `0 0 20px ${backgroundColor}99`;
toastEl.style.transition = 'all 0.4s cubic-bezier(0.16, 1, 0.3, 1)';
toastEl.style.opacity = '0';
toastEl.style.fontSize = '14px';
toastEl.style.fontWeight = '700';
toastEl.style.fontFamily = "'Roboto Mono', monospace";
document.body.appendChild(toastEl);
// Fade in
setTimeout(() => {
toastEl.style.opacity = '1';
}, 100);
// Fade out
setTimeout(() => {
toastEl.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(toastEl);
}, 500);
}, 3000);
}
// Improved safeFetch implementation with Cloudflare error handling
async function safeFetch(url, options = {}) {
try {
// Ensure URL is a string
const requestUrl = typeof url === 'string' ? url : url.toString();
// Default options
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
mode: 'cors',
credentials: 'same-origin'
};
// Merge provided options with defaults
const fetchOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...(options.headers || {})
}
};
// Log fetch details for debugging
console.log('Fetch Request:', {
url: requestUrl,
method: fetchOptions.method,
headers: Object.keys(fetchOptions.headers)
});
// Perform fetch with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
fetchOptions.signal = controller.signal;
const response = await window.fetch(requestUrl, fetchOptions);
clearTimeout(timeoutId);
// Check for successful response
if (response.status < 200 || response.status >= 300) {
let errorText;
try {
const errorData = await response.json();
errorText = errorData.error || JSON.stringify(errorData);
} catch (e) {
errorText = await response.text();
}
// Check if this is a Cloudflare error
if (errorText.includes('Cloudflare') || errorText.includes('cf-error')) {
console.error('Cloudflare Error:', {
status: response.status,
statusText: response.statusText,
body: errorText
});
// Return a special error object for Cloudflare errors
return {
error: true,
cloudflareError: true,
status: response.status,
message: 'The server is temporarily unavailable. Please try again later.'
};
}
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}
try {
return await response.json();
} catch (jsonError) {
console.error('Error parsing JSON response:', jsonError);
throw new Error('Invalid response format from server');
}
} catch (error) {
console.error('Fetch Error:', {
message: error.message,
name: error.name,
stack: error.stack
});
// Handle specific error types
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
}
}
function initializeShareButtons() {}
// Updated fetchProposals with fallback mechanism
async function fetchProposals() {
try {
// Show loading indicator
showLoadingIndicator();
const sortSelect = document.getElementById('sort-select');
const sortBy = sortSelect ? sortSelect.value : 'newest';
// Log the full request URL and parameters
const requestUrl = `${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser?.id || 'anonymous'}`;
console.log('Fetching proposals from:', requestUrl);
console.log('Current user:', currentUser);
const response = await fetch(requestUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'cors',
credentials: 'same-origin',
cache: 'no-store'
});
// Log the response details
console.log('Response status:', response.status);
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
if (!response.ok) {
let errorText;
try {
const errorData = await response.json();
console.error('Error response JSON:', errorData);
errorText = errorData.error || errorData.details?.message || JSON.stringify(errorData);
} catch (e) {
errorText = await response.text();
console.error('Error response text:', errorText);
}
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
}
let data;
try {
data = await response.json();
} catch (jsonError) {
console.error('Error parsing JSON response:', jsonError);
throw new Error('Invalid JSON response from server');
}
console.log('Proposals data received:', {
count: data?.data?.length || 0,
pagination: data?.pagination
});
if (!data || !data.data || !Array.isArray(data.data)) {
console.error('Invalid data structure:', data);
throw new Error('Invalid data structure received from server');
}
// Hide loading indicator before rendering proposals
hideLoadingIndicator();
renderProposals(data);
} catch (error) {
console.error('Error fetching proposals:', error);
showErrorMessage('Failed to load petitions. Please check the console for details.', true);
}
}
// Function to render all proposals
function renderProposals(response) {
const proposalsContainer = document.getElementById('proposals-container');
proposalsContainer.innerHTML = '';
// Clear loading indicator
hideLoadingIndicator();
// Handle empty or missing data
if (!response || !response.data || !Array.isArray(response.data) || response.data.length === 0) {
proposalsContainer.innerHTML = `
<div class="no-proposals">
<h3>No petitions found</h3>
<p>Be the first to launch a rebellion!</p>
</div>
`;
return;
}
// Access the data array from the response
const proposals = response.data;
// Create and append proposal cards
proposals.forEach(proposal => {
const proposalCard = createProposalCard(proposal);
proposalsContainer.appendChild(proposalCard);
});
// Activate share buttons
initializeShareButtons();
// Start CTA cycling for comment buttons
startCTACycling();
}
// Function to create a proposal card from proposal data
function createProposalCard(proposal) {
const netVotes = proposal.upvotes - proposal.downvotes;
const voteClass = netVotes > 0 ? 'positive' : netVotes < 0 ? 'negative' : '';
const proposalButton = document.createElement('a');
proposalButton.href = getShareableLink(proposal.id);
proposalButton.addEventListener('click', e => {
e.stopPropagation();
});
const proposalCard = document.createElement('div');
proposalCard.className = 'proposal-card';
proposalButton.appendChild(proposalCard);
// Badge for trending or power proposals
const badgeHtml = proposal.trending ?
`<div class="power-badge trending-badge">🔥 Trending</div>` :
(netVotes > 5 ? `<div class="power-badge">💯 Popular</div>` : '');
// Add petition signatures info if proposal has upvotes/signatures
const petitionInfo = proposal.upvotes > 0 ?
`<div class="petition-info">
<span class="petition-icon">✓</span>
<span class="petition-count">${proposal.petition_signatures || proposal.upvotes}</span>
<span>verified signatures</span>
</div>` : '';
// Add meme if available
const memeHtml = proposal.meme_url ?
`<div class="proposal-meme">
<img src="${proposal.meme_url}" alt="Meme for petition" onerror="this.style.display='none'" />
</div>` : '';
const milestoneHtml = getMilestoneBarHtml(proposal.upvotes || 0);
const postcodeBadge = proposal.postcode_count > 0
? `<span class="postcode-badge">across <span>${proposal.postcode_count}</span> postcode${proposal.postcode_count > 1 ? 's' : ''}</span>`
: '';
proposalCard.innerHTML = `
${badgeHtml}
<div class="proposal-author">${proposal.author_name}</div>
<div class="proposal-text">${proposal.text}</div>
${memeHtml}
${petitionInfo}
<div class="card-milestone">${milestoneHtml}</div>
<div class="proposal-stats">
<div class="vote-buttons">
<button class="vote-button ${proposal.userVote === 'upvote' ? 'upvoted' : ''}" data-proposal-id="${proposal.id}" data-vote-type="upvote" onclick="event.stopPropagation(); event.preventDefault();">
<span class="vote-icon">▲</span>
<span>${proposal.upvotes || 0}</span>
</button>
<button class="vote-button ${proposal.userVote === 'downvote' ? 'downvoted' : ''}" data-proposal-id="${proposal.id}" data-vote-type="downvote" onclick="event.stopPropagation(); event.preventDefault();">
<span class="vote-icon">▼</span>
<span>${proposal.downvotes || 0}</span>
</button>
<span class="net-votes ${voteClass}">${netVotes > 0 ? '+' : ''}${netVotes}</span>
${postcodeBadge}
<button class="card-action-btn" onclick="event.stopPropagation();event.preventDefault();smartCopyShare('${proposal.id}','${encodeURIComponent(proposal.text)}')">share ↗</button>
<button class="card-action-btn" onclick="event.stopPropagation();event.preventDefault();window.open('${API_BASE_URL}/petition-svg?id=${proposal.id}','_blank')">card ↗</button>
</div>
<div class="proposal-time">${formatDate(proposal.timestamp)}</div>
</div>
<div class="comments-section">
<button class="comments-toggle" id="comments-toggle-${proposal.id}" onclick="event.stopPropagation(); event.preventDefault();">
<span class="comments-toggle-icon">▶</span> 0 comments
</button>
<div class="comments-container" data-proposal-id="${proposal.id}" id="comments-container-${proposal.id}" style="display: none;">
<div class="comments-content">
<div class="comments-loading">Loading comments...</div>
</div>
<div class="comment-form">
<textarea class="comment-input" id="comment-input-${proposal.id}" placeholder="Add your comment..." onclick="event.stopPropagation(); event.preventDefault();"></textarea>
<button class="comment-submit" data-proposal-id="${proposal.id}" onclick="event.stopPropagation(); event.preventDefault();">${getRandomCTA()}</button>
</div>
</div>
</div>
`;
// Add event listeners for vote buttons
const upvoteButton = proposalCard.querySelector('[data-vote-type="upvote"]');
const downvoteButton = proposalCard.querySelector('[data-vote-type="downvote"]');
if (upvoteButton) {
upvoteButton.addEventListener('click', () => {
handleVote(proposal.id, 'upvote');
});
}
if (downvoteButton) {
downvoteButton.addEventListener('click', () => {
handleVote(proposal.id, 'downvote');
});
}
// Add comments toggle functionality
const commentsToggle = proposalCard.querySelector(`#comments-toggle-${proposal.id}`);
const commentsContainer = proposalCard.querySelector(`#comments-container-${proposal.id}`);
if (commentsToggle && commentsContainer) {
commentsToggle.addEventListener('click', function() {
const isVisible = commentsContainer.style.display !== 'none';
if (isVisible) {
commentsContainer.style.display = 'none';
this.querySelector('.comments-toggle-icon').textContent = '▶';
} else {
commentsContainer.style.display = 'block';
this.querySelector('.comments-toggle-icon').textContent = '▼';
loadComments(proposal.id);
}
});
}
// Add event listener for comment submission
const commentSubmit = proposalCard.querySelector(`.comment-submit[data-proposal-id="${proposal.id}"]`);
const commentInput = proposalCard.querySelector(`#comment-input-${proposal.id}`);
if (commentSubmit && commentInput) {
commentSubmit.addEventListener('click', function() {
const commentText = commentInput.value.trim();
if (commentText) {
// Store original text and disable button while submitting
const originalText = this.textContent;
this.textContent = "Sending...";
this.disabled = true;
submitComment(proposal.id, commentText)
.then(() => {
commentInput.value = '';
// Set a new random CTA after submission
this.textContent = getRandomCTA();
this.disabled = false;
})
.catch(() => {
// Restore original CTA if there was an error
this.textContent = originalText;
this.disabled = false;
});
}
});
// Enable/disable comment button based on input
commentInput.addEventListener('input', function() {
commentSubmit.disabled = this.value.trim() === '';
});
}
return proposalButton;
}
// Handle voting
async function handleVote(proposalId, voteType) {
currentProposalId = proposalId;
currentVoteType = voteType;
// TEMPORARILY DISABLED AEC VERIFICATION
// Simply submit the vote directly regardless of vote type
/* Original code commented out
// Check if user needs to provide AEC details for petition signing
if (voteType === 'upvote') {
// Check if user already verified for this vote
if (verifiedUsers[currentUser.id] && verifiedUsers[currentUser.id][proposalId]) {
// User already verified for this proposal, proceed with vote
submitVote(proposalId, voteType, true, verifiedUsers[currentUser.id][proposalId].userData);
return;
}
// Show verification modal for petition signing
showVerificationModal(proposalId);
} else {
// Downvotes don't require verification
submitVote(proposalId, voteType, false);
}
*/
// Find the proposal card
const proposalCards = document.querySelectorAll(`.proposal-card`);
let proposalCard = null;
for (const card of proposalCards) {
if (card.querySelector(`[data-proposal-id="${proposalId}"]`)) {
proposalCard = card;
break;
}
}
if (!proposalCard) return;
// Get the buttons and count elements
const upvoteButton = proposalCard.querySelector(`[data-vote-type="upvote"]`);
const downvoteButton = proposalCard.querySelector(`[data-vote-type="downvote"]`);
const upvoteCount = upvoteButton.querySelector('span:nth-child(2)');
const downvoteCount = downvoteButton.querySelector('span:nth-child(2)');
const netVotesElement = proposalCard.querySelector('.net-votes');
// Get current state
const wasUpvoted = upvoteButton.classList.contains('upvoted');
const wasDownvoted = downvoteButton.classList.contains('downvoted');
// Get current counts
let upvotes = parseInt(upvoteCount.textContent);
let downvotes = parseInt(downvoteCount.textContent);
// Update UI optimistically
if (voteType === 'upvote') {
if (wasUpvoted) {
// Toggle off upvote
upvoteButton.classList.remove('upvoted');
upvoteCount.textContent = upvotes - 1;
} else {
// Add upvote
upvoteButton.classList.add('upvoted');
upvoteCount.textContent = upvotes + 1;
// If was downvoted, remove downvote
if (wasDownvoted) {
downvoteButton.classList.remove('downvoted');
downvoteCount.textContent = downvotes - 1;
}
}
} else if (voteType === 'downvote') {
if (wasDownvoted) {
// Toggle off downvote
downvoteButton.classList.remove('downvoted');
downvoteCount.textContent = downvotes - 1;
} else {
// Add downvote
downvoteButton.classList.add('downvoted');
downvoteCount.textContent = downvotes + 1;
// If was upvoted, remove upvote
if (wasUpvoted) {
upvoteButton.classList.remove('upvoted');
upvoteCount.textContent = upvotes - 1;
}
}
}
// Update net votes
const newUpvotes = parseInt(upvoteCount.textContent);
const newDownvotes = parseInt(downvoteCount.textContent);
const netVotes = newUpvotes - newDownvotes;
netVotesElement.textContent = netVotes > 0 ? `+${netVotes}` : netVotes;
netVotesElement.className = `net-votes ${netVotes > 0 ? 'positive' : netVotes < 0 ? 'negative' : ''}`;
// Also update the modal if it's open and showing this proposal
updateModalVoteUI(proposalId, voteType, wasUpvoted, wasDownvoted);
// Track votes and trigger AEC membership modal
const vc = (parseInt(localStorage.getItem('radical_vote_count') || '0')) + 1;
localStorage.setItem('radical_vote_count', String(vc));
const alreadyJoined = localStorage.getItem('radical_member_joined');
if (!alreadyJoined) {
// Trigger on first upvote (petition) OR 3rd vote of any kind
if (voteType === 'upvote' || vc >= 3) {
setTimeout(showMembershipModal, 600);
}
}
// Submit vote to server in the background
try {
await submitVoteToServer(proposalId, voteType, voteType === 'upvote');
} catch (error) {
console.error('Error voting:', error);
showToastMessage('Hmm, your vote didn\'t count. That\'s a bit suspicious, isn\'t it?', '#ff0099');
// Revert UI changes
if (voteType === 'upvote') {
upvoteButton.classList.toggle('upvoted', wasUpvoted);
upvoteCount.textContent = upvotes;
} else if (voteType === 'downvote') {
downvoteButton.classList.toggle('downvoted', wasDownvoted);
downvoteCount.textContent = downvotes;
}
// Restore net votes
const originalNetVotes = upvotes - downvotes;
netVotesElement.textContent = originalNetVotes > 0 ? `+${originalNetVotes}` : originalNetVotes;
netVotesElement.className = `net-votes ${originalNetVotes > 0 ? 'positive' : originalNetVotes < 0 ? 'negative' : ''}`;
}
}
// New function to update vote UI in the modal (if open)
function updateModalVoteUI(proposalId, voteType, wasUpvoted, wasDownvoted) {
const modalUpvoteButton = document.getElementById('modal-upvote');
const modalDownvoteButton = document.getElementById('modal-downvote');
if (!modalUpvoteButton || !modalDownvoteButton) return;
// Make sure we're updating the right proposal
if (modalUpvoteButton.getAttribute('data-proposal-id') !== proposalId) return;
const modalUpvoteCount = modalUpvoteButton.querySelector('span:nth-child(2)');
const modalDownvoteCount = modalDownvoteButton.querySelector('span:nth-child(2)');
const modalNetVotesElement = document.querySelector('.modal-body .net-votes');
if (!modalUpvoteCount || !modalDownvoteCount || !modalNetVotesElement) return;
let upvotes = parseInt(modalUpvoteCount.textContent);
let downvotes = parseInt(modalDownvoteCount.textContent);
// Update UI in the modal
if (voteType === 'upvote') {
if (wasUpvoted) {
modalUpvoteButton.classList.remove('upvoted');
modalUpvoteCount.textContent = upvotes - 1;
} else {
modalUpvoteButton.classList.add('upvoted');
modalUpvoteCount.textContent = upvotes + 1;
if (wasDownvoted) {
modalDownvoteButton.classList.remove('downvoted');
modalDownvoteCount.textContent = downvotes - 1;
}
}
} else if (voteType === 'downvote') {
if (wasDownvoted) {
modalDownvoteButton.classList.remove('downvoted');
modalDownvoteCount.textContent = downvotes - 1;
} else {
modalDownvoteButton.classList.add('downvoted');
modalDownvoteCount.textContent = downvotes + 1;
if (wasUpvoted) {
modalUpvoteButton.classList.remove('upvoted');
modalUpvoteCount.textContent = upvotes - 1;
}
}
}
// Update net votes in modal
const newUpvotes = parseInt(modalUpvoteCount.textContent);
const newDownvotes = parseInt(modalDownvoteCount.textContent);
const netVotes = newUpvotes - newDownvotes;
modalNetVotesElement.textContent = netVotes > 0 ? `+${netVotes}` : netVotes;
modalNetVotesElement.className = `net-votes ${netVotes > 0 ? 'positive' : netVotes < 0 ? 'negative' : ''}`;
}
// New function to submit vote to server
async function submitVoteToServer(proposalId, voteType, isPetition = false, petitionDetails = null) {
const voteData = {
proposalId: proposalId,
userId: currentUser.id,
voteType: voteType
};
// Add petition data if this is a petition signature
if (isPetition && petitionDetails) {
voteData.isPetition = true;
voteData.petitionDetails = petitionDetails;
}
return await safeFetch(`${API_BASE_URL}/votes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(voteData),
mode: 'cors',
credentials: 'same-origin'
});
}
// Replace the old submitVote function
async function submitVote(proposalId, voteType, isPetition = false, petitionDetails = null) {
try {
return await submitVoteToServer(proposalId, voteType, isPetition, petitionDetails);
} catch (error) {
console.error('Error voting:', error);
showToastMessage('Hmm, your vote didn\'t count. That\'s a bit suspicious, isn\'t it?', '#ff0099');
throw error;
}
}
// Format date
function formatDate(timestamp) {
const now = new Date();
const date = new Date(timestamp);
const diffMs = now - date;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
if (diffSec < 60) {
return 'just now';
} else if (diffMin < 60) {
return diffMin + (diffMin === 1 ? ' minute ago' : ' minutes ago');
} else if (diffHour < 24) {
return diffHour + (diffHour === 1 ? ' hour ago' : ' hours ago');
} else if (diffDay < 30) {
return diffDay + (diffDay === 1 ? ' day ago' : ' days ago');
} else {
return date.toLocaleDateString();
}
}
// Render proposals - FIXED VERSION
// function renderProposals(proposals) {
// Removing duplicate implementation
// This function has been moved and implemented at line 762
// }
// Create meme upload elements
function createMemeUpload() {
const uploadContainer = document.createElement('div');
uploadContainer.className = 'meme-upload';
const uploadLabel = document.createElement('label');
uploadLabel.className = 'meme-upload-label';
uploadLabel.setAttribute('for', 'meme-upload-input');
const uploadIcon = document.createElement('img');
uploadIcon.className = 'meme-upload-icon';
uploadIcon.src = 'https://theradicalparty.com/memes/pepe.jpg';
uploadIcon.alt = 'Pepe';
uploadIcon.style.width = '20px';
uploadIcon.style.height = '20px';
uploadIcon.style.marginRight = '8px';
const uploadText = document.createElement('span');
uploadText.className = 'meme-upload-text';
uploadText.textContent = 'Add a meme to your rebellion (optional)';
const uploadInput = document.createElement('input');
uploadInput.className = 'meme-upload-input';
uploadInput.id = 'meme-upload-input';
uploadInput.type = 'file';
uploadInput.accept = 'image/jpeg, image/png, image/gif, image/webp';
uploadLabel.appendChild(uploadIcon);
uploadLabel.appendChild(uploadText);
uploadLabel.appendChild(uploadInput);
uploadContainer.appendChild(uploadLabel);
const previewContainer = document.createElement('div');
previewContainer.className = 'meme-preview';
previewContainer.style.display = 'none';
const previewImage = document.createElement('img');
previewImage.id = 'meme-preview-image';
const removeButton = document.createElement('button');
removeButton.className = 'meme-remove';
removeButton.innerHTML = '×';
removeButton.setAttribute('aria-label', 'Remove meme');
const progressContainer = document.createElement('div');
progressContainer.className = 'meme-upload-progress';
progressContainer.style.display = 'none';
const progressBar = document.createElement('div');
progressBar.className = 'meme-upload-progress-bar';
progressContainer.appendChild(progressBar);
previewContainer.appendChild(previewImage);
previewContainer.appendChild(removeButton);
uploadContainer.appendChild(previewContainer);
uploadContainer.appendChild(progressContainer);
return {
container: uploadContainer,
input: uploadInput,
preview: previewContainer,
image: previewImage,
removeBtn: removeButton,
progress: progressContainer,
progressBar: progressBar
};
}
// Initialize meme upload
function initMemeUpload() {
const newProposalSection = document.querySelector('.new-proposal');
if (!newProposalSection) return;
const charCounter = document.getElementById('char-counter');
if (!charCounter) return;
// Create meme upload elements
const memeUpload = createMemeUpload();
// Insert after character counter, before post button
charCounter.after(memeUpload.container);
// Variable to store the selected file
let selectedMeme = null;
// Handle file selection
memeUpload.input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
// Validate file type
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validTypes.includes(file.type)) {
showToastMessage('⚠️ Invalid file type. Only JPEG, PNG, GIF, and WebP images are allowed.', '#ff0099');
return;
}
// Validate file size (limit to 2MB)
const MAX_SIZE = 2 * 1024 * 1024; // 2MB
if (file.size > MAX_SIZE) {
showToastMessage('⚠️ File too large. Maximum size is 2MB.', '#ff0099');
return;
}
// Store the file
selectedMeme = file;
// Show preview
const reader = new FileReader();
reader.onload = (event) => {
memeUpload.image.src = event.target.result;
memeUpload.preview.style.display = 'flex';
};
reader.readAsDataURL(file);
});
// Handle remove button
memeUpload.removeBtn.addEventListener('click', () => {
selectedMeme = null;
memeUpload.input.value = '';
memeUpload.preview.style.display = 'none';
memeUpload.image.src = '';
});
// Modify post button click handler to include meme upload
const postButton = document.getElementById('post-button');
if (postButton) {
// Store the original click event handler
const originalClickHandler = postButton.onclick;
// Remove the original handler
postButton.onclick = null;
// Add new handler
postButton.addEventListener('click', async () => {
const proposalInput = document.getElementById('proposal-input');
const text = proposalInput?.value.trim();
if (!text) return;
// Show loading state
postButton.disabled = true;
postButton.innerHTML = 'Launching... <div class="loader"></div>';
try {
// Verify user exists first
if (!currentUser || !currentUser.id) {
console.log("No current user - creating one");
await loadUserData(); // Reinitialize user
}
console.log("Posting with user:", currentUser);
// Create the proposal with verified user
const trending = Math.random() > 0.8;
const proposalData = {
authorId: currentUser.id,
text: text,
trending: trending
};
console.log("Creating proposal:", proposalData);
// Add better error handling and logging
const createResponse = await fetch(`${API_BASE_URL}/proposals`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(proposalData),
mode: 'cors',
credentials: 'same-origin'
});
console.log("Create response status:", createResponse.status);
console.log("Create response headers:", Object.fromEntries(createResponse.headers.entries()));
if (!createResponse.ok) {
let errorData;
try {
errorData = await createResponse.json();
console.error("Error response JSON:", errorData);
} catch (parseError) {
const errorText = await createResponse.text();
console.error("Error response text:", errorText);
errorData = { error: errorText || 'Unknown error' };
}
throw new Error(`Failed to create proposal: ${errorData.error || createResponse.statusText}`);
}
let proposalResult;
try {
proposalResult = await createResponse.json();
console.log("Proposal created successfully:", proposalResult);
} catch (jsonError) {
console.error("Error parsing success response:", jsonError);
throw new Error("Server returned invalid response format");
}
// Get the proposal ID from the result
const proposalId = proposalResult.id;
if (!proposalId) {
console.error("Missing proposal ID in response:", proposalResult);
throw new Error("Server returned incomplete response");
}
// If there's a meme, upload it
if (selectedMeme) {
// Show progress bar
memeUpload.progress.style.display = 'block';
memeUpload.progressBar.style.width = '0%';
// Create form data for file upload
const formData = new FormData();
formData.append('proposalId', proposalId);
formData.append('meme', selectedMeme);
// Track upload progress
const xhr = new XMLHttpRequest();
xhr.open('POST', `${API_BASE_URL}/memes`);
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
memeUpload.progressBar.style.width = percent + '%';
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
// Upload successful
memeUpload.progress.style.display = 'none';
// Clear the meme upload
selectedMeme = null;
memeUpload.input.value = '';
memeUpload.preview.style.display = 'none';
memeUpload.image.src = '';
//Clear Loader
postButton.innerHTML = 'Launch Rebellion';
// Show success message
animatePostSuccess();
// Clear input
proposalInput.value = '';
charCounter.textContent = `${CHAR_LIMIT} characters remaining`;
postButton.disabled = true;
// Refresh proposals
fetchProposals();
} else {
// Upload failed
memeUpload.progress.style.display = 'none';
console.error('Meme upload failed:', xhr.responseText);
showToastMessage('Failed to upload meme. Please try again.', '#ff0099');
}
});
xhr.addEventListener('error', () => {
memeUpload.progress.style.display = 'none';
console.error('Meme upload network error');
showToastMessage('Failed to upload meme. Please check your connection.', '#ff0099');
});
// Send the upload
xhr.send(formData);
} else {
// No meme to upload, just show success and refresh
// Clear input
postButton.innerHTML = 'Launch Rebellion';
proposalInput.value = '';
charCounter.textContent = `${CHAR_LIMIT} characters remaining`;
postButton.disabled = true;
// Show success message
animatePostSuccess();
// Refresh proposals
fetchProposals();
}
} catch (error) {
console.error('Error creating proposal:', error);
showToastMessage('Failed to post your proposal. Please try again.', '#ff0099');
} finally {
// Always reset button state
postButton.disabled = false;
postButton.innerHTML = 'Launch Rebellion';
}
});
}
}
// Display the modal for petition verification
function showVerificationModal(proposalId) {
// Create the modal element
const modalOverlay = document.createElement('div');
modalOverlay.className = 'modal-overlay';
const modalContainer = document.createElement('div');
modalContainer.className = 'modal-container';
// Modal header
const modalHeader = document.createElement('div');
modalHeader.className = 'modal-header';
const modalTitle = document.createElement('h3');
modalTitle.textContent = 'Sign Official Petition';
const closeButton = document.createElement('button');
closeButton.className = 'modal-close';
closeButton.textContent = '✕';
closeButton.setAttribute('aria-label', 'Close modal');
modalHeader.appendChild(modalTitle);
modalHeader.appendChild(closeButton);
// Modal body
const modalBody = document.createElement('div');
modalBody.className = 'modal-body';
// Find the proposal text to display in the modal
const proposalCard = document.querySelector(`[data-proposal-id="${proposalId}"]`).closest('.proposal-card');
const proposalText = proposalCard.querySelector('.proposal-text').textContent;
// Current proposal context
const petitionContext = document.createElement('p');
petitionContext.innerHTML = `<strong>You are officially petitioning for:</strong>
<div style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.05); font-style: italic;">"${proposalText}"</div>
<strong>Your details will be verified with the Australian Electoral Commission (AEC).</strong>`;
const formElement = document.createElement('form');
formElement.className = 'petition-form';
// Form fields
const fields = [
{ id: 'full-name', label: 'Full Name (as registered with AEC)', type: 'text', required: true },
{ id: 'address', label: 'Residential Address', type: 'text', required: true },
{ id: 'postcode', label: 'Postcode', type: 'text', required: true, pattern: '\\d{4}' },
{ id: 'dob', label: 'Date of Birth', type: 'date', required: true },
{ id: 'email', label: 'Email Address', type: 'email', required: true }
];
fields.forEach(field => {
const formGroup = document.createElement('div');
formGroup.className = 'form-group';
const label = document.createElement('label');
label.setAttribute('for', field.id);
label.textContent = field.label;
const input = document.createElement('input');
input.className = 'form-input';
input.id = field.id;
input.type = field.type;
input.required = field.required;
if (field.pattern) {
input.pattern = field.pattern;
}
const errorSpan = document.createElement('span');
errorSpan.className = 'form-error';
errorSpan.id = `${field.id}-error`;
formGroup.appendChild(label);
formGroup.appendChild(input);
formGroup.appendChild(errorSpan);
formElement.appendChild(formGroup);
});
// Consent checkbox
const consentDiv = document.createElement('div');
consentDiv.className = 'consent-checkbox';
const consentCheckbox = document.createElement('input');
consentCheckbox.type = 'checkbox';
consentCheckbox.id = 'petition-consent';
consentCheckbox.required = true;
const consentLabel = document.createElement('label');
consentLabel.setAttribute('for', 'petition-consent');
consentLabel.innerHTML = 'I certify that I am an Australian citizen or eligible resident, registered to vote, and consent to my information being used for this official petition. I also certify that I am excited to join the radical party as a member and take back our sexy country.';
consentDiv.appendChild(consentCheckbox);
consentDiv.appendChild(consentLabel);
formElement.appendChild(consentDiv);
// Legal info
const infoText = document.createElement('p');
infoText.className = 'info-text';
infoText.textContent = 'Your details will be verified with the AEC database. A valid petition requires accurate information matching your electoral enrollment. Multiple signatures from the same person are prohibited.';
formElement.appendChild(infoText);
// Submit button
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.className = 'petition-submit-btn';
submitButton.textContent = 'Sign Petition';
submitButton.disabled = true;
formElement.appendChild(submitButton);
modalBody.appendChild(petitionContext);
modalBody.appendChild(formElement);
modalContainer.appendChild(modalHeader);
modalContainer.appendChild(modalBody);
modalOverlay.appendChild(modalContainer);
document.body.appendChild(modalOverlay);
// Form validation
const inputs = formElement.querySelectorAll('input:not([type="checkbox"])');
const checkValidation = () => {
let isValid = true;
inputs.forEach(input => {
if (!input.validity.valid) {
isValid = false;
}
});
if (!consentCheckbox.checked) {
isValid = false;
}
submitButton.disabled = !isValid;
};
inputs.forEach(input => {
input.addEventListener('input', checkValidation);
// Show specific error messages
input.addEventListener('invalid', (e) => {
e.preventDefault();
const errorSpan = document.getElementById(`${input.id}-error`);
if (input.validity.valueMissing) {
errorSpan.textContent = 'This field is required';
} else if (input.validity.patternMismatch && input.id === 'postcode') {
errorSpan.textContent = 'Please enter a valid 4-digit postcode';
} else if (input.validity.typeMismatch && input.type === 'email') {
errorSpan.textContent = 'Please enter a valid email address';
} else {
errorSpan.textContent = 'Please enter valid information';
}
});
});
consentCheckbox.addEventListener('change', checkValidation);
// Close button functionality
closeButton.addEventListener('click', () => {
document.body.removeChild(modalOverlay);
});
// Form submission
formElement.addEventListener('submit', async (e) => {
e.preventDefault();
// Show loading state
submitButton.disabled = true;
submitButton.textContent = 'Verifying...';
// Collect form data
const formData = {
fullName: document.getElementById('full-name').value,
address: document.getElementById('address').value,
postcode: document.getElementById('postcode').value,
dob: document.getElementById('dob').value,
email: document.getElementById('email').value,
consent: document.getElementById('petition-consent').checked
};
// Verify with AEC (simulated)
await verifyWithAEC(formData, proposalId);
});
}
// Verify user details with AEC (simulated)
async function verifyWithAEC(userData, proposalId) {
try {
// Show loading state with timeout to simulate verification
await new Promise(resolve => setTimeout(resolve, 1500));
// In a real implementation, this would call your server which would verify with AEC APIs
// For demo purposes, we'll simulate a successful verification
// Store the verified state
if (!verifiedUsers[currentUser.id]) {
verifiedUsers[currentUser.id] = {};
}
// Mark this user as verified for this proposal
verifiedUsers[currentUser.id][proposalId] = {
verified: true,
timestamp: Date.now(),
userData: userData
};
// Save verification data to localStorage (in real app, this would be in your database)
localStorage.setItem('radical_verified_users', JSON.stringify(verifiedUsers));
// Remove the modal
const modal = document.querySelector('.modal-overlay');
document.body.removeChild(modal);
// Show success message
showToastMessage('✓ Petition signed successfully!', '#11cc77');
// Finally submit the vote with petition details
submitVote(proposalId, currentVoteType, true, userData);
} catch (error) {
console.error('Verification error:', error);
showToastMessage('⚠️ Verification failed. Please try again.', '#ff0099');
// Reset the form
const submitButton = document.querySelector('.petition-submit-btn');
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = 'Sign Petition';
}
}
}
async function checkForProposalInUrl() {
const urlParams = new URLSearchParams(window.location.search);
const proposalId = urlParams.get('proposal');
if (proposalId) {
try {
// Show loading state
showToastMessage('Loading petition...', '#11cc77');
// Fetch the specific proposal
const response = await fetch(`${API_BASE_URL}/proposals/${proposalId}?userId=${currentUser ? currentUser.id : 'anonymous'}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch proposal: ${response.status} ${errorText}`);
}
const proposal = await response.json();
if (proposal) {
// Create and show the modal with the proposal data
createProposalViewModal(proposal);
} else {
showToastMessage('⚠️ Petition not found', '#ff0099');
}
} catch (error) {
console.error('Error fetching proposal:', error);
showToastMessage('⚠️ Failed to load petition', '#ff0099');
}
}
}
// Generate a shareable link for a proposal
function getShareableLink(proposalId) {
const baseUrl = window.location.origin + window.location.pathname;
return `${baseUrl}?proposal=${proposalId}`;
}
// Copy link to clipboard and show feedback
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
showToastMessage('✓ Link copied to clipboard!', '#11cc77');
// Find all share buttons and reset them
const shareButtons = document.querySelectorAll('.share-button');
shareButtons.forEach(button => {
button.classList.remove('copied');
});
// Find the specific button that was clicked (if any)
const clickedButton = document.querySelector(`.share-button[data-proposal-id="${text.split('proposal=')[1]}"]`);
if (clickedButton) {
clickedButton.classList.add('copied');
setTimeout(() => {
clickedButton.classList.remove('copied');
}, 2000);
}
})
.catch(err => {
console.error('Failed to copy: ', err);
showToastMessage('⚠️ Failed to copy link', '#ff0099');
});
}
// Create a modal to display when a proposal is opened via direct link
function createProposalViewModal(proposal) {
// Update meta tags for sharing
const ogImageUrl = proposal.share_image_url;
const ogImageElement = document.querySelector('meta[property="og:image"]');
if (ogImageElement) {
ogImageElement.setAttribute('content', ogImageUrl);
}
const twitterImageElement = document.querySelector('meta[name="twitter:image"]');
if (twitterImageElement) {
twitterImageElement.setAttribute('content', ogImageUrl);
}
// Create modal elements
const modalOverlay = document.createElement('div');
modalOverlay.className = 'modal-overlay';
const modalContainer = document.createElement('div');
modalContainer.className = 'modal-container proposal-view';
// Modal header
const modalHeader = document.createElement('div');
modalHeader.className = 'modal-header';
const modalTitle = document.createElement('h3');
modalTitle.textContent = 'Petition Details';
const closeButton = document.createElement('button');
closeButton.className = 'modal-close';
closeButton.textContent = '✕';
closeButton.setAttribute('aria-label', 'Close modal');
modalHeader.appendChild(modalTitle);
modalHeader.appendChild(closeButton);
// Modal body
const modalBody = document.createElement('div');
modalBody.className = 'modal-body';
// Add petition info
const netVotes = proposal.upvotes - proposal.downvotes;
const voteClass = netVotes > 0 ? 'positive' : netVotes < 0 ? 'negative' : '';
const badgeHtml = proposal.trending ?
`<div class="power-badge trending-badge" style="position: static; display: inline-block; margin-bottom: 15px;">🔥 Trending</div>` :
(netVotes > 5 ? `<div class="power-badge" style="position: static; display: inline-block; margin-bottom: 15px;">💯 Popular</div>` : '');
modalBody.innerHTML = `
${badgeHtml}
<div class="proposal-author" style="font-size: 1.1rem;">${proposal.author_name}</div>
<div class="proposal-text" style="font-size: 1.3rem; margin: 20px 0;">${proposal.text}</div>
${proposal.meme_url ? `<div class="proposal-meme">
<img src="${proposal.meme_url}" alt="Meme for petition" onerror="this.style.display='none'" />
</div>` : ''}
${proposal.petition_signatures > 0 ? `<div class="petition-info" style="margin: 20px 0;">
<span class="petition-icon">✓</span>
<span class="petition-count">${proposal.petition_signatures || proposal.upvotes}</span>
<span>verified signatures</span>
</div>` : ''}
<div class="proposal-stats" style="margin-top: 20px; padding-top: 20px;">
<div class="vote-buttons">
<button class="vote-button ${proposal.userVote === 'upvote' ? 'upvoted' : ''}" id="modal-upvote" data-proposal-id="${proposal.id}" data-vote-type="upvote">
<span class="vote-icon">▲</span>
<span>${proposal.upvotes || 0}</span>
</button>
<button class="vote-button ${proposal.userVote === 'downvote' ? 'downvoted' : ''}" id="modal-downvote" data-proposal-id="${proposal.id}" data-vote-type="downvote">
<span class="vote-icon">▼</span>
<span>${proposal.downvotes || 0}</span>
</button>
<span class="net-votes ${voteClass}">${netVotes > 0 ? '+' : ''}${netVotes}</span>
</div>
<div class="proposal-time">${formatDate(proposal.timestamp)}</div>
</div>
<div style="margin-top: 25px; border-top: 1px solid var(--border); padding-top: 20px;">
<p style="margin-bottom: 15px; font-size: 0.9rem;">Share this petition:</p>
<div class="share-link-container">
<input type="text" class="share-link-field" value="${getShareableLink(proposal.id)}" readonly>
<button class="share-copy-button" id="modal-copy-link">Copy</button>
</div>
`
+
(
('share' in navigator) ?`
<div class="social-share-buttons">
<button class="social-share-button" onclick="navigator.share({ title: 'What do you think?', text: '${proposal.text}', url: '${getShareableLink(proposal.id)}' })">
<span>Share</span>
</button>
</div>
`
: `
<div class="social-share-buttons">
<button class="social-share-button" id="share-twitter">
<span class="social-icon">🐦</span>
<span>Twitter</span>
</button>
<button class="social-share-button" id="share-facebook">
<span class="social-icon">📘</span>
<span>Facebook</span>
</button>
<button class="social-share-button" id="share-whatsapp">
<span class="social-icon">📱</span>
<span>WhatsApp</span>
</button>
</div>
`
)
+
`
</div>
`;
modalBody.innerHTML += `
<div class="comments-section" style="margin-top: 30px;">
<h4 style="margin-bottom: 15px; color: var(--primary);">Comments</h4>
<div class="comments-container show" id="modal-comments-container-${proposal.id}" data-proposal-id="${proposal.id}">
<div class="comment-form" id="modal-comment-form-${proposal.id}">
<textarea class="comment-input" id="modal-comment-input-${proposal.id}" placeholder="Add a comment..."></textarea>
<button type="submit" class="comment-submit" id="modal-comment-submit-${proposal.id}" disabled>Defy</button>
</div>
<div class="comments-content" id="modal-comments-content-${proposal.id}">
<div class="comments-loading">Loading comments...</div>
</div>
</div>
</div>
`;
modalContainer.appendChild(modalHeader);
modalContainer.appendChild(modalBody);
modalOverlay.appendChild(modalContainer);
document.body.appendChild(modalOverlay);
// Add event listeners
closeButton.addEventListener('click', () => {
document.body.removeChild(modalOverlay);
// Update URL to remove the proposal parameter without page reload
const newUrl = window.location.pathname;
window.history.pushState({}, '', newUrl);
});
const copyLinkButton = document.getElementById('modal-copy-link');
if (copyLinkButton) {
copyLinkButton.addEventListener('click', () => {
const linkField = document.querySelector('.share-link-field');
linkField.select();
copyToClipboard(getShareableLink(proposal.id));
// Add flash animation
copyLinkButton.classList.add('flash-animation');
setTimeout(() => {
copyLinkButton.classList.remove('flash-animation');
}, 500);
});
}
// Add event listeners for vote buttons
const upvoteButton = document.getElementById('modal-upvote');
const downvoteButton = document.getElementById('modal-downvote');
if (upvoteButton) {
upvoteButton.addEventListener('click', () => {
handleVote(proposal.id, 'upvote');
});
}
if (downvoteButton) {
downvoteButton.addEventListener('click', () => {
handleVote(proposal.id, 'downvote');
});
}
// Add event listeners for social share buttons
const twitterButton = document.getElementById('share-twitter');
const facebookButton = document.getElementById('share-facebook');
const whatsappButton = document.getElementById('share-whatsapp');
if (twitterButton) {
twitterButton.addEventListener('click', () => {
const text = `Check out this RADICAL petition: "${proposal.text.substring(0, 100)}${proposal.text.length > 100 ? '...' : ''}"`;
const url = getShareableLink(proposal.id);
window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`, '_blank');
});
}
if (facebookButton) {
facebookButton.addEventListener('click', () => {
const url = getShareableLink(proposal.id);
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, '_blank');
});
}
if (whatsappButton) {
whatsappButton.addEventListener('click', () => {
const text = `Check out this RADICAL petition: "${proposal.text.substring(0, 100)}${proposal.text.length > 100 ? '...' : ''}"`;
const url = getShareableLink(proposal.id);
window.open(`https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`, '_blank');
});
}
// Handle comment form in modal
const modalCommentInput = document.getElementById(`modal-comment-input-${proposal.id}`);
const modalCommentSubmit = document.getElementById(`modal-comment-submit-${proposal.id}`);
if (modalCommentSubmit) {
modalCommentSubmit.textContent = getRandomCTA();
}
if (modalCommentInput && modalCommentSubmit) {
// Enable/disable submit button based on input content
modalCommentInput.addEventListener('input', () => {
modalCommentSubmit.disabled = modalCommentInput.value.trim().length === 0;
});
// Handle form submission
modalCommentSubmit.addEventListener('click', () => {
const commentText = modalCommentInput.value.trim();
if (commentText) {
submitComment(proposal.id, commentText);
modalCommentInput.value = '';
modalCommentSubmit.disabled = true;
}
});
}
// Load comments for the modal
loadModalComments(proposal.id);
}
function loadModalComments(proposalId) {
loadComments(proposalId);
}
function renderModalComments(container, comments) {
if (!comments || comments.length === 0) {
container.innerHTML = `
<div class="no-comments">
No comments yet. Be the first to comment!
</div>
`;
return;
}
let commentsHtml = '';
comments.forEach(comment => {
const upvotedClass = comment.userVote === 'upvote' ? 'upvoted' : '';
const downvotedClass = comment.userVote === 'downvote' ? 'downvoted' : '';
commentsHtml += `
<div class="comment-item" data-comment-id="${comment.id}">
<div class="comment-header">
<div class="comment-author">${comment.user_name}</div>
<div class="comment-time">${formatDate(comment.timestamp)}</div>
</div>
<div class="comment-text">${formatCommentText(comment.comment_text)}</div>
<div class="comment-vote-buttons">
<button class="comment-vote-button ${upvotedClass}" data-comment-id="${comment.id}" data-vote-type="upvote" onclick="handleCommentVote('${comment.id}', 'upvote')">
<span class="comment-vote-icon">▲</span>
<span class="comment-vote-count">${comment.upvotes || 0}</span>
</button>
<button class="comment-vote-button ${downvotedClass}" data-comment-id="${comment.id}" data-vote-type="downvote" onclick="handleCommentVote('${comment.id}', 'downvote')">
<span class="comment-vote-icon">▼</span>
<span class="comment-vote-count">${comment.downvotes || 0}</span>
</button>
</div>
</div>
`;
});
container.innerHTML = commentsHtml;
}
// 4. Add function to handle comment votes
async function handleCommentVote(commentId, voteType) {
if (!commentId || !voteType) return;
// Find all instances of this comment (may be in modal and in main feed)
const commentItems = document.querySelectorAll(`.comment-item[data-comment-id="${commentId}"]`);
if (commentItems.length === 0) return;
// Get the buttons and counts for all instances
commentItems.forEach(commentItem => {
const upvoteButton = commentItem.querySelector(`.comment-vote-button[data-vote-type="upvote"]`);
const downvoteButton = commentItem.querySelector(`.comment-vote-button[data-vote-type="downvote"]`);
const upvoteCount = upvoteButton.querySelector('.comment-vote-count');
const downvoteCount = downvoteButton.querySelector('.comment-vote-count');
// Get current state
const wasUpvoted = upvoteButton.classList.contains('upvoted');
const wasDownvoted = downvoteButton.classList.contains('downvoted');
// Get current counts
let upvotes = parseInt(upvoteCount.textContent);
let downvotes = parseInt(downvoteCount.textContent);
// Update UI optimistically
if (voteType === 'upvote') {
if (wasUpvoted) {
// Toggle off upvote
upvoteButton.classList.remove('upvoted');
upvoteCount.textContent = upvotes - 1;
} else {
// Add upvote
upvoteButton.classList.add('upvoted');
upvoteCount.textContent = upvotes + 1;
// If was downvoted, remove downvote
if (wasDownvoted) {
downvoteButton.classList.remove('downvoted');
downvoteCount.textContent = downvotes - 1;
}
}
} else if (voteType === 'downvote') {
if (wasDownvoted) {
// Toggle off downvote
downvoteButton.classList.remove('downvoted');
downvoteCount.textContent = downvotes - 1;
} else {
// Add downvote
downvoteButton.classList.add('downvoted');
downvoteCount.textContent = downvotes + 1;
// If was upvoted, remove upvote
if (wasUpvoted) {
upvoteButton.classList.remove('upvoted');
upvoteCount.textContent = upvotes - 1;
}
}
}
});
// Submit vote to server
try {
await safeFetch(`${API_BASE_URL}/comment-votes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
commentId: commentId,
userId: currentUser.id,
voteType: voteType
})
});
} catch (error) {
console.error('Error voting on comment:', error);
showToastMessage('Hmm, your comment vote didn\'t count. Please try again.', '#ff0099');
// Revert UI changes by reloading the comments
// This is a simple approach; you could be more sophisticated by reverting just the affected elements
await loadComments(currentProposalId);
}
}
// Function to toggle comments visibility
function toggleComments(proposalId, button) {
const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
const isExpanded = button.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
button.setAttribute('aria-expanded', 'false');
commentsContainer.classList.remove('show');
} else {
button.setAttribute('aria-expanded', 'true');
commentsContainer.classList.add('show');
loadComments(proposalId);
}
}
// Function to load comments for a proposal (feed cards and modals)
async function loadComments(proposalId) {
// Show loading state immediately in all matching containers (feed + modal)
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (content) content.innerHTML = `<div class="comments-loading">Loading...</div>`;
});
const userId = currentUser?.id || 'anonymous';
try {
const res = await fetch(
`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${userId}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, mode: 'cors', credentials: 'same-origin' }
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (content) renderComments(content, data);
});
// Update toggle arrow + count, preserving open/closed arrow direction
const toggle = document.getElementById(`comments-toggle-${proposalId}`);
if (toggle) {
const arrow = toggle.querySelector('.comments-toggle-icon')?.textContent || '▼';
const count = Array.isArray(data) ? data.length : 0;
toggle.innerHTML = `<span class="comments-toggle-icon">${arrow}</span> ${count} ${count === 1 ? 'comment' : 'comments'}`;
}
} catch (err) {
console.error('Error loading comments:', err);
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (content) content.innerHTML = `<div class="no-comments">Could not load comments.</div>`;
});
}
}
// Function to render comments
function renderComments(container, comments) {
if (!comments || comments.length === 0) {
container.innerHTML = `
<div class="no-comments">
No comments yet. Be the first to comment!
</div>
`;
return;
}
let commentsHtml = '';
comments.forEach(comment => {
const upvotedClass = comment.userVote === 'upvote' ? 'upvoted' : '';
const downvotedClass = comment.userVote === 'downvote' ? 'downvoted' : '';
commentsHtml += `
<div class="comment-item" data-comment-id="${comment.id}">
<div class="comment-header">
<div class="comment-author">${comment.user_name}</div>
<div class="comment-time">${formatDate(comment.timestamp)}</div>
</div>
<div class="comment-text">${formatCommentText(comment.comment_text)}</div>
<div class="comment-vote-buttons">
<button class="comment-vote-button ${upvotedClass}" data-comment-id="${comment.id}" data-vote-type="upvote" onclick="handleCommentVote('${comment.id}', 'upvote')">
<span class="comment-vote-icon">▲</span>
<span class="comment-vote-count">${comment.upvotes || 0}</span>
</button>
<button class="comment-vote-button ${downvotedClass}" data-comment-id="${comment.id}" data-vote-type="downvote" onclick="handleCommentVote('${comment.id}', 'downvote')">
<span class="comment-vote-icon">▼</span>
<span class="comment-vote-count">${comment.downvotes || 0}</span>
</button>
</div>
</div>
`;
});
container.innerHTML = commentsHtml;
}
// Function to format comment text (converts URLs to links, etc.)
function formatCommentText(text) {
// Escape HTML to prevent XSS
const escaped = text.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// Convert URLs to clickable links
const withLinks = escaped.replace(
/(https?:\/\/[^\s]+)/g,
'<a href="$1" target="_blank" rel="noopener noreferrer" style="color:var(--primary);text-decoration:underline;">$1</a>'
);
return withLinks;
}
async function submitComment(proposalId, commentText) {
if (!commentText || !commentText.trim()) return;
if (!currentUser || !currentUser.id) await loadUserData();
// Optimistically render the comment immediately so it appears instant
const optimisticComment = {
id: 'optimistic_' + Date.now(),
user_name: currentUser.name || 'You',
comment_text: commentText,
timestamp: Date.now(),
upvotes: 0,
downvotes: 0,
userVote: null
};
document.querySelectorAll(`.comments-container[data-proposal-id="${proposalId}"]`).forEach(container => {
const content = container.querySelector('.comments-content');
if (!content) return;
const noComments = content.querySelector('.no-comments');
if (noComments) content.innerHTML = '';
const div = document.createElement('div');
div.className = 'comment-item optimistic';
div.style.opacity = '0.6';
div.innerHTML = `
<div class="comment-header">
<div class="comment-author">${optimisticComment.user_name}</div>
<div class="comment-time">just now</div>
</div>
<div class="comment-text">${formatCommentText(commentText)}</div>`;
content.prepend(div);
});
try {
const res = await fetch(`${API_BASE_URL}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ proposalId, userId: currentUser.id, commentText }),
mode: 'cors',
credentials: 'same-origin'
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${res.status}`);
}
// Replace optimistic with real data from server
loadComments(proposalId);
showToastMessage('Comment posted!', '#11cc77');
} catch (err) {
// Remove the optimistic comment on failure
document.querySelectorAll('.comment-item.optimistic').forEach(el => el.remove());
showToastMessage('Failed to post comment', '#ff0099');
throw err;
}
}
// Function to check if comment input is not empty
function checkCommentInput(input, submitButton) {
const hasContent = input.value.trim().length > 0;
submitButton.disabled = !hasContent;
}
// Addition to renderProposals function - Add comments section to each proposal card
// Array of cool CTAs that fit the radical theme
const commentCTAs = [
"Defy",
"Disrupt",
"Rebel",
"Agitate",
"Incite",
"Revolt",
"Dissent",
"Subvert",
"Condemn",
"Provoke",
"Expose",
"Overthrow",
"Resist",
"Sabotage",
"Infiltrate",
"Mobilize",
"Instigate",
"Counterstrike",
"Deconstruct",
"Destabilize",
"Undermine",
"Detonate",
"Defame",
"Shoot down"
];
// Function to get a random CTA from the array
function getRandomCTA() {
const randomIndex = Math.floor(Math.random() * commentCTAs.length);
return commentCTAs[randomIndex];
}
// Function to cycle through CTAs for all comment buttons
function startCTACycling() {
// Find all comment submit buttons
const commentSubmitButtons = document.querySelectorAll('.comment-submit');
// Set initial random CTAs
commentSubmitButtons.forEach(button => {
button.textContent = getRandomCTA();
});
// Change CTAs periodically
setInterval(() => {
commentSubmitButtons.forEach(button => {
// Only change CTA if button is not disabled (to avoid changing while submitting)
if (!button.disabled) {
button.textContent = getRandomCTA();
}
});
}, 30000); // Change every 30 seconds
}
// Modified addCommentsToProposal function to use random CTAs
function addCommentsToProposal(proposalCard, proposalId) {
// Check if comments section already exists to avoid duplicates
const existingComments = proposalCard.querySelector('.comments-section');
if (existingComments) return; // If comments section already exists, don't add another one
const commentsSection = document.createElement('div');
commentsSection.className = 'comments-section';
const commentsToggle = document.createElement('button');
commentsToggle.className = 'comments-toggle';
commentsToggle.id = `comments-toggle-${proposalId}`;
commentsToggle.innerHTML = `<span class="comments-toggle-icon">▶</span> 0 comments`;
const commentsContainer = document.createElement('div');
commentsContainer.className = 'comments-container';
commentsContainer.id = `comments-container-${proposalId}`;
commentsContainer.style.display = 'none';
const commentsList = document.createElement('div');
commentsList.className = 'comments-list';
commentsList.id = `comments-list-${proposalId}`;
const commentForm = document.createElement('div');
commentForm.className = 'comment-form';
const commentInput = document.createElement('textarea');
commentInput.className = 'comment-input';
commentInput.id = `comment-input-${proposalId}`;
commentInput.placeholder = 'Add your comment...';
const commentSubmit = document.createElement('button');
commentSubmit.className = 'comment-submit';
commentSubmit.dataset.proposalId = proposalId;
commentSubmit.textContent = getRandomCTA(); // Use random CTA instead of "Submit"
commentForm.appendChild(commentInput);
commentForm.appendChild(commentSubmit);
commentsContainer.appendChild(commentsList);
commentsContainer.appendChild(commentForm);
commentsSection.appendChild(commentsToggle);
commentsSection.appendChild(commentsContainer);
proposalCard.appendChild(commentsSection);
// Add event listener to toggle button
commentsToggle.addEventListener('click', function(e) {
e.stopPropagation(); // Prevent bubbling up to parent elements
const isVisible = commentsContainer.style.display !== 'none';
if (isVisible) {
commentsContainer.style.display = 'none';
this.querySelector('.comments-toggle-icon').textContent = '▶';
} else {
commentsContainer.style.display = 'block';
this.querySelector('.comments-toggle-icon').textContent = '▼';
loadComments(proposalId);
}
});
// Add event listener for comment submission
commentSubmit.addEventListener('click', function(e) {
e.stopPropagation(); // Prevent bubbling up to parent elements
const commentInput = document.getElementById(`comment-input-${proposalId}`);
const commentText = commentInput?.value.trim();
if (commentText) {
// Temporarily change text to show submission is in progress
const originalText = this.textContent;
this.textContent = "Sending...";
this.disabled = true;
submitComment(proposalId, commentText).then(() => {
commentInput.value = '';
// Set a new random CTA after submission
this.textContent = getRandomCTA();
this.disabled = false;
}).catch(() => {
// Restore original CTA if there was an error
this.textContent = originalText;
this.disabled = false;
});
}
});
}
// Music player functionality
document.addEventListener('DOMContentLoaded', () => {
// Set up variables
const backgroundMusic = document.getElementById('background-music');
const musicControls = document.getElementById('music-controls');
const musicToggle = document.getElementById('music-toggle');
const musicPrompt = document.getElementById('music-prompt');
let musicInitialized = false;
// Make sure the musicPrompt element exists
if (!musicPrompt) {
console.error("Music prompt element not found!");
return;
}
// Make sure it's visible and clickable
musicPrompt.style.opacity = '1';
musicPrompt.style.pointerEvents = 'auto';
musicPrompt.style.cursor = 'pointer';
musicPrompt.classList.add('visible');
// Log when the element is clicked for debugging
console.log("Music prompt element found, adding click listener");
// Audio file URL - using a more reliable source
const audioUrl = 'https://theradicalparty.com/memes/notlikeus.mp3';
// Initialize music only when the "hit me" button is clicked
musicPrompt.addEventListener('click', function(e) {
console.log("Hit me button clicked!");
e.preventDefault();
e.stopPropagation();
initializeMusic();
});
function initializeMusic() {
console.log("Initializing music...");
if (musicInitialized) {
console.log("Music already initialized, returning");
return;
}
// Set the audio source
backgroundMusic.src = audioUrl;
console.log("Set audio source to:", audioUrl);
// Load the audio
backgroundMusic.load();
console.log("Audio loaded");
try {
// Play the audio (this may fail due to browser autoplay policies)
console.log("Attempting to play audio...");
playAudio();
// Show the music controls
musicControls.style.display = 'flex';
// Hide the prompt
musicPrompt.style.display = 'none';
// Add event listener for the toggle button
musicToggle.addEventListener('click', toggleMusic);
// Mark as initialized
musicInitialized = true;
console.log("Music initialized successfully");
} catch (error) {
console.error("Error initializing music:", error);
showToastMessage("Failed to load audio. Please try again later.", "#ff0099");
}
}
function playAudio() {
console.log("Playing audio...");
backgroundMusic.play().then(() => {
console.log("Audio playing successfully");
// Use a span wrapper to apply the animation
musicToggle.innerHTML = '<span class="vinyl-spinning">📀</span>';
musicToggle.classList.remove('pulse');
}).catch(error => {
console.error('Failed to play audio:', error);
musicToggle.innerHTML = '<span class="vinyl-ready">📻</span>';
musicToggle.classList.add('pulse');
showToastMessage("Audio playback failed. Please try again.", "#ff0099");
});
}
function toggleMusic() {
if (backgroundMusic.paused) {
playAudio();
} else {
backgroundMusic.pause();
musicToggle.innerHTML = '<span class="vinyl-ready">📻</span>';
musicToggle.classList.add('pulse');
}
}
// Add error event listener to handle audio loading failures
backgroundMusic.addEventListener('error', (e) => {
console.error('Audio error:', e);
showToastMessage("Failed to load audio. Please try again later.", "#ff0099");
musicControls.style.display = 'none';
musicPrompt.style.display = 'block';
musicInitialized = false;
});
});
document.addEventListener('DOMContentLoaded', () => {
// Run after a short delay to ensure all buttons are rendered
setTimeout(startCTACycling, 1000);
});
// ─── STATS BAR ────────────────────────────────────────────────────────────
async function fetchStats() {
try {
const data = await safeFetch(`${API_BASE_URL}/stats`);
const fmt = n => n >= 1000 ? (n/1000).toFixed(1) + 'k' : String(n);
document.getElementById('stat-votes').textContent = fmt(data.votes_today || 0);
document.getElementById('stat-users').textContent = fmt(data.total_users || 0);
document.getElementById('stat-proposals').textContent = fmt(data.total_proposals || 0);
} catch(e) { /* silent */ }
}
// ─── MILESTONE HELPERS ────────────────────────────────────────────────────
const MILESTONES = [100, 500, 1000, 5000, 10000];
const MILESTONE_ACTIONS = {
100: 'Going viral 🔥',
500: 'Community backed 💪',
1000: 'Published publicly 📢',
5000: 'Written to minister 📬',
10000: 'Mass media push 📺'
};
function getMilestoneInfo(upvotes) {
for (let i = 0; i < MILESTONES.length; i++) {
if (upvotes < MILESTONES[i]) {
const prev = MILESTONES[i - 1] || 0;
const pct = Math.round(((upvotes - prev) / (MILESTONES[i] - prev)) * 100);
return { next: MILESTONES[i], pct, label: MILESTONE_ACTIONS[MILESTONES[i]], reached: false };
}
}
return { next: null, pct: 100, label: MILESTONE_ACTIONS[10000], reached: true };
}
function getMilestoneBarHtml(upvotes) {
const m = getMilestoneInfo(upvotes);
if (m.reached) return `<div class="milestone-bar-wrap"><div class="milestone-label"><span>✓ All milestones reached</span></div></div>`;
return `<div class="milestone-bar-wrap">
<div class="milestone-bar-track"><div class="milestone-bar-fill" style="width:${m.pct}%"></div></div>
<div class="milestone-label">${upvotes}/${m.next} → <span>${m.label}</span></div>
</div>`;
}
// ─── LEADERBOARD ──────────────────────────────────────────────────────────
async function fetchLeaderboard() {
try {
const data = await safeFetch(`${API_BASE_URL}/leaderboard?userId=${currentUser?.id || ''}`);
renderLeaderboard(data.data || []);
} catch(e) {
document.getElementById('leaderboard-container').innerHTML = '<div style="padding:12px;font-size:12px;color:#555;font-family:monospace">Could not load leaderboard.</div>';
}
}
function renderLeaderboard(proposals) {
const container = document.getElementById('leaderboard-container');
if (!proposals.length) {
container.innerHTML = '<div style="padding:12px;font-size:12px;color:#555;font-family:monospace">No proposals yet. Be first.</div>';
return;
}
container.innerHTML = proposals.map((p, i) => {
const m = getMilestoneInfo(p.upvotes || 0);
const postcodeTxt = p.postcode_count > 0 ? `<span class="leaderboard-postcode">${p.postcode_count} postcodes</span>` : '';
return `<div class="leaderboard-item" onclick="openProposal('${p.id}')">
<div style="display:flex;align-items:flex-start;gap:8px">
<span class="leaderboard-rank">${i + 1}.</span>
<span class="leaderboard-text">${p.text.substring(0, 120)}${p.text.length > 120 ? '...' : ''}</span>
</div>
<div class="leaderboard-meta">
<span class="leaderboard-upvotes">▲ ${p.upvotes || 0}</span>
${postcodeTxt}
<span style="color:#444;font-size:10px">${formatDate(p.timestamp)}</span>
</div>
${getMilestoneBarHtml(p.upvotes || 0)}
</div>`;
}).join('');
}
function openProposal(proposalId) {
fetch(`${API_BASE_URL}/proposals/${proposalId}?userId=${currentUser?.id || 'anonymous'}`)
.then(r => r.json())
.then(proposal => { if (proposal && proposal.id) createProposalViewModal(proposal); })
.catch(() => {});
}
// ─── AEC MEMBERSHIP MODAL ─────────────────────────────────────────────────
function showMembershipModal() {
if (document.querySelector('.membership-modal-overlay')) return;
const overlay = document.createElement('div');
overlay.className = 'membership-modal-overlay';
overlay.innerHTML = `
<div class="membership-modal">
<div class="membership-modal-header">
<h3>Become an Official Member</h3>
<button class="membership-modal-close" id="mm-close">✕</button>
</div>
<p class="membership-modal-intro">Your votes and petitions carry more weight as a verified RADICAL member. We collect these details to meet Australian Electoral Commission requirements for registered political parties.</p>
<form id="mm-form" autocomplete="on">
<div class="mm-row">
<div class="mm-field">
<label for="mm-fullname">Full Name (as on electoral roll) *</label>
<input type="text" id="mm-fullname" class="mm-input" required placeholder="Jane Smith" autocomplete="name">
</div>
<div class="mm-field">
<label for="mm-dob">Date of Birth *</label>
<input type="date" id="mm-dob" class="mm-input" required>
</div>
</div>
<div class="mm-field">
<label for="mm-address">Residential Address *</label>
<input type="text" id="mm-address" class="mm-input" required placeholder="123 Rebellion St" autocomplete="street-address">
</div>
<div class="mm-row">
<div class="mm-field">
<label for="mm-suburb">Suburb *</label>
<input type="text" id="mm-suburb" class="mm-input" required placeholder="Sydney" autocomplete="address-level2">
</div>
<div class="mm-field mm-field--xs">
<label for="mm-state">State *</label>
<select id="mm-state" class="mm-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="mm-field mm-field--xs">
<label for="mm-postcode">Postcode *</label>
<input type="text" id="mm-postcode" class="mm-input" required placeholder="2000" maxlength="4" pattern="\\d{4}" inputmode="numeric">
</div>
</div>
<div class="mm-row">
<div class="mm-field">
<label for="mm-email">Email Address *</label>
<input type="email" id="mm-email" class="mm-input" required placeholder="you@example.com" autocomplete="email">
</div>
<div class="mm-field">
<label for="mm-phone">Phone (optional)</label>
<input type="tel" id="mm-phone" class="mm-input" placeholder="04xx xxx xxx" autocomplete="tel">
</div>
</div>
<div class="mm-check">
<input type="checkbox" id="mm-roll" required>
<label for="mm-roll">I am enrolled to vote in Australian federal elections (or eligible to enrol) *</label>
</div>
<div class="mm-check">
<input type="checkbox" id="mm-consent" required>
<label for="mm-consent">I consent to joining the RADICAL Party as a member and to my details being used for AEC party registration *</label>
</div>
<div id="mm-msg" class="mm-msg"></div>
<button type="submit" class="mm-submit" id="mm-submit">JOIN THE RADICAL PARTY →</button>
</form>
<button class="mm-later" id="mm-later">I'll do this later</button>
</div>`;
document.body.appendChild(overlay);
document.getElementById('mm-close').addEventListener('click', () => dismissMembershipModal(overlay));
document.getElementById('mm-later').addEventListener('click', () => dismissMembershipModal(overlay));
document.getElementById('mm-form').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('mm-submit');
const msg = document.getElementById('mm-msg');
msg.textContent = '';
const dob = document.getElementById('mm-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 = 'mm-msg mm-error';
return;
}
btn.disabled = true;
btn.textContent = 'Submitting...';
try {
const res = await fetch(`${API_BASE_URL}/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fullName: document.getElementById('mm-fullname').value.trim(),
dob,
address: document.getElementById('mm-address').value.trim(),
suburb: document.getElementById('mm-suburb').value.trim(),
state: document.getElementById('mm-state').value,
postcode: document.getElementById('mm-postcode').value.trim(),
email: document.getElementById('mm-email').value.trim(),
phone: document.getElementById('mm-phone').value.trim(),
onRoll: document.getElementById('mm-roll').checked,
consent: document.getElementById('mm-consent').checked
})
});
const data = await res.json();
if (res.ok && data.success) {
localStorage.setItem('radical_member_joined', '1');
showToastMessage('✓ Welcome to RADICAL. Vibe, Vote, Veto.', '#11cc77');
document.body.removeChild(overlay);
} else {
throw new Error(data.error || 'Submission failed');
}
} catch(err) {
msg.textContent = '⚠ ' + (err.message || 'Something went wrong. Try again.');
msg.className = 'mm-msg mm-error';
btn.disabled = false;
btn.textContent = 'JOIN THE RADICAL PARTY →';
}
});
}
function dismissMembershipModal(overlay) {
localStorage.setItem('radical_member_joined', 'later');
document.body.removeChild(overlay);
}
// ─── FLAG / REPORT ────────────────────────────────────────────────────────
async function handleFlag(proposalId, btn) {
if (btn.classList.contains('flagged')) return;
if (!confirm('Report this proposal as harmful or spam?')) return;
try {
await safeFetch(`${API_BASE_URL}/flag`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proposalId, userId: currentUser.id })
});
btn.classList.add('flagged');
btn.title = 'Reported';
showToastMessage('Reported. The community will review it.', '#11cc77');
} catch(e) {
showToastMessage('Could not report — try again', '#ff0099');
}
}
function toggleConstitution(header) {
const body = document.getElementById('constitution-body');
const chevron = header.querySelector('.constitution-chevron');
const open = body.classList.toggle('open');
chevron.classList.toggle('open', open);
}
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>
<div class="music-controls" style="display: none;" id="music-controls">
<button class="music-button" id="music-toggle" aria-label="Play or pause music">
📀
</button>
<span class="music-label">MC Brunden</span>
</div>
<audio id="background-music" loop>
<!-- The src will be set via JavaScript after user interaction -->
</audio>
<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "e9dafdf5d3814f3fbbbc98a67b7e5519"}'></script><!-- End Cloudflare Web Analytics -->
</body>
</html>