Couple daily can and spend limits via usual can size

Link daily caps through the user's chosen standard can size (250/355/473ml)
and built-in prices. Onboarding asks for size and can ceiling with derived
spend preview. Settings syncs cans and spend bidirectionally. Forecast lock
button sets both limits together.

Co-authored-by: nh9961 <hello@nedhalksworth.com>
This commit is contained in:
Cursor Agent
2026-07-01 14:45:45 +00:00
parent 7f56274e65
commit 7a461af9ee
7 changed files with 229 additions and 148 deletions
+16 -1
View File
@@ -1,4 +1,4 @@
import type { RedBullEntry } from "../types";
import type { BuiltInSize, RedBullEntry } from "../types";
export const CAFFEINE_PER_250ML = 80;
export const SUGAR_PER_250ML = 27;
@@ -8,6 +8,21 @@ export const STANDARD_CAN_VALUES = {
473: { pricePerCan: 2.85, caffeineMg: 151 },
} as const;
export const BUILT_IN_SIZES: BuiltInSize[] = [250, 355, 473];
export function priceForLimitSize(size: BuiltInSize): number {
return STANDARD_CAN_VALUES[size].pricePerCan;
}
export function spendLimitFromCans(cans: number, size: BuiltInSize): number {
return Math.round(cans * priceForLimitSize(size) * 100) / 100;
}
export function canLimitFromSpend(spend: number, size: BuiltInSize): number {
const raw = spend / priceForLimitSize(size);
return Math.round(raw * 4) / 4;
}
export function spendFor(entry: RedBullEntry) {
return entry.cans * entry.pricePerCan;
}
+10
View File
@@ -7,6 +7,9 @@ export const DEFAULT_LIMITS: UserLimits = {};
const PREFS_CAN_KEY = "dailyCanLimit";
const PREFS_SPEND_KEY = "dailySpendLimit";
const PREFS_STOP_KEY = "stopTime";
const PREFS_SIZE_KEY = "limitCanSizeMl";
const VALID_LIMIT_SIZES = new Set([250, 355, 473]);
export function parseUserLimits(prefs: Record<string, unknown> | null | undefined): UserLimits {
if (!prefs) return { ...DEFAULT_LIMITS };
@@ -16,9 +19,12 @@ export function parseUserLimits(prefs: Record<string, unknown> | null | undefine
const spendLimit = Number(prefs[PREFS_SPEND_KEY]);
const stopTime = typeof prefs[PREFS_STOP_KEY] === "string" ? prefs[PREFS_STOP_KEY] : undefined;
const sizeLimit = Number(prefs[PREFS_SIZE_KEY]);
if (Number.isFinite(canLimit) && canLimit > 0) limits.dailyCanLimit = canLimit;
if (Number.isFinite(spendLimit) && spendLimit >= 0) limits.dailySpendLimit = spendLimit;
if (stopTime && /^\d{2}:\d{2}$/.test(stopTime)) limits.stopTime = stopTime;
if (VALID_LIMIT_SIZES.has(sizeLimit)) limits.limitCanSizeMl = sizeLimit as 250 | 355 | 473;
return limits;
}
@@ -34,6 +40,9 @@ export function serializeUserLimits(limits: UserLimits): Record<string, unknown>
if (limits.stopTime) {
data[PREFS_STOP_KEY] = limits.stopTime;
}
if (limits.limitCanSizeMl != null && VALID_LIMIT_SIZES.has(limits.limitCanSizeMl)) {
data[PREFS_SIZE_KEY] = limits.limitCanSizeMl;
}
return data;
}
@@ -45,6 +54,7 @@ export function mergePrefsWithLimits(
delete next[PREFS_CAN_KEY];
delete next[PREFS_SPEND_KEY];
delete next[PREFS_STOP_KEY];
delete next[PREFS_SIZE_KEY];
return { ...next, ...serializeUserLimits(limits) };
}