radical/index.html
Omar Najjar 25e2921d74 x
2025-04-13 04:47:32 +10:00

2841 lines
No EOL
106 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Title -->
<title>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">
</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>
<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="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>
<footer>
<p>RADICAL - Direct Democracy Movement 🇦🇺 &copy; 2025</p>
<div class="footer-links">
<a href="/login" class="login-link">Login 🔐</a>
<a href="#about">About Us</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() {
// Update UI elements
updateUIText();
// Load user information
await loadUserData();
// Load verified users data
loadVerifiedUsers();
// Setup event listeners
setupEventListeners();
// Initial load of proposals
fetchProposals();
// Check for proposal in URL
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>
`;
}
}
// 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;
}
}
// 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'
});
// 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');
}
renderProposals(data);
} catch (error) {
console.error('Error fetching proposals:', error);
showErrorMessage('Failed to load petitions. Please check the console for details.', true);
}
}
// 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);
// 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;
}
const response = await safeFetch(`${API_BASE_URL}/votes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(voteData),
mode: 'cors',
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error('Failed to submit vote');
}
return await response.json();
}
// 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) {
const proposalsContainer = document.getElementById('proposals-container');
if (!proposalsContainer) return;
// Clear container
proposalsContainer.innerHTML = '';
// Show empty state if no proposals
if (!proposals || proposals.length === 0) {
proposalsContainer.innerHTML = `
<div class="empty-state">
<p>No petitions yet. Be the first to create one!</p>
</div>
`;
return;
}
// Render each proposal
proposals.forEach(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>` : '';
proposalCard.innerHTML = `
${badgeHtml}
<div class="proposal-author">${proposal.author_name}</div>
<div class="proposal-text">${proposal.text}</div>
${memeHtml}
${petitionInfo}
<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>
<div class="custom-copy-button" data-id="${proposal.id}" style="display: inline-block; background-color: #000000; border: 0px solid #333333; color: #aaaaaa; padding: 4px 8px; font-size: 20px; position: relative; z-index: 10; cursor: pointer; vertical-align: middle;" onclick="event.stopPropagation(); event.preventDefault(); smartCopyShare('${proposal.id}', '${encodeURIComponent(proposal.text)}');">
<img src="https://theradicalparty.com/memes/share-arrow-icon-md.png" alt="Share" style="width: 20px; height: 14px; filter: brightness(0) invert(1); pointer-events: none;">
</div>
</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" id="comments-container-${proposal.id}" style="display: none;">
<div class="comments-list" id="comments-list-${proposal.id}"></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>
`;
proposalsContainer.appendChild(proposalButton);
// Add comments section to the proposal card - NEW CODE
addCommentsToProposal(proposalCard, proposal.id);
// Add event listeners for vote buttons
const upvoteButton = proposalCard.querySelector('[data-vote-type="upvote"]');
const downvoteButton = proposalCard.querySelector('[data-vote-type="downvote"]');
const shareButton = proposalCard.querySelector('.share-button');
if (upvoteButton) {
upvoteButton.addEventListener('click', () => {
handleVote(proposal.id, 'upvote');
});
}
if (downvoteButton) {
downvoteButton.addEventListener('click', () => {
handleVote(proposal.id, 'downvote');
});
}
if (shareButton) {
shareButton.addEventListener('click', async () => {
if ('share' in navigator) await navigator.share({ title: 'What do you think?', text: proposal.text, url: getShareableLink(proposal.id) });
else copyToClipboard(getShareableLink(proposal.id));
});
}
// Add comments toggle functionality
const commentsToggle = document.getElementById(`comments-toggle-${proposal.id}`);
const commentsContainer = document.getElementById(`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 = document.querySelector(`.comment-submit[data-proposal-id="${proposal.id}"]`);
if (commentSubmit) {
commentSubmit.addEventListener('click', function() {
const commentInput = document.getElementById(`comment-input-${proposal.id}`);
const commentText = commentInput.value.trim();
if (commentText) {
submitComment(proposal.id, commentText);
commentInput.value = '';
}
});
}
// Update comment count
updateCommentCount(proposal.id);
});
}
// 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);
}
// New function specifically for loading comments in the modal
async function loadModalComments(proposalId) {
const commentsContent = document.getElementById(`modal-comments-content-${proposalId}`);
if (!commentsContent) return;
// Show loading state
commentsContent.innerHTML = `<div class="comments-loading">Loading comments...</div>`;
try {
// Include the current user ID to get their votes
const response = await safeFetch(`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${currentUser.id}`);
if (!response.ok) {
throw new Error('Failed to fetch comments');
}
const comments = await response.json();
renderModalComments(commentsContent, comments);
} catch (error) {
console.error('Error fetching comments:', error);
commentsContent.innerHTML = `
<div class="no-comments">
Failed to load comments. Please try again.
</div>
`;
}
}
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 {
const response = await safeFetch(`${API_BASE_URL}/comment-votes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
commentId: commentId,
userId: currentUser.id,
voteType: voteType
})
});
if (!response.ok) {
throw new Error('Failed to submit comment vote');
}
const result = await response.json();
// If the server response indicates we should update the UI differently, we could handle that here
// But generally the optimistic UI update above should match the server result
} 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
async function loadComments(proposalId) {
const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
const commentsContent = commentsContainer.querySelector('.comments-content');
// Show loading state
commentsContent.innerHTML = `<div class="comments-loading">Loading comments...</div>`;
try {
// Include the current user ID to get their votes
const response = await safeFetch(`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${currentUser.id}`);
if (!response.ok) {
throw new Error('Failed to fetch comments');
}
const comments = await response.json();
renderComments(commentsContent, comments);
} catch (error) {
console.error('Error fetching comments:', error);
commentsContent.innerHTML = `
<div class="no-comments">
Failed to load comments. Please try again.
</div>
`;
}
}
// Function to render comments
function renderComments(container, comments) {
if (!comments || comments.length === 0) {
container.innerHTML = `
<div class="no-comments">
No comments yet. Be the first to comment!
</div>
`;
return;
}
let commentsHtml = '';
comments.forEach(comment => {
const upvotedClass = comment.userVote === 'upvote' ? 'upvoted' : '';
const downvotedClass = comment.userVote === 'downvote' ? 'downvoted' : '';
commentsHtml += `
<div class="comment-item" data-comment-id="${comment.id}">
<div class="comment-header">
<div class="comment-author">${comment.user_name}</div>
<div class="comment-time">${formatDate(comment.timestamp)}</div>
</div>
<div class="comment-text">${formatCommentText(comment.comment_text)}</div>
<div class="comment-vote-buttons">
<button class="comment-vote-button ${upvotedClass}" data-comment-id="${comment.id}" data-vote-type="upvote" onclick="handleCommentVote('${comment.id}', 'upvote')">
<span class="comment-vote-icon">▲</span>
<span class="comment-vote-count">${comment.upvotes || 0}</span>
</button>
<button class="comment-vote-button ${downvotedClass}" data-comment-id="${comment.id}" data-vote-type="downvote" onclick="handleCommentVote('${comment.id}', 'downvote')">
<span class="comment-vote-icon">▼</span>
<span class="comment-vote-count">${comment.downvotes || 0}</span>
</button>
</div>
</div>
`;
});
container.innerHTML = commentsHtml;
}
// Function to format comment text (converts URLs to links, etc.)
function formatCommentText(text) {
// Escape HTML to prevent XSS
const escaped = text.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// Convert URLs to clickable links
const withLinks = escaped.replace(
/(https?:\/\/[^\s]+)/g,
'<a href="$1" target="_blank" rel="noopener noreferrer" style="color:var(--primary);text-decoration:underline;">$1</a>'
);
return withLinks;
}
// Function to submit a new comment
async function submitComment(proposalId, commentText) {
// If commentText is an element, extract the value (backward compatibility)
if (typeof commentText !== 'string') {
const form = commentText;
const commentInput = form.querySelector('.comment-input');
commentText = commentInput.value.trim();
// Disable form while submitting
commentInput.disabled = true;
const submitButton = form.querySelector('.comment-submit');
submitButton.disabled = true;
// Enable form when done
const enableForm = () => {
commentInput.disabled = false;
submitButton.disabled = false;
};
// Return early if empty
if (!commentText) {
enableForm();
return;
}
// Clear input optimistically
commentInput.value = '';
}
// Ensure we have text to submit
if (!commentText || !commentText.trim()) {
console.error('Empty comment text');
return;
}
console.log(`Submitting comment for proposal ${proposalId}: ${commentText.substring(0, 20)}...`);
try {
console.log("Making POST request to comments endpoint");
// Log the full API URL and request details
const requestUrl = `${API_BASE_URL}/comments`;
const requestData = {
proposalId: proposalId,
userId: currentUser?.id || 'anonymous',
commentText: commentText
};
console.log("Request URL:", requestUrl);
console.log("Request data:", requestData);
// Ensure we have a user
if (!currentUser || !currentUser.id) {
console.error("No current user - creating anonymous");
showToastMessage('Please wait, initializing user...', '#ff0099');
await loadUserData(); // Reinitialize user
}
// Create a controller to manage request timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
const response = await fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
proposalId: proposalId,
userId: currentUser.id,
commentText: commentText
}),
mode: 'cors',
credentials: 'same-origin',
signal: controller.signal
});
clearTimeout(timeoutId);
console.log("Comment submission response status:", response.status);
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
// Process response based on status code
if (response.status === 200 || response.status === 201) {
// Successfully created comment
try {
const data = await response.json();
console.log("Comment created successfully:", data);
loadComments(proposalId);
updateCommentCount(proposalId);
showToastMessage('Comment posted!', '#11cc77');
return data;
} catch (jsonError) {
console.warn("Could not parse success response as JSON:", jsonError);
// Still consider this a success if status code was good
loadComments(proposalId);
updateCommentCount(proposalId);
showToastMessage('Comment posted!', '#11cc77');
return { success: true };
}
} else {
// Server returned an error
let errorMessage;
try {
const errorData = await response.json();
console.error("Error response data:", errorData);
errorMessage = errorData.error || 'Unknown server error';
} catch (e) {
try {
errorMessage = await response.text() || 'Failed to post comment';
console.error("Error response text:", errorMessage);
} catch (textError) {
errorMessage = `HTTP error ${response.status}`;
console.error("Could not read error response:", textError);
}
}
console.error('Error posting comment:', errorMessage);
showToastMessage('Failed to post comment', '#ff0099');
throw new Error(errorMessage);
}
} catch (error) {
if (error.name === 'AbortError') {
console.error('Comment request timed out');
showToastMessage('Comment request timed out', '#ff0099');
} else {
console.error('Network or processing error:', error);
showToastMessage('Failed to post comment', '#ff0099');
}
throw error;
}
}
// 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);
});
// Add these functions outside your renderProposals function
function updateCommentCount(proposalId) {
const commentsToggle = document.getElementById(`comments-toggle-${proposalId}`);
if (!commentsToggle) return;
safeFetch(`${API_BASE_URL}/comments?proposalId=${proposalId}`)
.then(response => response.json())
.then(comments => {
const commentCount = comments.length;
const commentText = commentCount === 1 ? 'comment' : 'comments';
commentsToggle.innerHTML = `<span class="comments-toggle-icon">▶</span> ${commentCount} ${commentText}`;
})
.catch(error => {
commentsToggle.innerHTML = `<span class="comments-toggle-icon">▶</span> 0 comments`;
});
}
function loadComments(proposalId) {
const commentsList = document.getElementById(`comments-list-${proposalId}`);
if (!commentsList) return;
commentsList.innerHTML = '<div class="comments-loading">Loading comments...</div>';
safeFetch(`${API_BASE_URL}/comments?proposalId=${proposalId}`)
.then(response => response.json())
.then(comments => {
if (comments.length === 0) {
commentsList.innerHTML = '<div class="no-comments">No comments yet. Be the first to comment!</div>';
return;
}
commentsList.innerHTML = '';
comments.forEach(comment => {
const commentElement = document.createElement('div');
commentElement.className = 'comment';
commentElement.innerHTML = `
<div class="comment-header">
<span class="comment-author">${comment.user_name}</span>
<span class="comment-time">${formatDate(comment.timestamp)}</span>
</div>
<div class="comment-body">${comment.comment_text}</div>
`;
commentsList.appendChild(commentElement);
});
})
.catch(error => {
commentsList.innerHTML = '<div class="comments-error">Failed to load comments.</div>';
});
}
// Update submitComment function to handle promises
function submitComment(proposalId, commentText) {
return safeFetch(`${API_BASE_URL}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
proposalId: proposalId,
userId: currentUser.id,
commentText: commentText
})
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to post comment');
}
return response.json();
})
.then(data => {
loadComments(proposalId);
updateCommentCount(proposalId);
showToastMessage('Comment posted!', '#11cc77');
return data;
})
.catch(error => {
console.error('Error posting comment:', error);
showToastMessage('Failed to post comment', '#ff0099');
throw error;
});
}
// Add these new functions for comment management
const commentRequestQueue = new Set();
let commentRequestTimeout = null;
const MAX_BATCH_SIZE = 10;
const COMMENT_REQUEST_DELAY = 1000; // 1 second delay between batches
async function batchLoadComments() {
if (commentRequestQueue.size === 0) return;
const batch = Array.from(commentRequestQueue).slice(0, MAX_BATCH_SIZE);
commentRequestQueue.clear();
try {
const responses = await Promise.all(
batch.map(proposalId =>
fetch(`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${currentUser.id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
mode: 'cors',
credentials: 'same-origin'
}).then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json().then(data => ({ proposalId, data }));
})
)
);
responses.forEach(({ proposalId, data }) => {
const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
if (commentsContainer) {
renderComments(commentsContainer.querySelector('.comments-content'), data);
}
});
} catch (error) {
console.error('Error loading comments batch:', error);
// Retry failed requests
batch.forEach(proposalId => commentRequestQueue.add(proposalId));
}
// Schedule next batch if there are more requests
if (commentRequestQueue.size > 0) {
commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY);
}
}
function queueCommentLoad(proposalId) {
commentRequestQueue.add(proposalId);
if (!commentRequestTimeout) {
commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY);
}
}
// Update the loadComments function to use the queue
function loadComments(proposalId) {
const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
const commentsContent = commentsContainer?.querySelector('.comments-content');
if (!commentsContent) return;
// Show loading state
commentsContent.innerHTML = `<div class="comments-loading">Loading comments...</div>`;
// Queue the comment load
queueCommentLoad(proposalId);
}
// Update the loadModalComments function to use the queue
async function loadModalComments(proposalId) {
const commentsContent = document.getElementById(`modal-comments-content-${proposalId}`);
if (!commentsContent) return;
// Show loading state
commentsContent.innerHTML = `<div class="comments-loading">Loading comments...</div>`;
// Queue the comment load
queueCommentLoad(proposalId);
}
</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>