Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions civictechprojects/static/css/partials/_VARSelectWeek.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.var-select-week {
display: flex;
flex-direction: column;
margin-bottom: 16px;

&__label {
font-weight: 600;
margin-bottom: 8px;
color: $color-text-strongest;
}

&__select {
padding: 10px 12px;
border: 1px solid $color-border-weak;
border-radius: 8px;
background: $color-background-light;
appearance: none;
}

&__error {
color: $color-red-50;
margin-top: 6px;
font-size: 0.9rem;
}

/* small responsive tweak */
@media (max-width: 480px) {
&__select {
padding: 8px 10px;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.var-card-intro {
display: flex;
flex-direction: column;
margin-bottom: 16px;
background: $color-background-light;
border: 1px solid $color-border-weakest;
border-radius: 8px;
padding: 12px;

&__label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}

&__title {
font-weight: 600;
color: $color-text-strongest;
font-size: 1rem;
}

&__toggle {
width: 40px;
height: 24px;
}

&__hint {
color: $color-text-strong;
font-size: 0.9rem;
margin-top: 8px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.var-q2 {
display: flex;
flex-direction: column;
margin-bottom: 16px;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Components like this should not define margin, background color, and should not have border or border-radius or padding. A parent component may pass in a className that defines these things, but to be reusable in a variety of situations, the parent should be the one to determine these things.

Please tell this to the ai and ask it to update this component, and review the other components considering this, and update the spec to include this guideline.

Also, if you use the chrome dev console to turn off those styles, the result looks much more like what's in figma.
Current Storybook
image

Storybook with outer styles turned off
image

Figma
image

Also, in figma there is a little thing in the bottom right called the resize handle: https://www.figma.com/design/WADcmVjJh5ARVoZ09xlpfdFN/DemocracyLab?node-id=39337-90309&t=gP1nOrA8DXDDAPpA-4

But the component we have auto resizes and I don't see anything like that in the code, so I think we should ignore it, but say we are ignoring it in the spec.

Lastly, the text at the top of the component needs to match what's in figma. Meaning instead of "summary" it should say "In a few words, describe what you did during the week."

And I'm not saying you should make these changes. I'm saying you should instruct the ai to do it, and to update the spec so that it follows these guidelines going forward.

background: $color-background-light;
border: 1px solid $color-border-weakest;
border-radius: 8px;
padding: 10px;

&__label {
font-weight: 600;
margin-bottom: 8px;
color: $color-text-strongest;
}

&__input-wrapper {
position: relative;
display: flex;
}

&__input {
padding: 10px 12px;
border: 1px solid $color-border-weak;
border-radius: 6px;
background: $color-background-light;
resize: none;
overflow: hidden;
min-height: 80px;
font-family: inherit;
font-size: inherit;
flex: 1;
padding-bottom: 30px;

&--error {
border-color: $color-red-50;
}
}

&__counter {
position: absolute;
bottom: 8px;
right: 8px;
color: $color-text-strong;
font-size: 0.85rem;
pointer-events: none;

&--over-limit {
color: $color-red-50;
font-weight: 600;
}
}

&__error {
color: $color-red-50;
font-size: 0.85rem;
margin-top: 6px;
}
}
5 changes: 4 additions & 1 deletion civictechprojects/static/css/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,7 @@
@import "partials/VARFormTitle";
@import "partials/VARFormDivider";
@import "partials/VARErrorNotification";
@import "partials/VARSubmitButton";
@import "partials/VARSubmitButton";
@import "partials/VolunteerActivityReportingCardIntro";
@import "partials/VARSelectWeek";
@import "partials/VolunteerActivityReportingQ2";
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// @flow
import * as React from 'react';

type Props = {|
name?: string,
defaultValue?: string,
errorMessage?: ?string,
weeksBack?: number,
allowFuture?: boolean,
|};

function isoDate(d: Date): string {
return d.toISOString().slice(0, 10);
}

function getWeekStart(date: Date): Date {
// Return Monday as week start
const d = new Date(date);
const day = d.getDay();
const diff = (day + 6) % 7; // days since Monday
d.setDate(d.getDate() - diff);
d.setHours(0, 0, 0, 0);
return d;
}

export default function VARSelectWeek({
name = 'week_start',
defaultValue,
errorMessage,
weeksBack = 12,
allowFuture = false,
}: Props): React.Node {
const today = new Date();
const start = getWeekStart(today);
const options = [];

for (let i = 0; i < weeksBack; i++) {
const weekStart = new Date(start);
weekStart.setDate(start.getDate() - i * 7);
const iso = isoDate(weekStart);
const labelStart = weekStart.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
const labelEnd = weekEnd.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
options.push({ value: iso, label: `${labelStart} - ${labelEnd}` });
}

// optional future weeks
if (allowFuture) {
const future = new Date(start);
future.setDate(start.getDate() + 7);
options.unshift({ value: isoDate(future), label: 'Next week' });
}

return (
<div className="var-select-week">
<label className="var-select-week__label">Week</label>
<select name={name} defaultValue={defaultValue || ''} className="var-select-week__select">
<option value="">Select a week</option>
{options.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{errorMessage && <div className="var-select-week__error">{errorMessage}</div>}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// @flow
import * as React from 'react';

type Props = {|
className?: string,
projectName: string,
projectId: string | number,
defaultChecked?: boolean,
|};

export default function VolunteerActivityReportingCardIntro({
className = '',
projectName,
projectId,
defaultChecked = false,
}: Props): React.Node {
const inputName = `project_${projectId}_log_activity`;

return (
<div className={(className || '') + ' var-card-intro'}>
<label className="var-card-intro__label">
<span className="var-card-intro__title">{projectName}</span>
<input
type="checkbox"
name={inputName}
defaultChecked={defaultChecked}
className="var-card-intro__toggle"
/>
</label>
{!defaultChecked && (
<div className="var-card-intro__hint">No activity to log</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// @flow
import * as React from 'react';

type Props = {|
name?: string,
className?: string,
defaultValue?: string,
maxLength?: number,
required?: boolean,
error?: boolean,
errorMessage?: string,
|};

export default function VolunteerActivityReportingQ2({
name = 'activity_summary',
className = '',
defaultValue = '',
maxLength = 150,
required = false,
error = false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error and errorMessage should not be props. They should be internal to this component. And the component should set the error state.

errorMessage = 'Please limit your response to 150 characters',
}: Props): React.Node {
const [value, setValue] = React.useState(defaultValue);
const textareaRef = React.useRef(null);
const inputClasses = 'var-q2__input' + (error ? ' var-q2__input--error' : '');
const isOverLimit = value.length > maxLength;
const counterClasses = 'var-q2__counter' + (isOverLimit ? ' var-q2__counter--over-limit' : '');

const handleChange = (e) => {
setValue(e.target.value);
autoResize();
};

const autoResize = () => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
}
};

React.useEffect(() => {
autoResize();
}, []);

return (
<div className={(className || '') + ' var-q2'}>
<label className="var-q2__label" htmlFor={name}>Summary</label>
<div className="var-q2__input-wrapper">
<textarea
ref={textareaRef}
id={name}
name={name}
className={inputClasses}
maxLength={maxLength}
defaultValue={defaultValue}
onChange={handleChange}
required={required}
/>
<div className={counterClasses}>{String(value.length)}/{maxLength}</div>
</div>
{error && <div className="var-q2__error">{errorMessage}</div>}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// @flow
import * as React from 'react';
import VARSelectWeek from '../VARSelectWeek';

export default {
title: 'VolunteerActivityReporting/VARSelectWeek',
component: VARSelectWeek,
};

export const Default = {
args: {
name: 'week_start',
weeksBack: 12,
allowFuture: false,
},
};

export const WithError = {
args: {
name: 'week_start',
errorMessage: 'Please choose a week',
weeksBack: 4,
},
};

export const WithDefaultValue = {
args: {
name: 'week_start',
defaultValue: '2025-11-17',
weeksBack: 12,
allowFuture: false,
},
};

export const ErrorWithLongMessage = {
args: {
name: 'week_start',
errorMessage: 'Invalid date range. Please select a week within the last 12 weeks.',
weeksBack: 12,
},
};

export const LimitedWeeksBack = {
args: {
name: 'week_start',
weeksBack: 4,
allowFuture: false,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// @flow
import * as React from 'react';
import VolunteerActivityReportingCardIntro from '../VolunteerActivityReportingCardIntro';

export default {
title: 'VolunteerActivityReporting/VolunteerActivityReportingCardIntro',
component: VolunteerActivityReportingCardIntro,
};

export const Default = {
args: {
projectName: 'Democracy Lab Project',
projectId: '1',
defaultChecked: false,
},
};

export const Checked = {
args: {
projectName: 'Open Data Project',
projectId: '2',
defaultChecked: true,
},
};

export const UncheckedWithHint = {
args: {
projectName: 'Community Engagement Project',
projectId: '3',
defaultChecked: false,
},
};

export const LongProjectName = {
args: {
projectName: 'Building Accessible Government Data Portals for Civic Technology Innovation',
projectId: '4',
defaultChecked: false,
},
};
Loading