# Response Wall Page ## Overview **File Path:** `admin/src/pages/public/ResponseWallPage.tsx` (492 lines) **Route:** `/responses/:campaignId` **Role Requirements:** Public access (no authentication required) **Purpose:** Community-driven response wall displaying user-submitted campaign feedback, verification status, government official replies, and social engagement through upvoting. Serves as social proof and community building tool for advocacy campaigns. **Key Features:** - Campaign-specific response display with back navigation - Real-time statistics cards (Total Responses, Verified, Total Upvotes) - Multi-criteria sorting (Recent, Most Upvoted, Verified Only) - Government level filtering (Federal, Provincial, Municipal, All) - Response cards with upvote functionality - User comments and representative details - Verification badges for confirmed responses - Response submission modal with long-form input - Pagination for large response sets - Dark blue/teal theme consistency - Mobile-responsive grid layout **Layout:** Uses `PublicLayout` component with dark theme --- ## Features ### 1. Campaign Context Header Navigation and campaign identification: - **Back Link**: Returns to campaign detail page (`/campaigns/:campaignId`) - **Campaign Title**: Displays as page heading - **Breadcrumb**: "Response Wall" subtitle - **Icon**: Comment icon for visual context ### 2. Statistics Dashboard Three key metrics displayed as cards: - **Total Responses**: Count of all submissions (verified + unverified) - **Verified Responses**: Count of email-verified submissions - **Total Upvotes**: Aggregate upvote count across all responses **Card Design:** - Large numeric display (32px font) - Icon with brand color - Label text below number - Responsive grid (xs=1, sm=3 columns) - Hover effect for visual feedback ### 3. Filtering and Sorting Controls User controls for response discovery: **Sort Dropdown:** - **Recent**: Newest first (default, `createdAt DESC`) - **Most Upvoted**: Highest upvote count first (`upvoteCount DESC`) - **Verified Only**: Only email-verified responses **Government Level Filter:** - **All Levels**: No filtering (default) - **Federal**: Federal government responses only - **Provincial**: Provincial/territorial responses only - **Municipal**: Municipal/local responses only **Layout:** - Row with two columns - Sort on left, filter on right - Full-width selects on mobile - Margin below for spacing ### 4. Response Cards Individual response display with rich metadata: **Card Header:** - User name (bold, 16px) - Timestamp (relative: "2 hours ago") - Verification badge (if `isVerified=true`) **Card Content:** - User comment (full text, auto-wrapping) - Quoted text if available (italicized, gray background) - Representative details: - Name (bold) - District/riding - Government level tag (colored by level) **Card Footer:** - Upvote button with count - Heart icon (filled if user upvoted) - Click toggles upvote status - Optimistic UI update **Styling:** - Dark background (`colorBgContainer`) - Rounded corners (8px) - Hover elevation shadow - Dividers between sections ### 5. Submit Response Modal Long-form response submission interface: **Form Fields:** - **Your Name** (required, text input) - **Your Email** (required, email validation) - **Your Postal Code** (optional, for rep lookup context) - **Representative** (read-only, from parent campaign context) - **Your Comment** (required, TextArea, 5 rows min) - **Email Me a Copy** (checkbox, default checked) **Validation:** - Required field indicators - Email format validation - Min/max length checks (comment: 10-5000 chars) - Disabled submit until valid **Submission Flow:** 1. User clicks "Submit Your Response" button 2. Modal opens with empty form 3. User fills fields 4. Clicks "Submit Response" button 5. API creates response (status: `unverified`) 6. Verification email sent if checkbox checked 7. Success modal displays 8. Form resets 9. Responses list refreshes ### 6. Pagination Ant Design Pagination component: - **Page Size**: 20 responses per page - **Total Count**: Fetched from API - **Page Change**: Triggers new API request - **Positioning**: Centered below response grid - **Styling**: Inherits dark theme from PublicLayout --- ## User Workflow ### Browsing Responses 1. User arrives from campaign page via "View Response Wall" link 2. Page loads responses (default: recent, all levels) 3. User views statistics cards showing community engagement 4. User scrolls through response cards 5. User reads comments and representative details 6. User upvotes responses they agree with 7. User clicks pagination to view more responses ### Filtering and Sorting 1. User selects "Most Upvoted" from sort dropdown 2. API re-fetches responses with new sort order 3. Grid updates with reordered responses 4. User selects "Federal" from government level filter 5. API re-fetches with government level filter 6. Grid shows only federal responses 7. User resets filters to "All Levels" to see everything ### Submitting a Response 1. User clicks "Submit Your Response" button 2. Modal opens with blank form 3. User enters name: "Jane Doe" 4. User enters email: "jane@example.com" 5. User enters postal code: "K1A 0B1" (optional) 6. User writes comment: "I strongly support this bill because..." 7. User checks "Email me a copy" checkbox 8. User clicks "Submit Response" 9. API creates response with `isVerified=false` 10. Backend sends verification email to jane@example.com 11. Success modal displays: "Response submitted! Check your email to verify." 12. User clicks "OK" 13. Modal closes 14. Responses grid refreshes (may not show new response if "Verified Only" filter active) ### Upvoting 1. User sees response they agree with 2. User clicks heart icon button 3. Optimistic update: upvote count increments, heart fills with color 4. API request to `/api/public/responses/:id/upvote` 5. If API succeeds: update persists 6. If API fails: revert to previous state, show error message 7. User can click again to remove upvote (toggle behavior) --- ## Component Structure ```tsx import React, { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import { Card, Row, Col, Typography, Button, Select, Statistic, Modal, Form, Input, Checkbox, Pagination, Tag, Space, message, Grid } from 'antd'; import { ArrowLeftOutlined, CommentOutlined, HeartOutlined, HeartFilled, CheckCircleOutlined, TrophyOutlined, FireOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import PublicLayout from '../../components/PublicLayout'; import axios from 'axios'; dayjs.extend(relativeTime); const { Title, Paragraph, Text } = Typography; const { TextArea } = Input; const { Option } = Select; const { useBreakpoint } = Grid; interface Response { id: string; userName: string; userEmail: string; postalCode: string | null; comment: string; quotedText: string | null; isVerified: boolean; upvoteCount: number; representativeName: string; representativeDistrict: string; governmentLevel: string; createdAt: string; hasUpvoted?: boolean; // Client-side tracking } interface Campaign { id: string; title: string; } interface Stats { totalResponses: number; verifiedResponses: number; totalUpvotes: number; } const ResponseWallPage: React.FC = () => { const { campaignId } = useParams<{ campaignId: string }>(); const [responses, setResponses] = useState([]); const [campaign, setCampaign] = useState(null); const [stats, setStats] = useState({ totalResponses: 0, verifiedResponses: 0, totalUpvotes: 0 }); const [loading, setLoading] = useState(true); const [sortBy, setSortBy] = useState('recent'); const [governmentLevel, setGovernmentLevel] = useState('all'); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [submitModalVisible, setSubmitModalVisible] = useState(false); const [form] = Form.useForm(); const screens = useBreakpoint(); const isMobile = !screens.md; const pageSize = 20; // Data fetching, handlers, etc. return ( {/* Back link and title */} {/* Statistics cards */} {/* Sort and filter controls */} {/* Response cards grid */} {/* Pagination */} {/* Submit modal */} ); }; export default ResponseWallPage; ``` --- ## State Management ### Component State ```tsx // Response data const [responses, setResponses] = useState([]); const [campaign, setCampaign] = useState(null); const [stats, setStats] = useState({ totalResponses: 0, verifiedResponses: 0, totalUpvotes: 0 }); const [loading, setLoading] = useState(true); // Filtering and sorting const [sortBy, setSortBy] = useState('recent'); // 'recent' | 'upvotes' | 'verified' const [governmentLevel, setGovernmentLevel] = useState('all'); // 'all' | 'federal' | 'provincial' | 'municipal' // Pagination const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const pageSize = 20; // Modal state const [submitModalVisible, setSubmitModalVisible] = useState(false); const [form] = Form.useForm(); // Responsive const screens = useBreakpoint(); const isMobile = !screens.md; ``` ### Derived State ```tsx // No complex derived state - filtering happens server-side // All data transformations done by API ``` ### State Flow 1. **Initial Load**: `loading=true`, fetch campaign + responses + stats 2. **Data Received**: `setCampaign()`, `setResponses()`, `setStats()`, `setTotal()`, `loading=false` 3. **Sort Changed**: `setSortBy()`, `setPage(1)`, refetch responses 4. **Filter Changed**: `setGovernmentLevel()`, `setPage(1)`, refetch responses 5. **Page Changed**: `setPage()`, refetch responses (keep sort/filter) 6. **Upvote Clicked**: Optimistic update to `responses` array, API call 7. **Submit Clicked**: `setSubmitModalVisible(true)`, open form 8. **Response Submitted**: API call, `setSubmitModalVisible(false)`, refetch responses --- ## API Integration ### Endpoints Used #### 1. Get Campaign (Basic Info) ```http GET /api/public/campaigns/:campaignId ``` **Response:** ```json { "id": "cm1abc123", "title": "Support Climate Action Bill" } ``` #### 2. Get Response Statistics ```http GET /api/public/responses/campaigns/:campaignId/stats ``` **Response:** ```json { "totalResponses": 342, "verifiedResponses": 287, "totalUpvotes": 1829 } ``` #### 3. List Responses ```http GET /api/public/responses/campaigns/:campaignId?page=1&limit=20&sortBy=recent&governmentLevel=all ``` **Query Parameters:** - `page`: Page number (1-indexed) - `limit`: Items per page (default 20, max 100) - `sortBy`: `recent` | `upvotes` | `verified` - `governmentLevel`: `all` | `federal` | `provincial` | `municipal` **Response:** ```json { "responses": [ { "id": "cm2abc123", "userName": "Jane Doe", "userEmail": "jane@example.com", "postalCode": "K1A 0B1", "comment": "I strongly support this bill because it addresses critical climate issues...", "quotedText": null, "isVerified": true, "upvoteCount": 47, "representativeName": "John Smith", "representativeDistrict": "Ottawa Centre", "governmentLevel": "federal", "createdAt": "2025-02-10T14:30:00.000Z" } ], "total": 342, "page": 1, "limit": 20 } ``` #### 4. Submit Response ```http POST /api/public/responses Content-Type: application/json { "campaignId": "cm1abc123", "userName": "Jane Doe", "userEmail": "jane@example.com", "postalCode": "K1A 0B1", "comment": "I strongly support this bill...", "representativeName": "John Smith", "representativeDistrict": "Ottawa Centre", "governmentLevel": "federal", "sendCopy": true } ``` **Response:** ```json { "success": true, "responseId": "cm2def456", "message": "Response submitted successfully. Please check your email to verify." } ``` #### 5. Upvote Response ```http POST /api/public/responses/:id/upvote ``` **Response:** ```json { "success": true, "upvoteCount": 48, "action": "added" } ``` **Note:** Second request to same endpoint toggles (removes upvote), returns `"action": "removed"`. ### Request Examples #### Fetch Responses ```tsx useEffect(() => { const fetchData = async () => { if (!campaignId) return; try { setLoading(true); const [campaignRes, statsRes, responsesRes] = await Promise.all([ axios.get(`/api/public/campaigns/${campaignId}`), axios.get(`/api/public/responses/campaigns/${campaignId}/stats`), axios.get(`/api/public/responses/campaigns/${campaignId}`, { params: { page, limit: pageSize, sortBy, governmentLevel: governmentLevel === 'all' ? undefined : governmentLevel } }) ]); setCampaign(campaignRes.data); setStats(statsRes.data); setResponses(responsesRes.data.responses); setTotal(responsesRes.data.total); } catch (error) { console.error('Failed to fetch data:', error); message.error('Failed to load responses'); } finally { setLoading(false); } }; fetchData(); }, [campaignId, page, sortBy, governmentLevel]); ``` #### Submit Response ```tsx const handleSubmit = async (values: any) => { try { await axios.post('/api/public/responses', { campaignId, userName: values.userName, userEmail: values.userEmail, postalCode: values.postalCode || null, comment: values.comment, representativeName: values.representativeName, representativeDistrict: values.representativeDistrict || '', governmentLevel: values.governmentLevel || 'federal', sendCopy: values.sendCopy }); Modal.success({ title: 'Response Submitted!', content: 'Please check your email to verify your response.', }); setSubmitModalVisible(false); form.resetFields(); // Refresh responses list setPage(1); // Triggers useEffect refetch } catch (error: any) { console.error('Submit failed:', error); message.error(error.response?.data?.message || 'Failed to submit response'); } }; ``` #### Upvote Response ```tsx const handleUpvote = async (responseId: string) => { // Optimistic update setResponses(prev => prev.map(r => { if (r.id === responseId) { const hasUpvoted = !r.hasUpvoted; return { ...r, hasUpvoted, upvoteCount: r.upvoteCount + (hasUpvoted ? 1 : -1) }; } return r; })); try { const response = await axios.post(`/api/public/responses/${responseId}/upvote`); // Update with server count (in case of race condition) setResponses(prev => prev.map(r => r.id === responseId ? { ...r, upvoteCount: response.data.upvoteCount } : r )); } catch (error) { console.error('Upvote failed:', error); // Revert on error setResponses(prev => prev.map(r => { if (r.id === responseId) { return { ...r, hasUpvoted: !r.hasUpvoted, upvoteCount: r.upvoteCount + (r.hasUpvoted ? -1 : 1) }; } return r; })); message.error('Failed to upvote. Please try again.'); } }; ``` --- ## Code Examples ### Statistics Cards ```tsx } valueStyle={{ color: '#1890ff', fontSize: 32 }} /> } valueStyle={{ color: '#52c41a', fontSize: 32 }} /> } valueStyle={{ color: '#eb2f96', fontSize: 32 }} /> ``` ### Sort and Filter Controls ```tsx Sort by: Government Level: ``` ### Response Cards ```tsx {responses.map((response) => ( {/* Header */}
{response.userName} {response.isVerified && ( }> Verified )} {dayjs(response.createdAt).fromNow()}
{/* Comment */} {response.comment} {/* Quoted Text (if any) */} {response.quotedText && (
"{response.quotedText}"
)} {/* Representative Info */}
Sent to:{' '} {response.representativeName} {response.representativeDistrict && ( {' '}• {response.representativeDistrict} )}
{response.governmentLevel.charAt(0).toUpperCase() + response.governmentLevel.slice(1)}
{/* Upvote Button */}
))}
``` ### Submit Response Modal ```tsx { setSubmitModalVisible(false); form.resetFields(); }} footer={null} width={600} >