diff --git a/app/components/goals/funding_accounts_breakdown_component.rb b/app/components/goals/funding_accounts_breakdown_component.rb index e63d7695c..345d40988 100644 --- a/app/components/goals/funding_accounts_breakdown_component.rb +++ b/app/components/goals/funding_accounts_breakdown_component.rb @@ -44,7 +44,7 @@ class Goals::FundingAccountsBreakdownComponent < ApplicationComponent # never blank if a string is missing. def accountable_label(account) if account.subtype.present? - I18n.t("goals.form_stepper.step1.subtypes.#{account.subtype}", default: account.subtype.titleize) + I18n.t("goals.form.subtypes.#{account.subtype}", default: account.subtype.titleize) else type = account.accountable_type.to_s I18n.t("accounts.types.#{type.underscore}", default: type.titleize) diff --git a/app/javascript/controllers/goal_form_controller.js b/app/javascript/controllers/goal_form_controller.js new file mode 100644 index 000000000..74229eafd --- /dev/null +++ b/app/javascript/controllers/goal_form_controller.js @@ -0,0 +1,141 @@ +import { Controller } from "@hotwired/stimulus"; + +// Single-form controller for the goal create / edit modal. +// +// Replaces the 2-step stepper: the form is short enough that all fields +// fit on one panel, so the previous review step (which only showed a +// derived "Save $X/mo to hit it on time" hint) collapses into an inline +// live hint below the target date. Validation + avatar preview from the +// name field still live here. +export default class extends Controller { + static targets = [ + "nameInput", + "amountInput", + "dateInput", + "avatarPreview", + "nameError", + "amountError", + "accountsError", + "linkedAccountCheckbox", + "suggested", + ]; + + static INVALID_INPUT_CLASSES = ["ring-2", "ring-destructive", "border-destructive"]; + + static values = { + currency: { type: String, default: "USD" }, + suggestedWithDate: { type: String, default: "Save {monthly}/mo across {accounts} to hit it on time." }, + suggestedNoDate: { type: String, default: "Set a target date to project a finish line." }, + }; + + connect() { + // Capture the default avatar contents (the "target" icon SVG) so we + // can restore it when the user clears the name field after typing. + if (this.hasAvatarPreviewTarget) { + this._defaultAvatarHTML = this.avatarPreviewTarget.innerHTML; + } + this.updateSuggested(); + } + + nameChanged() { + if (this.hasNameInputTarget) { + this.clearFieldError(this.nameInputTarget, this.hasNameErrorTarget ? this.nameErrorTarget : null); + } + if (!this.hasAvatarPreviewTarget || !this.hasNameInputTarget) return; + + // If the user has explicitly picked an icon, leave it alone. Name + // changes shouldn't undo an explicit choice. + const iconPicked = this.element.querySelector('input[name="goal[icon]"]:checked'); + if (iconPicked) return; + + const name = this.nameInputTarget.value.trim(); + if (name) { + this.avatarPreviewTarget.textContent = name.charAt(0).toUpperCase(); + } else if (this._defaultAvatarHTML) { + // Captured at connect. Restore the default "target" icon from the + // server-rendered template, not a "?" character. + this.avatarPreviewTarget.innerHTML = this._defaultAvatarHTML; + } + } + + amountChanged() { + if (this.hasAmountInputTarget) { + this.clearFieldError(this.amountInputTarget, this.hasAmountErrorTarget ? this.amountErrorTarget : null); + } + } + + linkedAccountChanged() { + this.updateSuggested(); + if (this.linkedAccountCheckboxTargets.some((cb) => cb.checked) && this.hasAccountsErrorTarget) { + this.accountsErrorTarget.classList.add("hidden"); + } + } + + // Hook for any input that influences the suggested-pace hint + // (target_amount, target_date). Also re-evaluates as accounts toggle. + suggestedChanged() { + this.amountChanged(); + this.updateSuggested(); + } + + updateSuggested() { + if (!this.hasSuggestedTarget) return; + + const amount = this.hasAmountInputTarget ? Number.parseFloat(this.amountInputTarget.value) : NaN; + const dateValue = this.hasDateInputTarget ? this.dateInputTarget.value : null; + const checkedCount = this.linkedAccountCheckboxTargets.filter((cb) => cb.checked).length; + + const amountValid = Number.isFinite(amount) && amount > 0; + if (!amountValid || checkedCount === 0) { + this.suggestedTarget.classList.add("hidden"); + this.suggestedTarget.textContent = ""; + return; + } + + let text; + if (dateValue) { + const months = this.#monthsBetween(new Date(), new Date(dateValue)); + if (months <= 0) { + this.suggestedTarget.classList.add("hidden"); + this.suggestedTarget.textContent = ""; + return; + } + const perMonth = Math.ceil(amount / months); + const accountLabel = `${checkedCount} ${checkedCount === 1 ? "account" : "accounts"}`; + text = this.suggestedWithDateValue + .replace("{monthly}", this.#money(perMonth)) + .replace("{accounts}", accountLabel); + } else { + text = this.suggestedNoDateValue; + } + + this.suggestedTarget.textContent = text; + this.suggestedTarget.classList.remove("hidden"); + } + + showFieldError(input, errorEl) { + if (input) input.classList.add(...this.constructor.INVALID_INPUT_CLASSES); + if (errorEl) errorEl.classList.remove("hidden"); + } + + clearFieldError(input, errorEl) { + if (input) input.classList.remove(...this.constructor.INVALID_INPUT_CLASSES); + if (errorEl) errorEl.classList.add("hidden"); + } + + #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()}`; + } + } + + #monthsBetween(from, to) { + return (to - from) / (1000 * 60 * 60 * 24 * 30.44); + } +} diff --git a/app/javascript/controllers/goal_stepper_controller.js b/app/javascript/controllers/goal_stepper_controller.js deleted file mode 100644 index ceb2b1326..000000000 --- a/app/javascript/controllers/goal_stepper_controller.js +++ /dev/null @@ -1,295 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -// 2-step modal stepper for creating a goal. -// -// Single
with two panels. Step 1 collects identity (name, amount, -// date, color, notes, linked accounts). Step 2 reviews and submits. All -// state lives in the DOM. No half-records, single POST. -export default class extends Controller { - static targets = [ - "step1Panel", - "step2Panel", - "step1Indicator", - "step2Indicator", - "step1Circle", - "step2Circle", - "stepperLine", - "modalSubtitle", - "nameInput", - "amountInput", - "avatarPreview", - "nameError", - "amountError", - "accountsError", - "linkedAccountCheckbox", - "reviewName", - "reviewSummary", - "reviewSuggested", - "footerLeftButton", - "footerRightButton", - "submitButton", - ]; - - static INVALID_INPUT_CLASSES = ["ring-2", "ring-destructive", "border-destructive"]; - - static values = { - step1Subtitle: { type: String, default: "Step 1 of 2 · Goal details" }, - step2Subtitle: { type: String, default: "Step 2 of 2 · Review & start" }, - cancelLabel: { type: String, default: "Cancel" }, - backLabel: { type: String, default: "Back" }, - continueLabel: { type: String, default: "Continue" }, - submitLabel: { type: String, default: "Create goal" }, - currency: { type: String, default: "USD" }, - summaryWithDate: { type: String, default: "{amount} by {date}" }, - summaryNoDate: { type: String, default: "{amount}" }, - accountCountOne: { type: String, default: "1 account" }, - accountCountOther: { type: String, default: "{count} accounts" }, - suggestedWithDate: { type: String, default: "Save {monthly}/mo across {accounts} to hit it on time." }, - suggestedNoDate: { type: String, default: "Set a target date to project a finish line." }, - }; - - connect() { - this.currentStep = 1; - this.refreshSubmitState(); - // Capture the default avatar contents (the "target" icon SVG) so we - // can restore it when the user clears the name field after typing. - if (this.hasAvatarPreviewTarget) { - this._defaultAvatarHTML = this.avatarPreviewTarget.innerHTML; - } - } - - blockEnter(event) { - if (this.currentStep !== 1) return; - // Allow Enter in the notes textarea so newlines work. - if (event.target.tagName === "TEXTAREA") return; - event.preventDefault(); - // Mirror Continue: validate + advance instead of swallowing silently. - this.next(); - } - - footerLeft(event) { - event.preventDefault(); - this.back(); - } - - footerRight(event) { - event.preventDefault(); - if (this.currentStep === 1) { - this.next(); - } else { - this.submitButtonTarget.click(); - } - } - - next() { - if (!this.validateStep1()) return; - - this.currentStep = 2; - this.step1PanelTarget.classList.add("hidden"); - this.step2PanelTarget.classList.remove("hidden"); - this.updateStepperState(); - this.updateReview(); - this.updateFooter(); - } - - back() { - this.currentStep = 1; - this.step2PanelTarget.classList.add("hidden"); - this.step1PanelTarget.classList.remove("hidden"); - this.updateStepperState(); - this.updateFooter(); - } - - linkedAccountChanged() { - this.refreshSubmitState(); - this.updateReview(); - if (this.linkedAccountCheckboxTargets.some((cb) => cb.checked) && this.hasAccountsErrorTarget) { - this.accountsErrorTarget.classList.add("hidden"); - } - } - - nameChanged() { - if (this.hasNameInputTarget) { - this.clearFieldError(this.nameInputTarget, this.hasNameErrorTarget ? this.nameErrorTarget : null); - } - if (!this.hasAvatarPreviewTarget || !this.hasNameInputTarget) return; - - // If the user has explicitly picked an icon, leave it alone. Name - // changes shouldn't undo an explicit choice. - const iconPicked = this.element.querySelector('input[name="goal[icon]"]:checked'); - if (iconPicked) return; - - const name = this.nameInputTarget.value.trim(); - if (name) { - this.avatarPreviewTarget.textContent = name.charAt(0).toUpperCase(); - } else if (this._defaultAvatarHTML) { - // Captured at connect. Restore the default "target" icon from the - // server-rendered template, not a "?" character. - this.avatarPreviewTarget.innerHTML = this._defaultAvatarHTML; - } - } - - validateStep1() { - let ok = true; - let firstInvalid = null; - - const nameInput = this.hasNameInputTarget ? this.nameInputTarget : null; - if (nameInput && nameInput.value.trim().length === 0) { - this.showFieldError(nameInput, this.hasNameErrorTarget ? this.nameErrorTarget : null); - firstInvalid ||= nameInput; - ok = false; - } - - const amountInput = this.hasAmountInputTarget ? this.amountInputTarget : null; - const amountValue = amountInput ? Number.parseFloat(amountInput.value) : Number.NaN; - if (amountInput && (!Number.isFinite(amountValue) || amountValue <= 0)) { - this.showFieldError(amountInput, this.hasAmountErrorTarget ? this.amountErrorTarget : null); - firstInvalid ||= amountInput; - ok = false; - } - - if (!this.linkedAccountCheckboxTargets.some((cb) => cb.checked)) { - if (this.hasAccountsErrorTarget) this.accountsErrorTarget.classList.remove("hidden"); - ok = false; - } - - if (firstInvalid) firstInvalid.focus(); - return ok; - } - - showFieldError(input, errorEl) { - if (input) input.classList.add(...this.constructor.INVALID_INPUT_CLASSES); - if (errorEl) errorEl.classList.remove("hidden"); - } - - clearFieldError(input, errorEl) { - if (input) input.classList.remove(...this.constructor.INVALID_INPUT_CLASSES); - if (errorEl) errorEl.classList.add("hidden"); - } - - amountChanged() { - if (this.hasAmountInputTarget) { - this.clearFieldError(this.amountInputTarget, this.hasAmountErrorTarget ? this.amountErrorTarget : null); - } - } - - refreshSubmitState() { - if (!this.hasFooterRightButtonTarget) return; - const anyChecked = this.linkedAccountCheckboxTargets.some((cb) => cb.checked); - this.footerRightButtonTarget.disabled = false; - this.footerRightButtonTarget.classList.toggle("opacity-50", !anyChecked && this.currentStep === 1); - } - - updateStepperState() { - if (this.hasStep1CircleTarget) { - this.step1CircleTarget.classList.toggle("bg-inverse", this.currentStep === 1); - this.step1CircleTarget.classList.toggle("text-inverse", this.currentStep === 1); - this.step1CircleTarget.classList.toggle("bg-success", this.currentStep > 1); - this.step1CircleTarget.classList.toggle("text-inverse", this.currentStep === 1); - if (this.currentStep > 1) { - this.step1CircleTarget.textContent = "✓"; - } else { - this.step1CircleTarget.textContent = "1"; - } - } - if (this.hasStep2CircleTarget) { - this.step2CircleTarget.classList.toggle("bg-inverse", this.currentStep === 2); - this.step2CircleTarget.classList.toggle("text-inverse", this.currentStep === 2); - this.step2CircleTarget.classList.toggle("border", this.currentStep < 2); - this.step2CircleTarget.classList.toggle("border-secondary", this.currentStep < 2); - this.step2CircleTarget.classList.toggle("text-secondary", this.currentStep < 2); - } - if (this.hasStepperLineTarget) { - this.stepperLineTarget.classList.toggle("border-inverse", this.currentStep > 1); - this.stepperLineTarget.classList.toggle("border-subdued", this.currentStep === 1); - } - // Modal subtitle lives in the dialog header, outside this controller's - // DOM scope. Walk up to the enclosing dialog first so two stepper - // instances on the same page (eg. edit modal opened over the new modal) - // each update their own header rather than the first match in the DOM. - const dialog = this.element.closest("dialog, [role='dialog']"); - const subtitle = (dialog || document).querySelector("[data-goal-stepper-modal-subtitle]"); - if (subtitle) { - subtitle.textContent = - this.currentStep === 1 ? this.step1SubtitleValue : this.step2SubtitleValue; - } - } - - updateFooter() { - if (this.hasFooterLeftButtonTarget) { - this.footerLeftButtonTarget.classList.toggle("hidden", this.currentStep === 1); - } - if (this.hasFooterRightButtonTarget) { - const labelSpan = this.footerRightButtonTarget.querySelector("span"); - if (labelSpan) { - labelSpan.textContent = - this.currentStep === 1 ? this.continueLabelValue : this.submitLabelValue; - } - } - this.refreshSubmitState(); - } - - updateReview() { - if (!this.hasReviewNameTarget) return; - - const name = this.element.querySelector('input[name="goal[name]"]')?.value || "…"; - const amountInput = this.element.querySelector('input[name="goal[target_amount]"]'); - const amount = amountInput?.value ? Number.parseFloat(amountInput.value) : 0; - const dateInput = this.element.querySelector('input[type="date"][name="goal[target_date]"]'); - const dateValue = dateInput?.value; - const checked = this.linkedAccountCheckboxTargets.filter((cb) => cb.checked); - - this.reviewNameTarget.textContent = name; - - if (this.hasReviewSummaryTarget) { - const formattedAmount = amount > 0 ? this.#money(amount) : "…"; - const template = dateValue ? this.summaryWithDateValue : this.summaryNoDateValue; - this.reviewSummaryTarget.textContent = template - .replace("{amount}", formattedAmount) - .replace("{date}", dateValue ? this.#formatDate(dateValue) : ""); - } - - if (this.hasReviewSuggestedTarget) { - const months = dateValue ? this.#monthsBetween(new Date(), new Date(dateValue)) : 0; - const accountLabel = checked.length === 1 - ? this.accountCountOneValue - : this.accountCountOtherValue.replace("{count}", checked.length.toString()); - - if (amount > 0 && months > 0 && checked.length > 0) { - const perMonth = Math.ceil(amount / months); - this.reviewSuggestedTarget.textContent = this.suggestedWithDateValue - .replace("{monthly}", this.#money(perMonth)) - .replace("{accounts}", accountLabel); - } else if (amount > 0 && checked.length > 0) { - this.reviewSuggestedTarget.textContent = this.suggestedNoDateValue; - } else { - this.reviewSuggestedTarget.textContent = "…"; - } - } - } - - #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()}`; - } - } - - #monthsBetween(from, to) { - return (to - from) / (1000 * 60 * 60 * 24 * 30.44); - } - - #formatDate(iso) { - try { - const d = new Date(iso); - return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); - } catch (e) { - return iso; - } - } -} diff --git a/app/views/goals/_color_picker.html.erb b/app/views/goals/_color_picker.html.erb index 4100d6d1a..beb74e73a 100644 --- a/app/views/goals/_color_picker.html.erb +++ b/app/views/goals/_color_picker.html.erb @@ -4,7 +4,7 @@ + data-goal-form-target="avatarPreview"> <% if form.object.icon.present? %> <%= icon(form.object.icon, color: "current", size: "md") %> <% elsif form.object.name.present? %> diff --git a/app/views/goals/_form.html.erb b/app/views/goals/_form.html.erb new file mode 100644 index 000000000..ac64c468e --- /dev/null +++ b/app/views/goals/_form.html.erb @@ -0,0 +1,89 @@ +<%# locals: (goal:, linkable_accounts:, currently_linked_account_ids: []) %> + +
" + data-goal-form-suggested-no-date-value="<%= t("goals.form.suggested_no_date") %>"> + <% if goal.errors[:base].any? %> + <%= render "shared/form_errors", model: goal %> + <% end %> + + <%= styled_form_with model: goal, + url: goal.persisted? ? goal_path(goal) : goals_path, + method: goal.persisted? ? :patch : :post, + class: "space-y-5" do |f| %> +
+ <%= render "color_picker", form: f, colors: Goal::COLORS, icons: Goal::ICONS %> +
+ +
+ <%= f.text_field :name, + placeholder: t("goals.form.fields.name_placeholder"), + autofocus: true, + required: true, + label: t("goals.form.fields.name"), + data: { goal_form_target: "nameInput", action: "input->goal-form#nameChanged" } %> + +
+ +
+
+ <%= f.money_field :target_amount, + label: t("goals.form.fields.target_amount"), + hide_currency: true, + required: true, + amount_data: { goal_form_target: "amountInput", action: "input->goal-form#suggestedChanged" } %> + +
+ <%= f.date_field :target_date, + label: t("goals.form.fields.target_date"), + data: { goal_form_target: "dateInput", action: "input->goal-form#suggestedChanged" } %> +
+ + + +
+
+ <%= t("goals.form.fields.funding_accounts") %> +

<%= t("goals.form.fields.funding_accounts_hint") %>

+
+
+ <% grouped = linkable_accounts.group_by { |a| a.subtype.to_s.presence || "other" } %> + <% grouped.each_with_index do |(subtype, accts), group_idx| %> +
<%= t("goals.form.subtypes.#{subtype}", default: subtype.titleize) %>
+
"> + <% accts.each_with_index do |account, idx| %> + + <% end %> +
+ <% end %> +
+ +
+ + <%= f.text_area :notes, + label: t("goals.form.fields.notes"), + rows: 2, + placeholder: t("goals.form.fields.notes_placeholder") %> + +
+ <%= f.submit goal.persisted? ? t("goals.form.save") : t("goals.form.create") %> +
+ <% end %> +
diff --git a/app/views/goals/_form_edit.html.erb b/app/views/goals/_form_edit.html.erb deleted file mode 100644 index 784c8e645..000000000 --- a/app/views/goals/_form_edit.html.erb +++ /dev/null @@ -1,64 +0,0 @@ -<%# locals: (goal:, linkable_accounts:, currently_linked_account_ids:) %> - -<% if goal.errors.any? %> - <%= render "shared/form_errors", model: goal %> -<% end %> - -<%= styled_form_with model: goal, - url: goal_path(goal), - method: :patch, - class: "space-y-3" do |f| %> -
- <%= render "color_picker", form: f, colors: Goal::COLORS, icons: Goal::ICONS %> -
- - <%= f.text_field :name, - label: t("goals.form_stepper.step1.fields.name"), - required: true, - autofocus: true %> - - <%= f.money_field :target_amount, - label: t("goals.form_stepper.step1.fields.target_amount"), - required: true %> - - <%= f.date_field :target_date, - label: t("goals.form_stepper.step1.fields.target_date") %> - -
-
- <%= t("goals.form_stepper.step1.fields.funding_accounts") %> -

<%= t("goals.form_stepper.step1.fields.funding_accounts_hint") %>

-
-
- <% grouped = linkable_accounts.group_by { |a| a.subtype.to_s.presence || "other" } %> - <% grouped.each_with_index do |(subtype, accts), group_idx| %> -
<%= t("goals.form_stepper.step1.subtypes.#{subtype}", default: subtype.titleize) %>
-
"> - <% accts.each_with_index do |account, idx| %> - - <% end %> -
- <% end %> -
-
- - <%= f.text_area :notes, - label: t("goals.form_stepper.step1.fields.notes"), - rows: 2 %> - -
- <%= f.submit t("goals.edit.save") %> -
-<% end %> diff --git a/app/views/goals/_form_stepper.html.erb b/app/views/goals/_form_stepper.html.erb deleted file mode 100644 index a9cb8481d..000000000 --- a/app/views/goals/_form_stepper.html.erb +++ /dev/null @@ -1,157 +0,0 @@ -<%# locals: (goal:, linkable_accounts:) %> - -
" - data-goal-stepper-summary-no-date-value="<%= t("goals.form_stepper.step2.review.summary_no_date") %>" - data-goal-stepper-account-count-one-value="<%= t("goals.form_stepper.step2.review.account_count.one") %>" - data-goal-stepper-account-count-other-value="<%= t("goals.form_stepper.step2.review.account_count.other") %>" - data-goal-stepper-suggested-with-date-value="<%= t("goals.form_stepper.step2.review.suggested_with_date") %>" - data-goal-stepper-suggested-no-date-value="<%= t("goals.form_stepper.step2.review.suggested_no_date") %>"> - <% if goal.errors[:base].any? %> - <%= render "shared/form_errors", model: goal %> - <% end %> - - <%# Connected stepper %> -
-
- 1 - <%= t("goals.form_stepper.step1.label") %> -
-
-
- 2 - <%= t("goals.form_stepper.step2.label") %> -
-
- - <%= styled_form_with model: goal, url: goals_path, class: "space-y-4", data: { action: "keydown.enter->goal-stepper#blockEnter" } do |f| %> -
-
-

<%= t("goals.form_stepper.step1.heading") %>

-

<%= t("goals.form_stepper.step1.subheading") %>

-
- -
- <%= render "color_picker", form: f, colors: Goal::COLORS, icons: Goal::ICONS %> -
- -
- <%= f.text_field :name, - placeholder: t("goals.form_stepper.step1.fields.name_placeholder"), - autofocus: true, - label: t("goals.form_stepper.step1.fields.name"), - data: { goal_stepper_target: "nameInput", action: "input->goal-stepper#nameChanged" } %> - -
- -
-
- <%= f.money_field :target_amount, - label: t("goals.form_stepper.step1.fields.target_amount"), - hide_currency: true, - amount_data: { goal_stepper_target: "amountInput", action: "input->goal-stepper#amountChanged" } %> - -
- <%= f.date_field :target_date, - label: t("goals.form_stepper.step1.fields.target_date") %> -
- -
-
- <%= t("goals.form_stepper.step1.fields.funding_accounts") %> -

<%= t("goals.form_stepper.step1.fields.funding_accounts_hint") %>

-
-
- <% grouped = linkable_accounts.group_by { |a| a.subtype.to_s.presence || "other" } %> - <% grouped.each_with_index do |(subtype, accts), group_idx| %> -
<%= t("goals.form_stepper.step1.subtypes.#{subtype}", default: subtype.titleize) %>
-
"> - <% accts.each_with_index do |account, idx| %> - - <% end %> -
- <% end %> -
- -
- - <%= render DS::Disclosure.new(title: t("goals.form_stepper.step1.fields.notes_summary"), align: "right") do %> - <%= f.text_area :notes, - label: t("goals.form_stepper.step1.fields.notes"), - rows: 3, - placeholder: t("goals.form_stepper.step1.fields.notes_placeholder") %> - <% end %> - -
- - - -
- -
- <%= render DS::Button.new( - text: t("goals.form_stepper.continue"), - variant: "primary", - icon: "arrow-right", - icon_position: :right, - data: { - goal_stepper_target: "footerRightButton", - action: "click->goal-stepper#footerRight" - } - ) %> -
- -
- <% end %> -
diff --git a/app/views/goals/edit.html.erb b/app/views/goals/edit.html.erb index 68d17ba25..de4628c7c 100644 --- a/app/views/goals/edit.html.erb +++ b/app/views/goals/edit.html.erb @@ -1,6 +1,6 @@ <%= render DS::Dialog.new do |dialog| %> <% dialog.with_header(title: t(".heading")) %> <% dialog.with_body do %> - <%= render "form_edit", goal: @goal, linkable_accounts: @linkable_accounts, currently_linked_account_ids: @currently_linked_account_ids %> + <%= render "form", goal: @goal, linkable_accounts: @linkable_accounts, currently_linked_account_ids: @currently_linked_account_ids %> <% end %> <% end %> diff --git a/app/views/goals/new.html.erb b/app/views/goals/new.html.erb index 0f673e668..fb4162f45 100644 --- a/app/views/goals/new.html.erb +++ b/app/views/goals/new.html.erb @@ -6,16 +6,14 @@ <%= render DS::FilledIcon.new(variant: :container, icon: "target", size: "md", rounded: true) %>

<%= t(".heading") %>

-

- <%= t(".step1_subtitle") %> -

+

<%= t(".subtitle") %>

<%= render DS::Button.new(variant: "icon", icon: "x", title: t("common.close"), aria_label: t("common.close"), data: { action: "DS--dialog#close" }) %> <% end %> <% dialog.with_body do %> - <%= render "form_stepper", goal: @goal, linkable_accounts: @linkable_accounts %> + <%= render "form", goal: @goal, linkable_accounts: @linkable_accounts %> <% end %> <% end %> <% else %> @@ -24,11 +22,9 @@ <%= render DS::FilledIcon.new(variant: :container, icon: "target", size: "md", rounded: true) %>

<%= t(".heading") %>

-

- <%= t(".step1_subtitle") %> -

+

<%= t(".subtitle") %>

- <%= render "form_stepper", goal: @goal, linkable_accounts: @linkable_accounts %> + <%= render "form", goal: @goal, linkable_accounts: @linkable_accounts %> <% end %> diff --git a/config/locales/views/goals/en.yml b/config/locales/views/goals/en.yml index 46ee5e1c9..031633272 100644 --- a/config/locales/views/goals/en.yml +++ b/config/locales/views/goals/en.yml @@ -65,8 +65,7 @@ en: completed: Completed new: heading: New goal - step1_subtitle: Step 1 of 2 · Goal details - step2_subtitle: Step 2 of 2 · Review & start + subtitle: Save toward something specific. edit: heading: Edit goal save: Save changes @@ -242,46 +241,29 @@ en: footer_last_days: one: Last pledge matched 1 day ago other: "Last pledge matched %{count} days ago" - form_stepper: - cancel: Cancel - continue: Continue - back: Back - submit: Create goal + form: + create: Create goal + save: Save changes + suggested_with_date: "Save {monthly}/mo across {accounts} to hit it on time." + suggested_no_date: Set a target date to project a finish line. errors: name_required: Give your goal a name. amount_required: Set a target above zero. accounts_required: Pick at least one funding account. - step1: - label: Goal details - heading: What are you saving for? - subheading: Give your goal a name and a target. You can change these later. - fields: - name: Name - name_placeholder: Emergency fund, House down payment… - target_amount: Target amount - target_date: Target date - color: Color - notes: Notes (optional) - notes_summary: Add notes (optional) - notes_placeholder: A reminder for future you… - funding_accounts: Funding accounts - funding_accounts_hint: This goal's balance is the balance of these accounts. - subtypes: - checking: Checking - savings: Savings - hsa: HSA - cd: CD - money_market: Money market - other: Other - step2: - label: Review & start - heading: Looks good? - subheading: Review your goal and confirm. - review: - summary_with_date: "{amount} by {date}" - summary_no_date: "{amount}" - account_count: - one: 1 account - other: "{count} accounts" - suggested_with_date: "Save {monthly}/mo across {accounts} to hit it on time." - suggested_no_date: Set a target date to project a finish line. + fields: + name: Name + name_placeholder: Emergency fund, House down payment… + target_amount: Target amount + target_date: Target date + color: Color + notes: Notes (optional) + notes_placeholder: A reminder for future you… + funding_accounts: Funding accounts + funding_accounts_hint: This goal's balance is the balance of these accounts. + subtypes: + checking: Checking + savings: Savings + hsa: HSA + cd: CD + money_market: Money market + other: Other