2444 lines
No EOL
96 KiB
HTML
2444 lines
No EOL
96 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<!-- 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="apple-mobile-web-app-title" 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://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<meta name="twitter:image:src"
|
||
content="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<meta name="twitter:image:alt" content="RADICAL Logo">
|
||
<link rel="shortcut icon" href="https://radical.omar-c29.workers.dev/memes/favicon.jpg" type="image/x-icon">
|
||
<link rel="shortcut icon" href="https://radical.omar-c29.workers.dev/memes/favicon.jpg"
|
||
type="image/jpg">
|
||
<link rel="mask-icon" href="https://radical.omar-c29.workers.dev/memes/favicon.jpg"
|
||
color="#ff0099">
|
||
<meta property="og:image"
|
||
content="https://radical.omar-c29.workers.dev/memes/mickeymeta.PNG">
|
||
<meta property="og:image:url"
|
||
content="https://radical.omar-c29.workers.dev/memes/mickeymeta.PNG">
|
||
<meta property="og:image:secure_url"
|
||
content="https://radical.omar-c29.workers.dev/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://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="icon" type="image/jpg" sizes="512x512"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="icon" type="image/jpg" sizes="256x256"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="icon" type="image/jpg" sizes="128x128"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="icon" type="image/jpg" sizes="64x64"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="icon" type="image/jpg" sizes="32x32"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="icon" type="image/jpg"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="apple-touch-icon" sizes="512x512"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="apple-touch-icon" sizes="256x256"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="apple-touch-icon" sizes="128x128"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="apple-touch-icon" sizes="64x64"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
<link rel="apple-touch-icon" sizes="32x32"
|
||
href="https://radical.omar-c29.workers.dev/memes/favicon.jpg">
|
||
|
||
<!-- 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">
|
||
</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://radical.omar-c29.workers.dev/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 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 🇦🇺 © 2025</p>
|
||
<div class="footer-links">
|
||
<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);
|
||
}
|
||
}
|
||
// Configuration
|
||
const API_BASE_URL = 'https://radical.omar-c29.workers.dev/api'; // Replace with your actual worker URL
|
||
|
||
// 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() {
|
||
// User information (in a real app, this would come from authentication)
|
||
currentUser = JSON.parse(localStorage.getItem('radical_user'));
|
||
|
||
// If no user exists, create a new one
|
||
if (!currentUser) {
|
||
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));
|
||
}
|
||
|
||
// Create or get user in the database
|
||
try {
|
||
await createUser(currentUser);
|
||
console.log("User registration complete");
|
||
} catch (error) {
|
||
console.error("Failed to register user on page load");
|
||
}
|
||
}
|
||
|
||
// 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); // Log the user data
|
||
|
||
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 errorData = await response.json();
|
||
console.error("User creation API error:", errorData);
|
||
throw new Error('Failed to create user');
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// Fetch proposals from API
|
||
async function fetchProposals() {
|
||
try {
|
||
// Show loading indicator
|
||
showLoadingIndicator();
|
||
|
||
const sortSelect = document.getElementById('sort-select');
|
||
const sortBy = sortSelect ? sortSelect.value : 'newest';
|
||
const response = await fetch(`${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser.id}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch proposals');
|
||
}
|
||
|
||
const proposals = await response.json();
|
||
renderProposals(proposals);
|
||
} catch (error) {
|
||
console.error('Error fetching proposals:', error);
|
||
showErrorMessage('Oops! Failed to load petitions. Parliament might be censoring us already!');
|
||
}
|
||
}
|
||
|
||
// 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 fetch(`${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://radical.omar-c29.workers.dev/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://radical.omar-c29.workers.dev/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();
|
||
|
||
postButton.innerHTML = 'Launch Rebellion <div class="loader"></div>';
|
||
|
||
if (text) {
|
||
try {
|
||
const trending = Math.random() > 0.8; // Randomly mark some as trending
|
||
|
||
// Create the proposal first
|
||
const createResponse = await fetch(`${API_BASE_URL}/proposals`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
authorId: currentUser.id,
|
||
text: text,
|
||
trending: trending
|
||
})
|
||
});
|
||
|
||
if (!createResponse.ok) {
|
||
throw new Error('Failed to create proposal');
|
||
}
|
||
|
||
const proposalData = await createResponse.json();
|
||
const proposalId = proposalData.id;
|
||
|
||
// 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 animation
|
||
animatePostSuccess();
|
||
|
||
// Refresh proposals
|
||
fetchProposals();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating proposal:', error);
|
||
showErrorMessage('Failed to post your petition. Parliament might be onto us!');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 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?userId=${currentUser.id}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch proposal');
|
||
}
|
||
|
||
const proposals = await response.json();
|
||
const targetProposal = proposals.find(p => p.id === proposalId);
|
||
|
||
if (targetProposal) {
|
||
// Create and show the modal with the proposal data
|
||
createProposalViewModal(targetProposal);
|
||
} 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 = getShareableImageUrl(proposal);
|
||
document.getElementById('og-image').setAttribute('content', ogImageUrl);
|
||
document.getElementById('twitter-image').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 fetch(`${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 fetch(`${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 getShareableImageUrl(proposal) {
|
||
// Base Cloudinary URL
|
||
const cloudinaryUrl = "https://res.cloudinary.com/dh8apmjya/image/fetch";
|
||
|
||
// Make sure we have valid text to work with
|
||
const text = proposal.text || "Unknown petition";
|
||
const authorName = proposal.author_name || "Anonymous";
|
||
const encodedupVotes = proposal.upvotes || "fk all";
|
||
|
||
// Encode the proposal text for URL - handle special characters better
|
||
// Limit text length to avoid excessively long URLs
|
||
const maxTextLength = 100;
|
||
const trimmedText = text.length > maxTextLength ?
|
||
text.substring(0, maxTextLength) + '...' : text;
|
||
|
||
const encodedText = encodeURIComponent(trimmedText)
|
||
.replace(/'/g, '%27')
|
||
.replace(/"/g, '%22')
|
||
.replace(/\(/g, '%28')
|
||
.replace(/\)/g, '%29')
|
||
.replace(/\*/g, '%2A');
|
||
|
||
const encodedAuthor = encodeURIComponent(authorName)
|
||
.replace(/'/g, '%27')
|
||
.replace(/"/g, '%22');
|
||
|
||
// Transformation parameters for text overlay - adjust for better readability
|
||
const textParams =
|
||
`b_black,w_1200,h_600,c_fill` +
|
||
`/l_text:Roboto_40_bold:${encodedText}` +
|
||
`,co_rgb:EAEAEA,w_900,c_fit,g_north,y_120,x_60` +
|
||
`/l_text:Roboto_28_bold:${encodedAuthor}` +
|
||
`,co_rgb:FF00FF,g_north_west,y_50,x_60`;
|
||
|
||
// URL of your template image in R2
|
||
const templateUrl = encodeURIComponent("https://radical.omar-c29.workers.dev/memes/petition-template.png");
|
||
|
||
// Add a cache-busting parameter to prevent caching issues
|
||
const cacheBuster = Date.now();
|
||
|
||
// Combine everything
|
||
return `${cloudinaryUrl}/${textParams}/${templateUrl}?cb=${cacheBuster}`;
|
||
}
|
||
|
||
// 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 fetch(`${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, '&')
|
||
.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;
|
||
}
|
||
|
||
// Function to submit a new comment
|
||
async function submitComment(proposalId, form) {
|
||
const commentInput = form.querySelector('.comment-input');
|
||
const submitButton = form.querySelector('.comment-submit');
|
||
|
||
const commentText = commentInput.value.trim();
|
||
|
||
if (!commentText) return;
|
||
|
||
// Disable form while submitting
|
||
commentInput.disabled = true;
|
||
submitButton.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE_URL}/comments`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
proposalId: proposalId,
|
||
userId: currentUser.id,
|
||
commentText: commentText
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to submit comment');
|
||
}
|
||
|
||
// Clear input
|
||
commentInput.value = '';
|
||
|
||
// Reload comments to show the new one
|
||
const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`);
|
||
const commentsContent = commentsContainer.querySelector('.comments-content');
|
||
|
||
// Add new comment to the top (optimistic update)
|
||
const newComment = await response.json();
|
||
const tempCommentHtml = `
|
||
<div class="comment-item" style="animation: fadeIn 0.5s;">
|
||
<div class="comment-header">
|
||
<div class="comment-author">${newComment.userName}</div>
|
||
<div class="comment-time">just now</div>
|
||
</div>
|
||
<div class="comment-text">${formatCommentText(newComment.commentText)}</div>
|
||
</div>
|
||
`;
|
||
|
||
// If there are no comments yet, replace the "no comments" message
|
||
if (commentsContent.querySelector('.no-comments')) {
|
||
commentsContent.innerHTML = tempCommentHtml;
|
||
} else {
|
||
// Otherwise, prepend to existing comments
|
||
commentsContent.insertAdjacentHTML('afterbegin', tempCommentHtml);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error posting comment:', error);
|
||
showToastMessage('Failed to post comment. Please try again.', '#ff0099');
|
||
} finally {
|
||
// Re-enable form
|
||
commentInput.disabled = false;
|
||
submitButton.disabled = false;
|
||
}
|
||
}
|
||
|
||
// 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 - this should point to your audio file in R2
|
||
const audioUrl = 'https://radical.omar-c29.workers.dev/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);
|
||
}
|
||
}
|
||
|
||
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');
|
||
});
|
||
}
|
||
|
||
function toggleMusic() {
|
||
if (backgroundMusic.paused) {
|
||
playAudio();
|
||
} else {
|
||
backgroundMusic.pause();
|
||
musicToggle.innerHTML = '<span class="vinyl-ready">📻</span>';
|
||
musicToggle.classList.add('pulse');
|
||
}
|
||
}
|
||
});
|
||
|
||
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;
|
||
|
||
fetch(`${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>';
|
||
|
||
fetch(`${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 fetch(`${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;
|
||
});
|
||
}
|
||
|
||
</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> |