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