Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
55 changes: 55 additions & 0 deletions app/assets/tailwind/allocation_slider.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@layer components {
.allocation-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 0.5rem;
border-radius: 9999px;
background: transparent;
cursor: pointer;
}

.allocation-slider::-webkit-slider-runnable-track {
height: 0.5rem;
border-radius: 9999px;
background: linear-gradient(
to right,
var(--slider-color) 0%,
var(--slider-color) var(--slider-value, 0%),
var(--color-line-soft) var(--slider-value, 0%),
var(--color-line-soft) 100%
);
}

.allocation-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: 1rem;
width: 1rem;
margin-top: -0.25rem;
border-radius: 9999px;
background: var(--slider-color);
border: 2px solid var(--color-surface);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
}

.allocation-slider::-moz-range-track {
height: 0.5rem;
border-radius: 9999px;
background: var(--color-line-soft);
}

.allocation-slider::-moz-range-progress {
height: 0.5rem;
border-radius: 9999px;
background: var(--slider-color);
}

.allocation-slider::-moz-range-thumb {
height: 1rem;
width: 1rem;
border: 2px solid var(--color-surface);
border-radius: 9999px;
background: var(--slider-color);
}
}
1 change: 1 addition & 0 deletions app/assets/tailwind/application.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "tailwindcss";
@import "./allocation_slider.css";

@theme {
--color-canvas: #F5F2ED;
Expand Down
32 changes: 32 additions & 0 deletions app/javascript/controllers/allocation_slider_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Controller } from "@hotwired/stimulus"

const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
})

export default class extends Controller {
static targets = ["input", "percent", "dollar", "perpetuity"]
static values = {
ongoingAmount: Number,
payoutRate: Number,
}

update() {
const percent = Number(this.inputTarget.value)
const dollar = Math.round((percent / 100) * this.ongoingAmountValue)
const perpetuity = Math.round(dollar * this.payoutRateValue)

this.inputTarget.style.setProperty("--slider-value", `${percent}%`)
this.percentTargets.forEach((el) => (el.textContent = `${percent}%`))
this.dollarTargets.forEach((el) => (el.textContent = currency.format(dollar)))
if (this.hasPerpetuityTarget) {
this.perpetuityTarget.textContent = currency.format(perpetuity)
}
}

save() {
this.inputTarget.form?.requestSubmit()
}
}
10 changes: 0 additions & 10 deletions app/javascript/controllers/range_controller.js

This file was deleted.

105 changes: 83 additions & 22 deletions app/views/scenarios/_allocation.html.erb
Original file line number Diff line number Diff line change
@@ -1,25 +1,86 @@
<%# locals: (allocation:, scenario:) %>
<div data-controller="dialog">
<div class="flex items-start justify-between gap-3 rounded-lg border border-line-soft bg-surface px-4 py-3 transition hover:shadow-sm">
<div class="min-w-0">
<p class="font-medium text-ink truncate"><%= allocation.display_label %></p>
<% if allocation.note.present? %>
<p class="mt-1 text-sm text-ink-soft"><%= allocation.note %></p>
<% end %>
</div>
<div class="flex items-center gap-3 shrink-0">
<span class="font-semibold text-ink">
<%= allocation.ongoing? ? "#{allocation.percentage}%" : number_to_currency(allocation.amount, precision: 0) %>
</span>
<button type="button" data-action="dialog#open"
class="text-sm text-ink-faint hover:text-accent cursor-pointer bg-transparent p-0 leading-none">
Edit
</button>
<%= button_to "✕", scenario_allocation_path(allocation.scenario, allocation), method: :delete,
class: "text-ink-faint hover:text-danger cursor-pointer bg-transparent p-0 leading-none",
data: { turbo_confirm: "Remove this allocation?" } %>
<%# locals: (allocation:, scenario:, allocation_counter: 0) %>
<% if allocation.ongoing? %>
<% color = allocation_chart_color(allocation_counter) %>
<div data-controller="dialog">
<div data-controller="allocation-slider"
data-allocation-slider-ongoing-amount-value="<%= scenario.ongoing_giving_amount %>"
data-allocation-slider-payout-rate-value="<%= Allocation::Ongoing::PERPETUITY_PAYOUT_RATE %>"
class="rounded-lg border border-line-soft bg-surface px-4 py-3 transition hover:shadow-sm">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="font-medium text-ink truncate"><%= allocation.display_label %></p>
<p class="mt-0.5 text-sm text-ink-soft truncate">
<%= allocation.note.presence || "No additional preferences" %>
</p>
</div>
<div class="flex items-start gap-3 shrink-0">
<div class="w-16 text-right leading-tight">
<span class="block font-semibold text-ink tabular-nums" data-allocation-slider-target="percent"><%= allocation.percentage %>%</span>
<span class="block text-sm text-ink-faint tabular-nums" data-allocation-slider-target="dollar"><%= number_to_currency(allocation.dollar_amount, precision: 0) %></span>
</div>
<button type="button" data-action="dialog#open" aria-label="Edit allocation"
class="text-ink-faint hover:text-accent cursor-pointer bg-transparent p-0 leading-none">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z" />
</svg>
</button>
<%= button_to scenario_allocation_path(allocation.scenario, allocation), method: :delete,
class: "text-ink-faint hover:text-danger cursor-pointer bg-transparent p-0 leading-none",
form_class: "leading-none", aria: { label: "Remove allocation" },
data: { turbo_confirm: "Remove this allocation?" } do %>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
<% end %>
</div>
</div>

<div class="mt-3 flex items-center gap-4">
<%= form_with url: scenario_allocation_path(scenario, allocation), method: :patch, class: "flex-1 min-w-0 leading-none" do %>
<%= range_field_tag "allocation[percentage]", allocation.percentage, min: 0, max: 100, step: 1,
style: "--slider-color: #{color}; --slider-value: #{allocation.percentage}%",
data: { allocation_slider_target: "input", action: "input->allocation-slider#update change->allocation-slider#save" },
class: "allocation-slider block" %>
<% end %>
<div class="flex items-baseline gap-3 shrink-0">
<span class="w-10 text-right font-semibold text-ink tabular-nums" data-allocation-slider-target="percent"><%= allocation.percentage %>%</span>
<span class="w-16 text-right text-ink-soft tabular-nums" data-allocation-slider-target="dollar"><%= number_to_currency(allocation.dollar_amount, precision: 0) %></span>
</div>
</div>

<p class="mt-3 text-xs text-ink-faint">
<span style="color: <%= color %>">&infin;</span>
Est. <span class="font-medium text-ink-soft" data-allocation-slider-target="perpetuity"><%= number_to_currency(allocation.perpetuity_annual_amount, precision: 0) %></span>
annually in perpetuity
<span class="text-ink-faint">(<%= (Allocation::Ongoing::PERPETUITY_PAYOUT_RATE * 100).to_i %>% of principal)</span>
</p>
</div>

<%= render "allocation_modal", allocation: allocation, scenario: scenario, color: color %>
</div>
<% else %>
<div data-controller="dialog">
<div class="flex items-start justify-between gap-3 rounded-lg border border-line-soft bg-surface px-4 py-3 transition hover:shadow-sm">
<div class="min-w-0">
<p class="font-medium text-ink truncate"><%= allocation.display_label %></p>
<% if allocation.note.present? %>
<p class="mt-1 text-sm text-ink-soft"><%= allocation.note %></p>
<% end %>
</div>
<div class="flex items-center gap-3 shrink-0">
<span class="font-semibold text-ink">
<%= number_to_currency(allocation.amount, precision: 0) %>
</span>
<button type="button" data-action="dialog#open"
class="text-sm text-ink-faint hover:text-accent cursor-pointer bg-transparent p-0 leading-none">
Edit
</button>
<%= button_to "✕", scenario_allocation_path(allocation.scenario, allocation), method: :delete,
class: "text-ink-faint hover:text-danger cursor-pointer bg-transparent p-0 leading-none",
data: { turbo_confirm: "Remove this allocation?" } %>
</div>
</div>

<%= render "allocation_modal", allocation: allocation, scenario: scenario %>
</div>
<%= render "allocation_modal", allocation: allocation, scenario: scenario %>
</div>
<% end %>
12 changes: 7 additions & 5 deletions app/views/scenarios/_allocation_modal.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<%# locals: (allocation:, scenario:) %>
<%# locals: (allocation:, scenario:, color: nil) %>
<% klass = allocation.class %>
<% editing = allocation.persisted? %>
<% suffix = editing ? dom_id(allocation) : klass.model_name.element %>
<% percentage = allocation.percentage || 20 %>
<% color ||= ScenariosHelper::CHART_COLORS.first %>
<dialog data-dialog-target="dialog" class="m-auto w-full max-w-lg rounded-2xl p-0 shadow-lg backdrop:bg-[rgba(20,18,14,0.48)]">
<%= form_with url: (editing ? scenario_allocation_path(scenario, allocation) : scenario_allocations_path(scenario)), method: (editing ? :patch : :post), class: "p-8" do |form| %>
<%= hidden_field_tag "allocation[type]", klass.name %>
Expand All @@ -11,15 +12,16 @@
<%= render "scenarios/category_picker", allocation: allocation, scenario: scenario %>

<% if klass == Allocation::Ongoing %>
<div class="mt-6" data-controller="range">
<div class="mt-6" data-controller="allocation-slider">
<div class="flex items-center justify-between">
<span class="text-sm text-ink-soft">Giving percentage %</span>
<span class="text-sm text-ink-faint">100 % reminder</span>
</div>
<p class="mt-1 text-lg font-medium text-ink"><span data-range-target="output"><%= percentage %></span> %</p>
<p class="mt-1 text-lg font-medium text-ink" data-allocation-slider-target="percent"><%= percentage %>%</p>
<%= range_field_tag "allocation[percentage]", percentage, min: 0, max: 100, step: 1,
data: { range_target: "input", action: "input->range#update" },
class: "mt-2 w-full accent-brand" %>
style: "--slider-color: #{color}; --slider-value: #{percentage}%",
data: { allocation_slider_target: "input", action: "input->allocation-slider#update" },
class: "allocation-slider block mt-2" %>
</div>
<% else %>
<div class="mt-6">
Expand Down
2 changes: 1 addition & 1 deletion app/views/scenarios/_allocation_section.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@
</div>
<% end %>

<%= render "allocation_modal", allocation: klass.new, scenario: scenario %>
<%= render "allocation_modal", allocation: klass.new, scenario: scenario, color: allocation_chart_color(allocations.size) %>
</div>
4 changes: 2 additions & 2 deletions app/views/scenarios/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<%= render "scenarios/total_giving_amounts/total_giving_amount", scenario: @scenario %>

<div class="mt-8 grid gap-6 lg:grid-cols-2">
<%= turbo_frame_tag dom_id(@scenario, :allocations), class: "mt-8 grid gap-6 lg:grid-cols-2" do %>
<!-- Allocations -->
<div class="rounded-2xl border border-line bg-surface p-6 shadow-sm">
<h2 class="font-serif font-medium text-xl text-ink">Allocations</h2>
Expand Down Expand Up @@ -129,5 +129,5 @@
</div>
<% end %>
</div>
</div>
<% end %>
</div>
Loading