Skip to content

Commit

Permalink
feat(TripPlanner): tagged itineraries + feedback form (#1879)
Browse files Browse the repository at this point in the history
* refactor(TripPlanController): remove extra inputs
For the remaining wheelchair accessibility checkbox, make it initially selected.

* refactor(OpenTripPlanner): stop merging accessible+inaccessible
Because requests without `wheelchair: true` do not compute the accessibilityScore, remove the (misleading, inaccurate) "Might not be accessible" icon from the result.

* feat(OTP.Parser): improved error handling
* deps(mix): add open_trip_planner_client
* refactor: use OpenTripPlannerClient
- deprecate `TripPlan.Api` behaviour and the modules using it (`TripPlan.Api.MockPlanner` and `TripPlan.Api.OpenTripPlanner`) in favor of using the client (`OpenTripPlannerClient` and its behaviour `OpenTripPlannerClient.Behaviour`).
- remove `TripPlan.plan/4` function

* deps(mix): install Mox
* feat(OpenTripPlannerClient.Mock): add test stub
* deps(mix): add ex_machina and faker
* feat(Test.Support.Factory): trip plan data generation
* feat: request itinerary tags
* feat: render itinerary tags
* refactor: one tag per itinerary
* feat(TripPlan.Query): different tags for arrive/depart
* fixup: one tag per itinerary
* feat(TripPlan.Query): add new MostDirect tag
* fix: preserve selected modes and wheelchair options
* cleanup: remove unused component
* feat(FeedbackForm): new component
* chore: stop server-rendering TripPlannerResults
* chore(TripPlan.Query): remove unused argument
* feat(TripPlanView): add trip_plan_metadata/1
* pass trip plan metadata to frontend
* feat(TripPlan.Feedback): submit response to backend
  • Loading branch information
thecristen authored Mar 4, 2024
1 parent db3b6c0 commit 9332225
Show file tree
Hide file tree
Showing 74 changed files with 2,600 additions and 2,851 deletions.
16 changes: 15 additions & 1 deletion assets/css/_trip-plan-results.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,28 @@
}
}

&-tag {
background-color: $brand-primary-darkest;
border-radius: 0 0 8px;
color: $white;
font-weight: bold;
grid-column-end: 3;
grid-column-start: 1;
margin-bottom: 1rem;
margin-left: -1rem;
margin-top: -1rem;
padding: .5rem 1rem;
width: fit-content;
}

/* stylelint-disable property-no-vendor-prefix */
&-header {
background-color: $brand-primary-lightest-contrast;
border: 1px solid $brand-primary;
border-bottom: 0;
display: -ms-grid;
display: grid;
-ms-grid-columns: 1fr 1fr;
-ms-grid-columns: 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr;
line-height: 1.5;
margin-top: 0;
Expand Down
4 changes: 4 additions & 0 deletions assets/css/_utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@
margin-top: 0 !important;
}

.mt-05 {
margin-top: calc(#{$base-spacing} / 2) !important;
}

.mt-1 {
margin-top: $base-spacing !important;
}
Expand Down
2 changes: 2 additions & 0 deletions assets/css/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ $gray-stripe-pattern: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAA
$form-check-margin-bottom: $base-spacing-sm;
$form-check-input-gutter: $base-spacing;
$form-check-input-margin-y: 0;
$input-border-color: $brand-primary;
$input-btn-border-width: 2px;

// Font-awesome settings
$fa-font-path: '/fonts';
2 changes: 0 additions & 2 deletions assets/react_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import readline from "readline";
import TransitNearMe from "../assets/ts/tnm/components/TransitNearMe";
import AdditionalLineInfo from "../assets/ts/schedule/components/AdditionalLineInfo";
import ScheduleFinder from "../assets/ts/schedule/components/ScheduleFinder";
import TripPlannerResults from "../assets/ts/trip-plan-results/components/TripPlannerResults";
import ProjectsPage from "../assets/ts/projects/components/ProjectsPage";
import LiveCrowdingIcon from "./ts/schedule/components/line-diagram/LiveCrowdingIcon";

Expand Down Expand Up @@ -56,7 +55,6 @@ const Components = {
ScheduleFinder,
AdditionalLineInfo,
TransitNearMe,
TripPlannerResults,
ProjectsPage,
LiveCrowdingIcon
};
Expand Down
21 changes: 0 additions & 21 deletions assets/ts/components/Feedback.tsx

This file was deleted.

190 changes: 190 additions & 0 deletions assets/ts/components/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { uniqueId } from "lodash";
import React, {
ChangeEvent,
ReactElement,
useEffect,
useRef,
useState
} from "react";
import renderFa from "../helpers/render-fa";

interface VoteButtonProps {
upOrDown: "up" | "down";
ariaLabel: string;
onPress: Function;
isActive?: boolean;
}
const VoteButton = ({
upOrDown,
ariaLabel,
onPress,
isActive = false
}: VoteButtonProps): ReactElement<HTMLButtonElement> => {
return (
<button
type="button"
className="btn btn-sm btn-link"
aria-pressed={isActive}
aria-label={ariaLabel}
onClick={() => onPress()}
>
{renderFa(
"",
`fa-${isActive ? "solid" : "regular"} fa-thumbs-${upOrDown}`,
true
)}
</button>
);
};

const sendFormData = (
formEl: HTMLFormElement | null,
callback: Function
): void => {
if (!formEl) return;
const formData = new FormData(formEl);
const formJson = Object.fromEntries(formData.entries());
callback(formJson);
};

interface FeedbackFormProps {
promptText: string;
upLabel: string;
downLabel: string;
commentPromptText: string;
commentLabel: string;
commentPlaceholder: string;
formDataCallback: (formData: Record<string, string>) => void;
}
/**
* A compact form featuring upvote/downvote button, optional comment submission,
* and invocation of a callback function on submit
*/
const FeedbackForm = ({
promptText,
upLabel,
downLabel,
commentPromptText,
commentLabel,
commentPlaceholder,
formDataCallback
}: FeedbackFormProps): ReactElement<HTMLElement> => {
const [showComment, setShowComment] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const [vote, setVote] = useState<"up" | "down" | null>(null);
const [hasComment, setHasComment] = useState(false);

const upVote = (): void => {
setVote(v => (v === "up" ? null : "up"));
setShowComment(false);
};
const downVote = (): void => setVote(v => (v === "down" ? null : "down"));
const showCommentPrompt = vote === "down";
const toggleComment = (): void => {
setShowComment(show => !show);
};
const handleTextAreaChange = (
event: ChangeEvent<HTMLTextAreaElement>
): void => {
const {
target: { value }
} = event;
setHasComment(!!value && value !== "");
};

const formRef = useRef<HTMLFormElement>(null);

useEffect(() => {
sendFormData(formRef.current, formDataCallback);
}, [vote, formDataCallback]);

if (showConfirmation)
return (
<p>
Your feedback has been submitted. Thanks for helping us improve the
website!
</p>
);

const commentId = uniqueId();
return (
<form
ref={formRef}
aria-label="feedback"
method="post"
onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
sendFormData(event.currentTarget, formDataCallback);
setShowComment(false);
setShowConfirmation(true);
}}
>
<div className="d-inline me-8">
{vote === null ? promptText : "Thanks for your response!"}
</div>
<div className="btn-group">
<input type="hidden" name="feedback_vote" value={vote || ""} />
<VoteButton
ariaLabel={upLabel}
upOrDown="up"
onPress={upVote}
isActive={vote === "up"}
/>
<VoteButton
ariaLabel={downLabel}
upOrDown="down"
onPress={downVote}
isActive={vote === "down"}
/>
</div>
<span aria-live="assertive">
{showCommentPrompt && (
<button
type="button"
className="btn btn-sm btn-link"
style={{ border: 0, paddingInline: "0rem" }}
aria-controls={commentId}
aria-expanded={showComment}
aria-pressed={showComment}
onClick={toggleComment}
>
{commentPromptText}
</button>
)}
</span>
{showComment && (
<div id={commentId} className="mt-05">
<div className="form-group">
<label className="w-100">
<strong>{commentLabel}</strong>
<textarea
name="feedback_long"
className="form-control"
rows={2}
maxLength={1000}
placeholder={commentPlaceholder}
onChange={handleTextAreaChange}
/>
</label>
</div>
<button
className="btn btn-sm btn-primary"
type="submit"
disabled={!hasComment}
>
Submit
</button>
<button
className="btn btn-sm btn-link"
type="button"
onClick={() => setShowComment(false)}
>
Cancel
</button>
</div>
)}
</form>
);
};

export default FeedbackForm;
Loading

0 comments on commit 9332225

Please sign in to comment.