diff --git a/civictechprojects/static/css/partials/_VARSelectWeek.scss b/civictechprojects/static/css/partials/_VARSelectWeek.scss
new file mode 100644
index 000000000..217d54d83
--- /dev/null
+++ b/civictechprojects/static/css/partials/_VARSelectWeek.scss
@@ -0,0 +1,37 @@
+.VARSelectWeek-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.VARSelectWeek-label {
+ font-weight: 600;
+ font-size: 1rem;
+}
+
+.required {
+ color: red;
+}
+
+.VARSelectWeek-dropdown {
+ height: 40px;
+ border-radius: 4px;
+ border: 1px solid #cbd5e1;
+ padding: 0 0.5rem;
+ font-size: 1rem;
+ background-color: #fff;
+}
+
+.VARSelectWeek-dropdown.error {
+ border-color: #dc2626;
+}
+
+.VARSelectWeek-helper {
+ font-size: 0.875rem;
+ color: #6b7280;
+}
+
+.VARSelectWeek-error {
+ font-size: 0.875rem;
+ color: #dc2626;
+}
diff --git a/civictechprojects/static/css/partials/_VolunteerActivityReportingCardIntro.scss b/civictechprojects/static/css/partials/_VolunteerActivityReportingCardIntro.scss
new file mode 100644
index 000000000..d8bd553cb
--- /dev/null
+++ b/civictechprojects/static/css/partials/_VolunteerActivityReportingCardIntro.scss
@@ -0,0 +1,64 @@
+.VARCardIntro-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.VARCardIntro-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.VARCardIntro-project {
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+/* Toggle */
+.VARCardIntro-toggle {
+ position: relative;
+ display: inline-block;
+ width: 36px;
+ height: 20px;
+}
+
+.VARCardIntro-toggle input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.VARCardIntro-slider {
+ position: absolute;
+ cursor: pointer;
+ inset: 0;
+ background-color: #ccc;
+ border-radius: 999px;
+ transition: background-color 0.2s;
+}
+
+.VARCardIntro-slider::before {
+ content: '';
+ position: absolute;
+ height: 14px;
+ width: 14px;
+ left: 3px;
+ top: 3px;
+ background-color: #fff;
+ border-radius: 50%;
+ transition: transform 0.2s;
+}
+
+.VARCardIntro-toggle input:checked + .VARCardIntro-slider {
+ background-color: #16a34a;
+}
+
+.VARCardIntro-toggle input:checked + .VARCardIntro-slider::before {
+ transform: translateX(16px);
+}
+
+.VARCardIntro-message {
+ font-size: 0.875rem;
+ color: #6b7280;
+}
diff --git a/civictechprojects/static/css/partials/_VolunteerActivityReportingQ2.scss b/civictechprojects/static/css/partials/_VolunteerActivityReportingQ2.scss
new file mode 100644
index 000000000..e54301eda
--- /dev/null
+++ b/civictechprojects/static/css/partials/_VolunteerActivityReportingQ2.scss
@@ -0,0 +1,70 @@
+.VARQ2-wrapper {
+ font-family: Arial, sans-serif;
+ padding: 1rem;
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.VARQ2-label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-size: 1rem;
+ font-weight: bold;
+}
+
+.VARQ2-input-container {
+ position: relative;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 0.5rem;
+ transition: border-color 0.2s ease-in-out;
+ background-color: #fff;
+}
+
+.VARQ2-input-container.over-limit {
+ border-color: red;
+}
+
+.VARQ2-input {
+ width: 100%;
+ border: none;
+ resize: vertical;
+ min-height: 40px;
+ font-size: 1rem;
+ outline: none;
+ padding-bottom: 1.5rem;
+ overflow: hidden;
+}
+
+/* Focus handled via CSS — no JS state */
+.VARQ2-input:focus {
+ outline: none;
+}
+
+.VARQ2-input::placeholder {
+ color: #aaa;
+}
+
+.VARQ2-counter-wrapper {
+ position: absolute;
+ bottom: 0.25rem;
+ right: 0.5rem;
+ font-size: 0.75rem;
+ color: #666;
+}
+
+.VARQ2-char-count.error-color {
+ color: red;
+}
+
+.VARQ2-error-message {
+ color: red;
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
+}
+
+@media (min-width: 768px) {
+ .VARQ2-wrapper {
+ max-width: 900px;
+ }
+}
diff --git a/civictechprojects/static/css/styles.scss b/civictechprojects/static/css/styles.scss
index 12022bcd2..f603431ad 100644
--- a/civictechprojects/static/css/styles.scss
+++ b/civictechprojects/static/css/styles.scss
@@ -110,4 +110,7 @@
@import "partials/VARFormTitle";
@import "partials/VARFormDivider";
@import "partials/VARErrorNotification";
-@import "partials/VARSubmitButton";
\ No newline at end of file
+@import "partials/VARSubmitButton";
+@import "partials/VolunteerActivityReportingCardIntro";
+@import "partials/VARSelectWeek";
+@import "partials/VolunteerActivityReportingQ2";
\ No newline at end of file
diff --git a/common/components/componentsBySection/VolunteerActivityReporting/VARSelectWeek.jsx b/common/components/componentsBySection/VolunteerActivityReporting/VARSelectWeek.jsx
new file mode 100644
index 000000000..4ecbfcd77
--- /dev/null
+++ b/common/components/componentsBySection/VolunteerActivityReporting/VARSelectWeek.jsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { format, startOfWeek, endOfWeek, subWeeks } from 'date-fns';
+
+/**
+ * Helper to generate recent week ranges
+ */
+const generateWeeks = (count = 8) => {
+ const weeks = [];
+ const today = new Date();
+
+ for (let i = 0; i < count; i += 1) {
+ const start = startOfWeek(subWeeks(today, i), { weekStartsOn: 1 });
+ const end = endOfWeek(start, { weekStartsOn: 1 });
+
+ weeks.push({
+ label: `${format(start, 'MMMM d')} – ${format(end, 'MMMM d, yyyy')}`,
+ startDate: format(start, 'yyyy-MM-dd'),
+ endDate: format(end, 'yyyy-MM-dd'),
+ });
+ }
+
+ return weeks;
+};
+
+const VARSelectWeek = ({ selectedWeek, onUpdate, errorMessage }) => {
+ const weeks = generateWeeks();
+
+ const handleChange = (e) => {
+ const week = weeks.find(w => w.startDate === e.target.value);
+ if (week && onUpdate) {
+ onUpdate(week);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {!errorMessage && (
+
+ Use dropdown to select an earlier week
+
+ )}
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+
+ );
+};
+
+VARSelectWeek.propTypes = {
+ selectedWeek: PropTypes.shape({
+ label: PropTypes.string.isRequired,
+ startDate: PropTypes.string.isRequired,
+ endDate: PropTypes.string.isRequired,
+ }),
+ onUpdate: PropTypes.func,
+ errorMessage: PropTypes.string,
+};
+
+export default VARSelectWeek;
\ No newline at end of file
diff --git a/common/components/componentsBySection/VolunteerActivityReporting/VolunteerActivityReportingCardIntro.jsx b/common/components/componentsBySection/VolunteerActivityReporting/VolunteerActivityReportingCardIntro.jsx
new file mode 100644
index 000000000..4e2b24201
--- /dev/null
+++ b/common/components/componentsBySection/VolunteerActivityReporting/VolunteerActivityReportingCardIntro.jsx
@@ -0,0 +1,64 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+const VolunteerActivityReportingCardIntro = ({
+ className = '',
+ style,
+ projectName,
+ logActivity: propLogActivity = true,
+ onUpdate,
+}) => {
+ const [logActivity, setLogActivity] = useState(propLogActivity);
+
+ // Sync state if parent updates logActivity (e.g. fetched data)
+ useEffect(() => {
+ setLogActivity(propLogActivity);
+ }, [propLogActivity]);
+
+ const handleToggle = () => {
+ const updatedValue = !logActivity;
+ setLogActivity(updatedValue);
+
+ if (onUpdate) {
+ onUpdate({ logActivity: updatedValue });
+ }
+ };
+
+ return (
+
+
+
+ Project: {projectName}
+
+
+
+
+
+
+ {logActivity
+ ? 'Log activity for this project'
+ : 'No activity to log'}
+
+
+ );
+};
+
+VolunteerActivityReportingCardIntro.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ projectName: PropTypes.string.isRequired,
+ logActivity: PropTypes.bool,
+ onUpdate: PropTypes.func,
+};
+
+export default VolunteerActivityReportingCardIntro;
diff --git a/common/components/componentsBySection/VolunteerActivityReporting/VolunteerActivityReportingQ2.jsx b/common/components/componentsBySection/VolunteerActivityReporting/VolunteerActivityReportingQ2.jsx
new file mode 100644
index 000000000..0bde3aba3
--- /dev/null
+++ b/common/components/componentsBySection/VolunteerActivityReporting/VolunteerActivityReportingQ2.jsx
@@ -0,0 +1,57 @@
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+
+const VolunteerActivityReportingQ2 = ({ className = '', value: propValue = '' }) => {
+ const [value, setValue] = useState(propValue);
+
+ // Keep internal state in sync if parent value changes (e.g. fetched data)
+ useEffect(() => {
+ setValue(propValue);
+ }, [propValue]);
+
+ const charCount = value.length;
+ const isOverLimit = charCount > 150;
+
+ const handleChange = (e) => {
+ setValue(e.target.value);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {charCount}/150
+
+
+
+
+ {isOverLimit && (
+
+ Please limit your response to 150 characters.
+
+ )}
+
+ );
+};
+
+VolunteerActivityReportingQ2.propTypes = {
+ className: PropTypes.string,
+ value: PropTypes.string,
+};
+
+export default VolunteerActivityReportingQ2;
diff --git a/common/components/componentsBySection/VolunteerActivityReporting/stories/VARSelectWeek.stories.jsx b/common/components/componentsBySection/VolunteerActivityReporting/stories/VARSelectWeek.stories.jsx
new file mode 100644
index 000000000..d54d0e28b
--- /dev/null
+++ b/common/components/componentsBySection/VolunteerActivityReporting/stories/VARSelectWeek.stories.jsx
@@ -0,0 +1,52 @@
+import React, { useState } from 'react';
+import VARSelectWeek from '../VARSelectWeek';
+
+export default {
+ title: 'VolunteerActivityReporting/VARSelectWeek',
+ component: VARSelectWeek,
+};
+
+const Wrapper = (args) => {
+ const [week, setWeek] = useState(args.selectedWeek);
+
+ return (
+ {
+ setWeek(selectedWeek);
+ console.log('Selected week:', selectedWeek);
+ }}
+ />
+ );
+};
+
+export const DefaultState = {
+ render: Wrapper,
+ args: {
+ selectedWeek: null,
+ },
+};
+
+export const SelectedWeek = {
+ render: Wrapper,
+ args: {
+ selectedWeek: {
+ label: 'August 8 – August 14, 2022',
+ startDate: '2022-08-08',
+ endDate: '2022-08-14',
+ },
+ },
+};
+
+export const ErrorState = {
+ render: Wrapper,
+ args: {
+ selectedWeek: {
+ label: 'August 8 – August 14, 2022',
+ startDate: '2022-08-08',
+ endDate: '2022-08-14',
+ },
+ errorMessage: 'Report already submitted. Please select a different week to log activity.',
+ },
+};
diff --git a/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingCardIntro.stories.jsx b/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingCardIntro.stories.jsx
new file mode 100644
index 000000000..fe513c57f
--- /dev/null
+++ b/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingCardIntro.stories.jsx
@@ -0,0 +1,51 @@
+import VolunteerActivityReportingCardIntro from '../VolunteerActivityReportingCardIntro';
+
+export default {
+ title: 'VolunteerActivityReporting/VARCardIntro',
+ component: VolunteerActivityReportingCardIntro,
+};
+
+export const DefaultState = {
+ args: {
+ projectName: 'DemocracyLab',
+ logActivity: true,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Displays project name with log activity toggle enabled.',
+ },
+ },
+ },
+};
+
+export const NoActivity = {
+ args: {
+ projectName: 'DemocracyLab',
+ logActivity: false,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Displays placeholder message when there is no activity to log.',
+ },
+ },
+ },
+};
+
+export const Interaction = {
+ args: {
+ projectName: 'DemocracyLab',
+ logActivity: true,
+ onUpdate: (state) => {
+ console.log('Updated state:', state);
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Toggling the switch updates the internal state and triggers onUpdate.',
+ },
+ },
+ },
+};
diff --git a/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingQ1.stories.jsx b/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingQ1.stories.jsx
index 3ac551d56..db841ec50 100644
--- a/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingQ1.stories.jsx
+++ b/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingQ1.stories.jsx
@@ -3,7 +3,7 @@ import VolunteerActivityReportingQ1 from '../VolunteerActivityReportingQ1';
export default {
- title: 'VolunteerActivityReporting/VolunteerAactivityReportingQ1',
+ title: 'VolunteerActivityReporting/VolunteerActivityReportingQ1',
component: VolunteerActivityReportingQ1,
};
diff --git a/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingQ2.stories.jsx b/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingQ2.stories.jsx
new file mode 100644
index 000000000..443469e63
--- /dev/null
+++ b/common/components/componentsBySection/VolunteerActivityReporting/stories/VolunteerActivityReportingQ2.stories.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import VolunteerActivityReportingQ2 from '../VolunteerActivityReportingQ2';
+
+export default {
+ title: 'VolunteerActivityReporting/VolunteerActivityReportingQ2',
+ component: VolunteerActivityReportingQ2,
+};
+
+/**
+ * Empty / default state
+ */
+export const IdleState = {
+ args: {
+ value: '',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Default state with empty input and no validation error.',
+ },
+ },
+ },
+};
+
+/**
+ * Pre-filled valid content
+ */
+export const FilledState = {
+ args: {
+ value: 'Worked on V16 of the screens. Presented at the design team meeting and received feedback.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Input is pre-filled with a valid value under the character limit.',
+ },
+ },
+ },
+};
+
+/**
+ * Character limit exceeded
+ */
+export const ErrorState = {
+ args: {
+ value:
+ 'I created a new version of the form screens, developed the user flow diagram, attended multiple design and team meetings, and iterated on feedback several times. This work took longer than expected but resulted in better overall outcomes.',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Displays validation error when character count exceeds 150.',
+ },
+ },
+ },
+};