mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 15:59:02 +00:00
Behavioural + RUI audit follow-ups.
The yellow overload finding flagged three concurrent yellow surfaces
on the show page: the "Behind" status pill, the catch-up alert, and
the open-pledge banner(s). Demoting the alert to outline ownership
of the primary CTA addressed one layer, but the pill kept fighting
the alert for hue attention. "Behind" is a state, not a call to
action; the alert owns the action signal.
Switch the pill's classes from `bg-yellow-500/10 text-yellow-700`
to `bg-surface-inset text-yellow-700` (with the same dark-mode
override). Background goes neutral (matches paused/archived chips);
the text keeps the warning hue and the triangle-alert icon stays.
Signal preserved, weight reduced. The yellow alert below now reads
as the primary nudge instead of one of three matching tones.
Also: copy/em-dash sweep across goal surfaces. User-facing strings
that contained em-dashes ("Reaches 70% — $X of $Y", "into your
linked account — Sure will catch it", "You're at 80% — $X of $Y")
read as a stylistic tic; replace with comma/period/period
respectively. Form-stepper review placeholders "—" become "…"
(ellipsis reads as "not yet set" without the typographic weight).
Code comments + log messages also scrubbed for consistency; awkward
sed artifacts (//. its...) restored to readable English.
No locale-key shape changes; pure string-content edits + one
component-style tweak.
88 lines
2.9 KiB
JavaScript
88 lines
2.9 KiB
JavaScript
import { Controller } from "@hotwired/stimulus";
|
|
|
|
// Live impact preview for the record-pledge modal. Reads current balance +
|
|
// target amount from values and updates a preview sentence each keystroke.
|
|
// Template strings come from ERB so the wording stays localized.
|
|
export default class extends Controller {
|
|
static targets = [
|
|
"amountInput",
|
|
"preview",
|
|
"accountSelect",
|
|
"helperConnected",
|
|
"helperManual",
|
|
];
|
|
static values = {
|
|
currentBalance: Number,
|
|
targetAmount: Number,
|
|
currency: String,
|
|
templateZero: String,
|
|
templateNonzero: String,
|
|
templateReached: String,
|
|
};
|
|
|
|
connect() {
|
|
this.update();
|
|
this.accountChanged();
|
|
}
|
|
|
|
// Helper text reacts to the currently-selected account, not the goal as a
|
|
// whole. A mixed-funding goal (one connected account + one manual) used to
|
|
// paint the "connected" helper even if the user then picked the manual
|
|
// account from the dropdown; the saved pledge would be `kind: manual_save`
|
|
// (correct, per `kind_for_account` in the controller) but the helper read
|
|
// "transfer-style" copy until submission.
|
|
accountChanged() {
|
|
if (!this.hasAccountSelectTarget) return;
|
|
if (!this.hasHelperConnectedTarget || !this.hasHelperManualTarget) return;
|
|
const opt = this.accountSelectTarget.selectedOptions[0];
|
|
const isManual = opt?.dataset.manual === "true";
|
|
this.helperConnectedTarget.hidden = isManual;
|
|
this.helperManualTarget.hidden = !isManual;
|
|
}
|
|
|
|
update() {
|
|
if (!this.hasPreviewTarget) return;
|
|
|
|
const amount = this.#amountValue();
|
|
const newTotal = this.currentBalanceValue + amount;
|
|
const target = this.targetAmountValue;
|
|
const reached = newTotal >= target && target > 0;
|
|
const percent = target > 0 ? Math.min(100, Math.round((newTotal / target) * 100)) : 0;
|
|
|
|
let text;
|
|
if (reached) {
|
|
text = this.templateReachedValue.replace("{target}", this.#money(target));
|
|
} else if (amount === 0) {
|
|
text = this.templateZeroValue
|
|
.replaceAll("{percent}", percent.toString())
|
|
.replaceAll("{current}", this.#money(this.currentBalanceValue))
|
|
.replaceAll("{target}", this.#money(target));
|
|
} else {
|
|
text = this.templateNonzeroValue
|
|
.replaceAll("{percent}", percent.toString())
|
|
.replaceAll("{newTotal}", this.#money(newTotal))
|
|
.replaceAll("{target}", this.#money(target));
|
|
}
|
|
|
|
this.previewTarget.textContent = text;
|
|
}
|
|
|
|
#amountValue() {
|
|
if (!this.hasAmountInputTarget) return 0;
|
|
const parsed = Number.parseFloat(this.amountInputTarget.value);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
}
|
|
|
|
#money(value) {
|
|
try {
|
|
return new Intl.NumberFormat(undefined, {
|
|
style: "currency",
|
|
currency: this.currencyValue || "USD",
|
|
maximumFractionDigits: 0,
|
|
}).format(value);
|
|
} catch {
|
|
return `${this.currencyValue || "$"}${Math.round(value).toLocaleString()}`;
|
|
}
|
|
}
|
|
}
|