diff --git a/src/components/Calculator.test.ts b/src/components/Calculator.test.ts
deleted file mode 100644
index 6c9365c..0000000
--- a/src/components/Calculator.test.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { formatUSD, formatPerc, computeEvenDollarAmounts } from "./Calculator";
-
-import { describe, expect, it } from "vitest";
-
-describe("formatUSD", () => {
- it("should format a number as USD with cents", () => {
- expect(formatUSD(1)).toBe("$1.00");
- expect(formatUSD(1.5)).toBe("$1.50");
- expect(formatUSD(1.505)).toBe("$1.51");
- });
-
- it("should format a number as USD without cents", () => {
- expect(formatUSD(1, false)).toBe("$1");
- expect(formatUSD(1.499, false)).toBe("$1");
- expect(formatUSD(1.505, false)).toBe("$2");
- });
-
- it("should format a number as USD with cents and commas", () => {
- expect(formatUSD(1000)).toBe("$1,000.00");
- expect(formatUSD(1000.5)).toBe("$1,000.50");
- expect(formatUSD(1000.505)).toBe("$1,000.51");
- });
-
- it("should format without a dollar sign if requested", () => {
- expect(formatUSD(1, true, false)).toBe("1.00");
- expect(formatUSD(1.5, true, false)).toBe("1.50");
- expect(formatUSD(1.505, true, false)).toBe("1.51");
- });
-});
-
-describe("formatPerc", () => {
- it("should format a number as a percentage", () => {
- expect(formatPerc(1)).toBe("100%");
- expect(formatPerc(1.5)).toBe("150%");
- expect(formatPerc(0.02)).toBe("2%");
- });
-});
-
-describe("computeEvenDollarAmounts", () => {
- it("Gives 100% to the only available allocation", () => {
- const percs = [1.0];
- const result = computeEvenDollarAmounts(percs, 50);
- expect(result).toEqual([50]);
- });
-
- it("Gives 50% to each of two allocations", () => {
- const percs = [0.5, 0.5];
- const result = computeEvenDollarAmounts(percs, 50);
- expect(result).toEqual([25, 25]);
- });
-
- it("Gives evenly to four allocations", () => {
- const percs = [0.4, 0.2, 0.2, 0.2];
- const result = computeEvenDollarAmounts(percs, 50);
- expect(result).toEqual([20, 10, 10, 10]);
- });
-
- it("Rounds total cents down", () => {
- const percs = [1.0];
- const result = computeEvenDollarAmounts(percs, 50.95);
- expect(result).toEqual([50]);
- });
-
- it("Adjusts larger categories down by $2 if the sum doesn't match the total", () => {
- const percs = [0.4, 0.2, 0.2, 0.2];
- const result = computeEvenDollarAmounts(percs, 23);
- expect(result).toEqual([8, 5, 5, 5]);
- });
-
- it("Adjusts larger categories down by $1 if the sum doesn't match the total", () => {
- const percs = [0.4, 0.2, 0.2, 0.2];
- const result = computeEvenDollarAmounts(percs, 24);
- expect(result).toEqual([9, 5, 5, 5]);
- });
-
- it("Evenly distributes across several categories", () => {
- const percs = [0.4, 0.2, 0.2, 0.2];
- const result = computeEvenDollarAmounts(percs, 25);
- expect(result).toEqual([10, 5, 5, 5]);
- });
-
- it("Adjusts larger categories up by $1 if the sum doesn't match the total", () => {
- const percs = [0.4, 0.2, 0.2, 0.2];
- const result = computeEvenDollarAmounts(percs, 26);
- expect(result).toEqual([11, 5, 5, 5]);
- });
-
- it("Adjusts larger categories up by $2 if the sum doesn't match the total", () => {
- const percs = [0.4, 0.2, 0.2, 0.2];
- const result = computeEvenDollarAmounts(percs, 27);
- expect(result).toEqual([12, 5, 5, 5]);
- });
-});
diff --git a/src/components/Calculator.tsx b/src/components/Calculator.tsx
deleted file mode 100644
index 76fbe91..0000000
--- a/src/components/Calculator.tsx
+++ /dev/null
@@ -1,398 +0,0 @@
-import { useEffect, useState, useCallback } from "react";
-import type { ReactNode, MouseEvent } from "react";
-
-import { P21, P28, Em28, H2 } from "./Typeography";
-
-/** Format dollars as dollars, with commas. */
-export const formatUSD = (
- usd: number,
- showCents: boolean = true,
- showDollarSign: boolean = true
-) => {
- const formatted = usd.toLocaleString("en-US", {
- style: "currency",
- currency: "USD",
- minimumFractionDigits: showCents ? 2 : 0,
- maximumFractionDigits: showCents ? 2 : 0,
- });
- return showDollarSign ? formatted : formatted.slice(1);
-};
-
-/** Format a percentage (from 0.0 to 1.0) in human-readable form. */
-export const formatPerc = (perc: number) => `${Math.round(perc * 100)}%`;
-
-/** A partial allocation of a total election-cycle donation. */
-interface Allocation {
- name: string;
- description: ReactNode;
- perc: number;
- url: (usd: number) => string;
-}
-
-/** The type of a distribution. */
-type Distribution = Allocation[];
-
-/** The suggested distribution of donations for the 2024 election cycle. */
-const DISTRIBUTION: Distribution = [
- {
- name: "Voter Turnout",
- description: (
- <>
-
- The most quantitatively measurable and cost-effective tactics for
- voter registration, persuasion, and turnout. These are 501(c)3
- tax-deductible donations.
-
-
- We’ve chosen the{" "}
-
- Movement Voter Project
- {" "}
- and{" "}
-
- Working America
-
- .
-
- >
- ),
- perc: 0.4,
- url: (usd: number) =>
- `https://secure.actblue.com/donate/voter-turnout-vs-trump?amount=${usd}`,
- },
- {
- name: "Community Specific",
- description: (
- <>
-
- These organizations build long-term relationships with specific
- communities and demographics. They are trusted messengers in critical
- battleground states.
-
-
- We’ve chosen{" "}
-
- Somos Votantes
-
- ,{" "}
-
- Black Leaders Organizing Communities
-
- , the{" "}
-
- New Georgia Project
-
- , and{" "}
-
- LUCHA Arizona
-
- .
-
- >
- ),
- perc: 0.2,
- url: (usd: number) =>
- `https://secure.actblue.com/donate/community-specific-vs-trump?amount=${usd}`,
- },
- {
- name: "Biden Campaign",
- description: (
-
- Donating to the Biden campaign is impactful because they know the most
- important voters to target with get-out-the-vote and advertising.
-
- ),
- perc: 0.2,
- url: (usd: number) =>
- `https://secure.actblue.com/donate/biden-vs-trump?amount=${usd}`,
- },
- {
- name: "Competitive House Races",
- description: (
- <>
-
- Good house candidates help turn out votes for Biden. Winning the house
- a good strategy in case Trump wins.
-
-
- We've chosen the 10 most impactful house races according to{" "}
-
- Swing Left
-
- .
-
- >
- ),
- perc: 0.2,
- url: (usd: number) =>
- `https://secure.actblue.com/donate/competitive-house-races-vs-trump?amount=${usd}`,
- },
-];
-
-/** React component that displays a single Allocation. */
-const AllocationComponent = ({
- allocation,
- usd,
- index,
-}: {
- allocation: Allocation;
- usd: number;
- index: number;
-}) => {
- const handleDonateClick = useCallback(
- (e: MouseEvent) => {
- e.preventDefault();
-
- window.gtag("event", "click_donate", {
- event_category: "donation",
- event_label: "Clicked a donation link",
- allocation: allocation.name,
- usd: usd,
- url: allocation.url(usd),
- });
-
- // ideally we'd wait a moment for the gtag event to fire, or even
- // use the `event_callback` parameter to wait for it to fire, but
- // given the constraints of making it work no matter the condition
- // of the gtag scripts, let's just do this instead for now?
-
- window.open(allocation.url(usd), "_blank");
- },
- [allocation, usd]
- );
-
- return (
-
-
- {index}.
-
-
-
-
- {allocation.name} — {formatPerc(allocation.perc)}
-
-
-
- {allocation.description}
-
-
-
-
- );
-};
-
-/** Compute a dollar-rounded amount for each allocation in a distribtion. */
-export const computeEvenDollarAmounts = (
- percs: number[],
- usd: number
-): number[] => {
- // Compute the *total* number of USD we want to distribute over
- // our allocations.
- const totalUSD = Math.floor(usd);
-
- // Assert that our percentages are ordered from greatest to least.
- // Also assert that they sum to 1.
- let lastPerc = 1;
- let sum = 0;
- for (const perc of percs) {
- if (perc > lastPerc) {
- throw new Error(
- "Allocation percentages must be in reverse order (greatest to least)"
- );
- }
- lastPerc = perc;
- sum += perc;
- }
- if (sum !== 1) {
- throw new Error("Allocation percentages must sum to 1");
- }
-
- // Distribute the USD over our allocations, rounding each allocation
- // to the nearest dollar.
- const evenDollarAmounts: number[] = [];
- let remainingUSD = totalUSD;
- for (const perc of percs) {
- const roundedUSD = Math.round(perc * totalUSD);
- evenDollarAmounts.push(roundedUSD);
- remainingUSD -= roundedUSD;
- }
-
- // Now, check to make sure the total dollar amount we've distributed
- // matches the total USD we wanted to distribute. If it's off, we need
- // to adjust the allocations. Start by adjusting the larger allocations
- // up/down by one dollar, then the smaller allocations, etc.
- let i = 0;
- while (remainingUSD !== 0) {
- const roundedUSD = evenDollarAmounts[i];
- const delta = remainingUSD > 0 ? 1 : -1;
- evenDollarAmounts[i] = roundedUSD + delta;
- remainingUSD -= delta;
- i++;
- }
-
- // Assert that we've distributed the correct amount of USD by explicitly
- // summing the rounded amounts.
- const actualUSD = evenDollarAmounts.reduce(
- (sum, roundedUSD) => sum + roundedUSD,
- 0
- );
- if (actualUSD !== totalUSD) {
- throw new Error(
- `Expected to distribute ${totalUSD} USD, but actually distributed ${actualUSD} USD`
- );
- }
-
- // Return the final dollar amounts.
- return evenDollarAmounts;
-};
-
-/** React component that displays a full Distribution. */
-const DistributionComponent = ({
- distribution,
- usd,
-}: {
- distribution: Distribution;
- usd: number;
-}) => {
- const dollarAmounts = computeEvenDollarAmounts(
- distribution.map((allocation) => allocation.perc),
- usd
- );
-
- return (
-
- {distribution.map((allocation, i) => (
-
- ))}
-
- );
-};
-
-/** Donation amount react box. */
-const DonationAmountBox = ({
- usd,
- setUSD,
-}: {
- usd: number;
- setUSD: (usd: number) => void;
-}) => {
- const [text, setText] = useState(formatUSD(usd, false, false));
-
- useEffect(() => {
- // Copy the text for us to process
- let decimalPlaces = 0;
-
- // is there a decimal point?
- const hasDecimal = text.indexOf(".") !== -1;
-
- // if there *is* a decimal point, make sure there are exactly two digits after it
- if (hasDecimal) {
- const parts = text.split(".");
- if (parts.length !== 2) {
- return;
- }
-
- decimalPlaces = parts[1].length;
- }
-
- // remove all non-numeric characters
- const cleaned = text.replace(/[^0-9]/g, "");
-
- // if it's empty, bail
- if (cleaned === "") {
- return;
- }
-
- // parse an integer
- const parsed = parseInt(cleaned);
-
- // if it's not a number, bail
- if (isNaN(parsed)) {
- return;
- }
-
- // convert to USD (using hasDecimal)
- const cents = (parsed / Math.pow(10, decimalPlaces)) * 100;
- const newUSD = cents / 100;
- setUSD(newUSD);
- }, [text]);
-
- return (
-
-
- Your donation amount
-
-
-
$
-
- setText(e.target.value)}
- className="text-right caret-sun bg-transparent pr-4 outline-none transition-colors duration-200 leading-10 focus:bg-dark w-[100%] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
- />
-
-
-
- );
-};
-
-/** React component that displays a calculator. */
-const Calculator = () => {
- const [usd, setUSD] = useState(5000);
- return (
-
-
Make your donation
-
Enter the amount you’d like to donate:
-
-
-
- );
-};
-
-/** Export the Calculator component. */
-export default Calculator;