3140 lines
No EOL
130 KiB
HTML
3140 lines
No EOL
130 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}
|
||
/* ── 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, ban face coverings, 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>
|
||
|
||
<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="#events">Events</a>
|
||
<a href="#contact">Contact</a>
|
||
</div>
|
||
</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/steppingout.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');
|
||
}
|
||
}
|
||
|
||
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> |