3328 lines
No EOL
153 KiB
HTML
3328 lines
No EOL
153 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<!-- Title -->
|
||
<title>RADICAL - The Underground Parliament</title>
|
||
<meta name="title" content="RADICAL - The Underground Parliament">
|
||
<meta property="og:title" content="RADICAL - The Underground Parliament">
|
||
<meta name="twitter:title" content="RADICAL - The Underground Parliament">
|
||
<meta itemprop="name" content="RADICAL - The Underground Parliament">
|
||
<meta property="og:site_name" content="RADICAL">
|
||
<meta name="application-name" content="RADICAL">
|
||
<meta name="twitter:domain" content="RADICAL">
|
||
|
||
<!-- Description -->
|
||
<meta property="og:description"
|
||
content="Australia's direct democracy movement. Share policy ideas, vote on proposals, take back control. Vibe, Vote, Veto.">
|
||
<meta name="description" content="Australia's direct democracy movement. Share policy ideas, vote on proposals, take back control. Vibe, Vote, Veto.">
|
||
<meta name="twitter:description"
|
||
content="Australia's direct democracy movement. Share policy ideas, vote on proposals, take back control. Vibe, Vote, Veto.">
|
||
<meta name="abstract"
|
||
content="Australia's direct democracy movement. Share policy ideas, vote on proposals, take back control. Vibe, Vote, Veto.">
|
||
<meta itemprop="description"
|
||
content="Australia's direct democracy movement. Share policy ideas, vote on proposals, take back control. Vibe, Vote, Veto.">
|
||
|
||
<!-- 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}
|
||
</style>
|
||
</head>
|
||
|
||
<!-- 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="radical, democracy, direct democracy, petitions, australia, policy, activism, movement, underground parliament">
|
||
<meta name="rating" content="general">
|
||
<meta name="theme-color"
|
||
content="#ff0099">
|
||
|
||
<!-- JSON LD -->
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": "WebSite",
|
||
"name": "RADICAL",
|
||
"description": "Australia's direct democracy movement. Share policy ideas, vote on proposals, take back control. Vibe, Vote, Veto.",
|
||
"image": "https://theradicalparty.com/memes/mickeymeta.PNG",
|
||
"url": "https://theradicalparty.com",
|
||
"author": {
|
||
"@type": "Organization",
|
||
"name": "RADICAL Party"
|
||
},
|
||
"sameAs": [
|
||
"https://twitter.com/RadicalAustralia"
|
||
],
|
||
"potentialAction": {
|
||
"@type": "SearchAction",
|
||
"target": {
|
||
"@type": "EntryPoint",
|
||
"urlTemplate": "https://theradicalparty.com/search?q={search_term_string}"
|
||
},
|
||
"query-input": "required name=search_term_string"
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<body>
|
||
<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 · 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 · Version 3.0 · 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 & 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 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 the 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> members.</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 <strong>unanimous vote</strong> of all 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.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 Convenor and Reserved Powers</h3>
|
||
<p class="const-clause"><span class="const-num">6.1</span><strong>The Convenor.</strong> <strong>[Founder name]</strong> is the inaugural <strong>Convenor</strong> of the Founding Fathers/Mummies.</p>
|
||
<p class="const-clause"><span class="const-num">6.2</span><strong>Reserved powers.</strong> While the inaugural Convenor holds office:</p>
|
||
<p class="const-sub">(a) the Convenor appoints the inaugural members of the Founding Fathers/Mummies;</p>
|
||
<p class="const-sub">(b) no amendment to this constitution may be made without the Convenor's written consent (clause 9);</p>
|
||
<p class="const-sub">(c) the Convenor may not be removed from the Founding Fathers/Mummies except by a unanimous resolution of <strong>all</strong> other sitting members of the Founding Fathers/Mummies; and</p>
|
||
<p class="const-sub">(d) the Convenor holds the casting vote of the Founding Fathers/Mummies.</p>
|
||
<p class="const-clause"><span class="const-num">6.3</span><strong>Succession.</strong> The Convenor may nominate a successor Convenor by written instrument; failing a valid nomination, the Founding Fathers/Mummies appoints a Convenor from among its members.</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 resolution of the Founding Fathers/Mummies, passed by a majority of its sitting members <strong>and</strong> with the written consent of the Convenor while clause 6.2 applies.</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 🇦🇺 © 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
|
||
async function initializeApp() {
|
||
updateUIText();
|
||
await loadUserData();
|
||
loadVerifiedUsers();
|
||
setupEventListeners();
|
||
fetchProposals();
|
||
fetchLeaderboard();
|
||
fetchStats();
|
||
checkForProposalInUrl();
|
||
}
|
||
|
||
// 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
// 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> |