Skip to content

Implement price alerts feature and subscription management#28

Merged
Mr-Technician merged 4 commits into
devfrom
feat/price-alerts-ui
Jun 2, 2026
Merged

Implement price alerts feature and subscription management#28
Mr-Technician merged 4 commits into
devfrom
feat/price-alerts-ui

Conversation

@Mr-Technician
Copy link
Copy Markdown
Member

@Mr-Technician Mr-Technician commented Apr 22, 2026

This pull request introduces the core UI and routing for a new "Price Alerts" feature, allowing users to set fare alerts and manage their subscription status. Key changes include new alert-related pages, navigation updates, a call-to-action button in the fares view, and a new CSS file for consistent alert styling.

Feature implementation and routing:

  • Added new pages: Alerts, Subscribed, and Unsubscribed, and registered their routes in AppRouter.js to handle alert creation and subscription status feedback. [1] [2] [3] [4]

User interface enhancements:

  • Added a prominent "Alert me" button to the fares view in Fares.js, enabling users to quickly set a price alert for their selected trip and dates. [1] [2] [3]
  • Updated the navigation bar (Navbar.js) to include a "Price Alerts" link, improving discoverability of the new feature.

Styling:

  • Introduced a dedicated Alerts.css file to style all alert-related components, ensuring a consistent and responsive design across alert pages.

Configuration:

  • Updated the Turnstile site key in Form.js to use an environment variable, improving security and configurability.

Summary by CodeRabbit

  • New Features

    • Added a fare drop alerts system enabling users to subscribe to price alerts via email
    • Added an "Alert me" button on the fares results page that pre-fills alert parameters
    • New alerts management interface supporting subscription, confirmation, and unsubscription flows
    • Updated navigation to include alerts access
  • Bug Fixes

    • Refined date range picker styling for better visual clarity

@Mr-Technician Mr-Technician requested a review from tikkisean April 22, 2026 04:21
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 11a2e32a-be5e-470a-b50f-a08efd06b1c1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/price-alerts-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/Alerts.js (2)

108-128: 💤 Low value

Email validation regex could be more robust.

The email validation regex /^\S+@\S+\.\S+$/ (line 110) is very permissive and would accept inputs like a@b.c or test@..test. Consider using a more robust regex or a validation library to catch obviously malformed addresses before submission.

Suggested improvement

Use a more comprehensive email regex:

- } else if (!/^\S+@\S+\.\S+$/.test(email.trim())) {
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {

Or consider a more thorough pattern that handles edge cases:

} else if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email.trim())) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Alerts.js` around lines 108 - 128, The current email check in Alerts.js
uses a too-permissive regex (the /^\S+@\S+\.\S+$/ test against
email.trim())—replace it with a stricter pattern or a validation helper: either
swap the regex to a more robust one (for example one that enforces valid local
and domain characters and a minimum 2-char TLD) or import/use a lightweight
validator (e.g., validator.isEmail) where the validation logic around email,
email.trim(), and errorText is set; update the condition that sets errorText
("Invalid email address") to use that stricter validation (refer to the email
variable and the existing branch that currently runs the /^\S+@\S+\.\S+$/ test).

82-98: 💤 Low value

Consider validating date ranges during URL parameter hydration.

The date hydration from URL params (lines 84-89) only checks if dates are valid but doesn't verify they're within allowed ranges (e.g., start date should be between today and today+30 days). This could prefill dates that immediately show validation errors, creating a confusing UX.

Optional enhancement

Add range validation during hydration:

  const s = searchParams.get("start");
  const e = searchParams.get("end");
+ const today = dayjs.utc().startOf("d");
+ const maxStart = today.add(30, "d");
  if (s && dayjs(s).isValid()) {
-   setStartDate(dayjs(s).utc().startOf("d"));
+   const parsed = dayjs(s).utc().startOf("d");
+   if (parsed.isSameOrAfter(today.add(1, "d")) && parsed.isSameOrBefore(maxStart)) {
+     setStartDate(parsed);
+   }
  }
  if (e && dayjs(e).isValid()) {
-   setEndDate(dayjs(e).utc().startOf("d"));
+   const parsed = dayjs(e).utc().startOf("d");
+   if (parsed.isSameOrAfter(today.add(1, "d"))) {
+     setEndDate(parsed);
+   }
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Alerts.js` around lines 82 - 98, URL param hydration sets start/end dates
without enforcing allowed ranges, which can prefill invalid values; update the
hydration logic around setStartDate and setEndDate to validate that parsed dates
are within the allowed window (e.g., >= today and <= today.plus(30 days)) before
calling setStartDate/setEndDate, and if out of range either clamp to the nearest
valid date or skip setting them; keep existing validity checks
(dayjs(...).isValid()) and apply the same range check for price before calling
setPriceThreshold (use parseFloat result) and preserve existing accommodation
lookup via ACCOMMODATIONS and setAccommodation.
src/DateRangePopover.js (1)

447-451: 💤 Low value

Consider if !important is necessary for border style override.

The new sx prop applies borderStyle: "dashed none !important" to day cells. While !important is generally discouraged, MUI style overrides sometimes require it to override specificity. However, the sx prop typically has high enough specificity without !important.

Try removing !important to see if the styles apply correctly:

  sx={{
    ".MuiPickersDay-root": {
-     borderStyle: "dashed none !important",
+     borderStyle: "dashed none",
    },
  }}

If the styles don't apply without !important, the current implementation is correct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/DateRangePopover.js` around lines 447 - 451, The sx override in
DateRangePopover applies borderStyle: "dashed none !important" to
".MuiPickersDay-root"; remove the "!important" from that borderStyle in the sx
prop inside the DateRangePopover component and test the picker to see if the
dashed border is applied without it, and only restore "!important" if removing
it fails to override MUI styles; target the sx block referencing
".MuiPickersDay-root" when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Alerts.js`:
- Around line 60-80: Add an error state for station loading and set it in the
fetch catch so the UI can show feedback: introduce a state like stationsError
with setter setStationsError (alongside existing setStations and
setStationsLoaded), update the catch handler in the useEffect that loads
stations to call setStationsError(true) (and optionally setStations([]) and
setStationsLoaded(true)), and clear setStationsError(false) on a successful
fetch. Then render a user-facing message (e.g., an Alert) in the Alerts
component before the form that checks stationsError and displays "Failed to load
stations. Please refresh the page." so users see the failure instead of empty
Autocomplete dropdowns.

In `@src/Fares.js`:
- Around line 869-882: The URL passed to the Button's Link prop (the "to" prop
in the Button component usage) currently contains literal newlines and unencoded
query values; change the construction so the template does not span lines (no
embedded newlines) and URL-encode each query value (origin.code,
destination.code, fareClass) and the formatted dates
(dateRangeStart.format(...), dateRangeEnd.format(...)) — e.g., build the query
string with encodeURIComponent for each value or use URLSearchParams to generate
the query portion, then concatenate with `/alerts?` and pass that single-line
encoded string to the to prop.

In `@src/Navbar.js`:
- Around line 26-30: The mobile label for the Alerts link is rendering a leading
space because the ternary uses " " as the mobile prefix; update the rendering
logic in the Navbar component (the Link that uses useLocation().pathname and the
ternary that produces `{!mobile ? "Fare " : " "}`) to use an empty string "" for
the mobile case (or compute the label once based on useLocation() and mobile and
return either "Fare Alerts", "Alerts", or "Home" accordingly) so mobile displays
"Alerts" without a leading space.

---

Nitpick comments:
In `@src/Alerts.js`:
- Around line 108-128: The current email check in Alerts.js uses a
too-permissive regex (the /^\S+@\S+\.\S+$/ test against email.trim())—replace it
with a stricter pattern or a validation helper: either swap the regex to a more
robust one (for example one that enforces valid local and domain characters and
a minimum 2-char TLD) or import/use a lightweight validator (e.g.,
validator.isEmail) where the validation logic around email, email.trim(), and
errorText is set; update the condition that sets errorText ("Invalid email
address") to use that stricter validation (refer to the email variable and the
existing branch that currently runs the /^\S+@\S+\.\S+$/ test).
- Around line 82-98: URL param hydration sets start/end dates without enforcing
allowed ranges, which can prefill invalid values; update the hydration logic
around setStartDate and setEndDate to validate that parsed dates are within the
allowed window (e.g., >= today and <= today.plus(30 days)) before calling
setStartDate/setEndDate, and if out of range either clamp to the nearest valid
date or skip setting them; keep existing validity checks (dayjs(...).isValid())
and apply the same range check for price before calling setPriceThreshold (use
parseFloat result) and preserve existing accommodation lookup via ACCOMMODATIONS
and setAccommodation.

In `@src/DateRangePopover.js`:
- Around line 447-451: The sx override in DateRangePopover applies borderStyle:
"dashed none !important" to ".MuiPickersDay-root"; remove the "!important" from
that borderStyle in the sx prop inside the DateRangePopover component and test
the picker to see if the dashed border is applied without it, and only restore
"!important" if removing it fails to override MUI styles; target the sx block
referencing ".MuiPickersDay-root" when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bb7bd747-bf68-428c-af84-595b20d30f75

📥 Commits

Reviewing files that changed from the base of the PR and between 6599289 and 8c39e2f.

📒 Files selected for processing (11)
  • src/Alerts.css
  • src/Alerts.js
  • src/AppRouter.js
  • src/DateRangePopover.css
  • src/DateRangePopover.js
  • src/Fares.js
  • src/Form.js
  • src/Navbar.css
  • src/Navbar.js
  • src/Subscribed.js
  • src/Unsubscribed.js
💤 Files with no reviewable changes (1)
  • src/DateRangePopover.css

Comment thread src/Alerts.js
Comment on lines +60 to +80
useEffect(() => {
fetch(`${process.env.REACT_APP_API_DOMAIN}/stations`)
.then((res) => res.json())
.then((data) => {
const sorted = data
.sort((a, b) => a.stateLong.localeCompare(b.stateLong))
.map((station) => ({ ...station, group: station.stateLong }));
setStations(sorted);
const o = searchParams.get("origin");
const d = searchParams.get("destination");
if (o) {
const match = sorted.find((s) => s.code === o);
if (match) setOrigin(match);
}
if (d) {
const match = sorted.find((s) => s.code === d);
if (match) setDestination(match);
}
setStationsLoaded(true);
})
.catch(() => setStationsLoaded(true));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add error feedback when station loading fails.

If the stations API call fails (line 80), stationsLoaded is set to true but the stations array remains empty. This enables the Autocomplete dropdowns with no options, leaving users unable to select stations without any indication of what went wrong.

💡 Suggested improvement

Add an error state and display a message to the user:

  const [stations, setStations] = useState([]);
  const [stationsLoaded, setStationsLoaded] = useState(false);
+ const [stationsError, setStationsError] = useState(false);

  useEffect(() => {
    fetch(`${process.env.REACT_APP_API_DOMAIN}/stations`)
      .then((res) => res.json())
      .then((data) => {
        const sorted = data
          .sort((a, b) => a.stateLong.localeCompare(b.stateLong))
          .map((station) => ({ ...station, group: station.stateLong }));
        setStations(sorted);
        // ... rest of hydration logic
        setStationsLoaded(true);
      })
-     .catch(() => setStationsLoaded(true));
+     .catch(() => {
+       setStationsLoaded(true);
+       setStationsError(true);
+     });

Then display the error in the UI before the form, e.g.:

{stationsError && (
  <Alert severity="error">
    Failed to load stations. Please refresh the page.
  </Alert>
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Alerts.js` around lines 60 - 80, Add an error state for station loading
and set it in the fetch catch so the UI can show feedback: introduce a state
like stationsError with setter setStationsError (alongside existing setStations
and setStationsLoaded), update the catch handler in the useEffect that loads
stations to call setStationsError(true) (and optionally setStations([]) and
setStationsLoaded(true)), and clear setStationsError(false) on a successful
fetch. Then render a user-facing message (e.g., an Alert) in the Alerts
component before the form that checks stationsError and displays "Failed to load
stations. Please refresh the page." so users see the failure instead of empty
Autocomplete dropdowns.

Comment thread src/Fares.js
Comment on lines +869 to +882
<Button
component={Link}
data-rybbit-event="alert"
endIcon={<NotificationsActiveIcon />}
to={`/alerts?origin=${origin.code}&destination=${
destination.code
}&start=${dateRangeStart.format(
"YYYY-MM-DD"
)}&end=${dateRangeEnd.format("YYYY-MM-DD")}
&accommodation=${fareClass}`}
variant="outlined"
>
Alert me
</Button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix URL construction: remove literal newlines and add URL encoding.

The template literal spans multiple lines (873-878), which embeds literal newline characters into the URL string. This will result in a malformed URL or newlines encoded as %0A. Additionally, query parameter values (origin.code, destination.code, fareClass) are not URL-encoded, which will break if they contain special characters like spaces, &, or =.

🔗 Proposed fix
 				<Button
 					component={Link}
 					data-rybbit-event="alert"
 					endIcon={<NotificationsActiveIcon />}
-					to={`/alerts?origin=${origin.code}&destination=${
-						destination.code
-					}&start=${dateRangeStart.format(
-						"YYYY-MM-DD"
-					)}&end=${dateRangeEnd.format("YYYY-MM-DD")}
-					&accommodation=${fareClass}`}
+					to={`/alerts?origin=${encodeURIComponent(origin.code)}&destination=${encodeURIComponent(destination.code)}&start=${dateRangeStart.format("YYYY-MM-DD")}&end=${dateRangeEnd.format("YYYY-MM-DD")}&accommodation=${encodeURIComponent(fareClass)}`}
 					variant="outlined"
 				>
 					Alert me
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Fares.js` around lines 869 - 882, The URL passed to the Button's Link
prop (the "to" prop in the Button component usage) currently contains literal
newlines and unencoded query values; change the construction so the template
does not span lines (no embedded newlines) and URL-encode each query value
(origin.code, destination.code, fareClass) and the formatted dates
(dateRangeStart.format(...), dateRangeEnd.format(...)) — e.g., build the query
string with encodeURIComponent for each value or use URLSearchParams to generate
the query portion, then concatenate with `/alerts?` and pass that single-line
encoded string to the to prop.

Comment thread src/Navbar.js
Mr-Technician and others added 2 commits June 1, 2026 22:37
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@Mr-Technician Mr-Technician merged commit 02f7760 into dev Jun 2, 2026
1 check passed
@Mr-Technician Mr-Technician deleted the feat/price-alerts-ui branch June 2, 2026 03:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants