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
+
+
+
+