diff --git a/influence/README.MD b/influence/README.MD index 73e4aea..31e9a89 100644 --- a/influence/README.MD +++ b/influence/README.MD @@ -434,6 +434,36 @@ The Response Wall creates transparency and accountability by allowing campaign p 5. Admins can mark verified responses with special badge 6. Community upvotes highlight most impactful responses +### QR Code Sharing Feature +The application includes dynamic QR code generation for easy campaign and response wall sharing. + +**Key Features:** +- **Campaign QR Codes**: Generate scannable QR codes for campaign pages +- **Response Wall QR Codes**: Share response walls with QR codes for mobile scanning +- **High-Quality Generation**: 400x400px PNG images with high error correction (level H) +- **Download Support**: One-click download of QR code images for printing or sharing +- **Social Integration**: QR code button alongside social share buttons (Facebook, Twitter, LinkedIn, etc.) +- **Caching**: QR codes cached for 1 hour to improve performance + +**How to Use:** +1. Visit any campaign page or response wall +2. Click the QR code icon (📱) in the social share buttons section +3. A modal appears with the generated QR code +4. Scan with any smartphone camera to visit the page +5. Click "Download QR Code" to save the image for printing or sharing + +**Technical Implementation:** +- Backend endpoint: `GET /api/campaigns/:slug/qrcode?type=campaign|response-wall` +- Uses `qrcode` npm package for generation +- Proper MIME type and cache headers +- Modal UI with download functionality + +**Use Cases:** +- Print QR codes on flyers and posters for offline campaign promotion +- Share QR codes in presentations and meetings +- Include in email newsletters for mobile-friendly access +- Display at events for easy sign-up + ### Email Integration - Modal-based email composer - Pre-filled recipient information diff --git a/influence/app/controllers/campaigns.js b/influence/app/controllers/campaigns.js index 21a284a..6fa29fd 100644 --- a/influence/app/controllers/campaigns.js +++ b/influence/app/controllers/campaigns.js @@ -1,6 +1,7 @@ const nocoDB = require('../services/nocodb'); const emailService = require('../services/email'); const representAPI = require('../services/represent-api'); +const qrcodeService = require('../services/qrcode'); const { generateSlug, validateSlug } = require('../utils/validators'); const multer = require('multer'); const path = require('path'); @@ -1074,6 +1075,66 @@ class CampaignsController { }); } } + + // Generate QR code for campaign or response wall + async generateQRCode(req, res, next) { + try { + const { slug } = req.params; + const { type } = req.query; // 'campaign' or 'response-wall' + + // Validate type parameter + if (type && !['campaign', 'response-wall'].includes(type)) { + return res.status(400).json({ + success: false, + error: 'Invalid type parameter. Must be "campaign" or "response-wall"' + }); + } + + // Get campaign to verify it exists + const campaign = await nocoDB.getCampaignBySlug(slug); + if (!campaign) { + return res.status(404).json({ + success: false, + error: 'Campaign not found' + }); + } + + // Build URL based on type + const appUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`; + let targetUrl; + + if (type === 'response-wall') { + targetUrl = `${appUrl}/response-wall.html?campaign=${slug}`; + } else { + // Default to campaign page + targetUrl = `${appUrl}/campaign/${slug}`; + } + + // Generate QR code + const qrCodeBuffer = await qrcodeService.generateQRCode(targetUrl, { + width: 400, + margin: 2, + errorCorrectionLevel: 'H' // High error correction for better scanning + }); + + // Set response headers + res.set({ + 'Content-Type': 'image/png', + 'Content-Length': qrCodeBuffer.length, + 'Cache-Control': 'public, max-age=3600' // Cache for 1 hour + }); + + // Send the buffer + res.send(qrCodeBuffer); + } catch (error) { + console.error('Generate QR code error:', error); + res.status(500).json({ + success: false, + error: 'Failed to generate QR code', + message: error.message + }); + } + } } // Export controller instance and upload middleware diff --git a/influence/app/controllers/responses.js b/influence/app/controllers/responses.js index a4a2411..47b77e6 100644 --- a/influence/app/controllers/responses.js +++ b/influence/app/controllers/responses.js @@ -913,6 +913,123 @@ async function reportResponse(req, res) { } } +/** + * Resend verification email for a response + * Public endpoint - no authentication required + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +async function resendVerification(req, res) { + try { + const { id } = req.params; + + console.log('=== RESEND VERIFICATION REQUEST ==='); + console.log('Response ID:', id); + + // Get the response + const response = await nocodbService.getRepresentativeResponseById(id); + if (!response) { + console.log('Response not found for ID:', id); + return res.status(404).json({ + success: false, + error: 'Response not found' + }); + } + + // Check if already verified + if (response.verified_at) { + return res.status(400).json({ + success: false, + error: 'This response has already been verified' + }); + } + + // Check if we have the necessary data + if (!response.representative_email) { + return res.status(400).json({ + success: false, + error: 'No representative email on file for this response' + }); + } + + if (!response.verification_token) { + // Generate a new token if one doesn't exist + const crypto = require('crypto'); + const newToken = crypto.randomBytes(32).toString('hex'); + const verificationSentAt = new Date().toISOString(); + + await nocodbService.updateRepresentativeResponse(id, { + verification_token: newToken, + verification_sent_at: verificationSentAt + }); + + response.verification_token = newToken; + response.verification_sent_at = verificationSentAt; + } + + // Get campaign details + const campaign = await nocodbService.getCampaignBySlug(response.campaign_slug); + if (!campaign) { + return res.status(404).json({ + success: false, + error: 'Campaign not found' + }); + } + + // Send verification email + try { + const baseUrl = process.env.BASE_URL || `${req.protocol}://${req.get('host')}`; + const verificationUrl = `${baseUrl}/api/responses/${response.id}/verify/${response.verification_token}`; + const reportUrl = `${baseUrl}/api/responses/${response.id}/report/${response.verification_token}`; + + const campaignTitle = campaign.Title || campaign.title || 'Unknown Campaign'; + const submittedDate = new Date(response.created_at).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + + await emailService.sendResponseVerification({ + representativeEmail: response.representative_email, + representativeName: response.representative_name, + campaignTitle, + responseType: response.response_type, + responseText: response.response_text, + submittedDate, + submitterName: response.is_anonymous ? 'Anonymous' : (response.submitted_by_name || 'A constituent'), + verificationUrl, + reportUrl + }); + + // Update verification_sent_at timestamp + await nocodbService.updateRepresentativeResponse(id, { + verification_sent_at: new Date().toISOString() + }); + + console.log('Verification email resent successfully to:', response.representative_email); + + res.json({ + success: true, + message: 'Verification email sent successfully to the representative' + }); + + } catch (emailError) { + console.error('Failed to send verification email:', emailError); + res.status(500).json({ + success: false, + error: 'Failed to send verification email. Please try again later.' + }); + } + + } catch (error) { + console.error('Error resending verification:', error); + res.status(500).json({ + success: false, + error: 'An error occurred while processing your request' + }); + } +} + module.exports = { getCampaignResponses, submitResponse, @@ -924,5 +1041,6 @@ module.exports = { updateResponse, deleteResponse, verifyResponse, - reportResponse + reportResponse, + resendVerification }; diff --git a/influence/app/package.json b/influence/app/package.json index 93788b0..9fcfa89 100644 --- a/influence/app/package.json +++ b/influence/app/package.json @@ -28,7 +28,8 @@ "nodemailer": "^6.9.4", "express-session": "^1.17.3", "bcryptjs": "^2.4.3", - "multer": "^1.4.5-lts.1" + "multer": "^1.4.5-lts.1", + "qrcode": "^1.5.3" }, "devDependencies": { "nodemon": "^3.0.1", diff --git a/influence/app/public/campaign.html b/influence/app/public/campaign.html index b6c0090..9bd0af2 100644 --- a/influence/app/public/campaign.html +++ b/influence/app/public/campaign.html @@ -493,6 +493,11 @@ + @@ -605,6 +610,19 @@ + +
+
+ × +

Scan QR Code to Visit Campaign

+
+ Campaign QR Code +
+

Scan this code with your phone to visit this campaign page

+ +
+
+