From b0abe936876a3194f79e63660dea4f401b36032b Mon Sep 17 00:00:00 2001 From: Michael Hanson <186724+mybuddymichael@users.noreply.github.com> Date: Tue, 18 Feb 2025 07:52:42 -0800 Subject: [PATCH 1/7] Update beeminder extension - Add option to sort goals by days above the line - Add setting to show days above the red line - Update CHANGELOG - Show the last data point in the placeholder --- extensions/beeminder/.gitignore | 3 +- extensions/beeminder/CHANGELOG.md | 5 +- extensions/beeminder/README.md | 12 ++- extensions/beeminder/package.json | 18 ++++ extensions/beeminder/src/beeminder.tsx | 115 +++++++++++++++++-------- extensions/beeminder/src/types.ts | 2 + 6 files changed, 115 insertions(+), 40 deletions(-) diff --git a/extensions/beeminder/.gitignore b/extensions/beeminder/.gitignore index a6187eaaf0e..7686eaf6b80 100644 --- a/extensions/beeminder/.gitignore +++ b/extensions/beeminder/.gitignore @@ -7,4 +7,5 @@ raycast-env.d.ts # misc -.DS_Store \ No newline at end of file +.DS_Store +.cursor* diff --git a/extensions/beeminder/CHANGELOG.md b/extensions/beeminder/CHANGELOG.md index 549870533c0..08a2ec6199a 100644 --- a/extensions/beeminder/CHANGELOG.md +++ b/extensions/beeminder/CHANGELOG.md @@ -1,8 +1,9 @@ # Beeminder Changelog -## [A better placeholder when entering data] - 2024-09-25 +## [Show how many days goals are above the red line] - 2025-02-18 -The data entry field will now show the most recent data point as its placeholder value. +- Adds a preference to show how many days goals are above the red line. +- Adds a preference to sort and color goals by how many days they are above the red line. ## [Better synchronization after submitting data] - 2024-08-23 diff --git a/extensions/beeminder/README.md b/extensions/beeminder/README.md index a83892320df..03026f65f7a 100644 --- a/extensions/beeminder/README.md +++ b/extensions/beeminder/README.md @@ -6,4 +6,14 @@ This extensions allows you to manage your Beeminder goals with Raycast 🐝 You'll need to get your Personal Auth Token for the Beeminder API, which you can get [here](https://www.beeminder.com/settings/account#account-permissions). -You'll also need to add your Beeminder username. +You'll also need to add your Beeminder username to the extension preferences. + +## Advanced usage + +### Show days above line + +If you check this option, the extension will show how many days the goal is above the red line. This can be useful to see how ahead you are on a goal, ignoring breaks. + +### Sort by days above line + +With this option, the extension will sort and color goals by how many days they are above the red line. diff --git a/extensions/beeminder/package.json b/extensions/beeminder/package.json index 7c949689168..7e308199002 100644 --- a/extensions/beeminder/package.json +++ b/extensions/beeminder/package.json @@ -72,6 +72,24 @@ "value": "rainbow" } ] + }, + { + "name": "showDaysAboveLine", + "title": "Days Above Line", + "description": "Show how many days the goal is above the red line.", + "type": "checkbox", + "required": false, + "label": "Show days above the red line", + "default": false + }, + { + "name": "sortByDaysAboveLine", + "title": "Sort by Days Above the Line", + "description": "Sort goals by how many days they are above the red line.", + "type": "checkbox", + "required": false, + "label": "Sort goals by # of days above the red line", + "default": false } ], "scripts": { diff --git a/extensions/beeminder/src/beeminder.tsx b/extensions/beeminder/src/beeminder.tsx index b931c6b95d1..95c2ac1a6a7 100644 --- a/extensions/beeminder/src/beeminder.tsx +++ b/extensions/beeminder/src/beeminder.tsx @@ -83,13 +83,7 @@ export default function Command() { } } - function DataPointForm({ - goalSlug, - lastDatapoint, - }: { - goalSlug: string; - lastDatapoint?: number; - }) { + function DataPointForm({ goalSlug }: { goalSlug: string }) { const { pop } = useNavigation(); const { handleSubmit, itemProps } = useForm({ async onSubmit(values) { @@ -133,11 +127,7 @@ export default function Command() { @@ -146,52 +136,110 @@ export default function Command() { } function GoalsList({ goalsData }: { goalsData: GoalResponse }) { - const { beeminderUsername, colorProgression } = getPreferenceValues(); + const { beeminderUsername, colorProgression, showDaysAboveLine, sortByDaysAboveLine } = + getPreferenceValues(); const goals = Array.isArray(goalsData) ? goalsData : undefined; const getCurrentDayStart = () => { return new Date().setHours(0, 0, 0, 0) / 1000; // Convert to Unix timestamp }; - const getGoalIcon = (safebuf: number) => { + const getDailyRate = (rate: number, runits: string) => { + switch (runits) { + case "y": + return rate / 365; + case "m": + return rate / 30; + case "w": + return rate / 7; + case "h": + return rate * 24; + case "d": + default: + return rate; + } + }; + + const getDaysAboveLine = (goal: Goal) => { + const dailyRate = getDailyRate(goal.rate, goal.runits); + return Math.floor(goal.delta / dailyRate + 1); + }; + + const getGoalIcon = (safebuf: number, daysAbove: number) => { + let value = sortByDaysAboveLine ? daysAbove : safebuf; + if (!Number.isFinite(value)) return "🟣"; if (colorProgression === "rainbow") { - if (safebuf < 1) return "🔴"; - if (safebuf < 2) return "🟠"; - if (safebuf < 3) return "🟡"; - if (safebuf < 7) return "🟢"; - if (safebuf < 14) return "🔵"; + if (value < 1) return "🔴"; + if (value < 2) return "🟠"; + if (value < 3) return "🟡"; + if (value < 7) return "🟢"; + if (value < 14) return "🔵"; return "🟣"; } else { - if (safebuf < 1) return "🔴"; - if (safebuf < 2) return "🟠"; - if (safebuf < 3) return "🔵"; + if (value < 1) return "🔴"; + if (value < 2) return "🟠"; + if (value < 3) return "🔵"; return "🟢"; } }; + // Sort goals by days above line if the preference is enabled + const sortedGoals = goals + ? [...goals].sort((a, b) => { + if (sortByDaysAboveLine) { + const aDaysAbove = getDaysAboveLine(a); + const bDaysAbove = getDaysAboveLine(b); + if (!Number.isFinite(aDaysAbove) && !Number.isFinite(bDaysAbove)) return 0; + if (!Number.isFinite(aDaysAbove) || !Number.isFinite(bDaysAbove)) { + return Number.isFinite(aDaysAbove) ? -1 : 1; // Place non-finite numbers at the end + } + return aDaysAbove - bDaysAbove; // Sort in ascending order + } + return 0; + }) + : goals; + return ( - {goals?.map((goal: Goal) => { + {sortedGoals?.map((goal: Goal) => { const diff = moment.unix(goal.losedate).diff(new Date()); const timeDiffDuration = moment.duration(diff); const goalRate = goal.baremin; - const goalIcon = getGoalIcon(goal.safebuf); + const goalIcon = getGoalIcon(goal.safebuf, getDaysAboveLine(goal)); let dueText = `${goalRate} ${goal.gunits} due in `; if (goal.safebuf > 1) { - dueText += `${goal.safebuf} days`; + dueText += showDaysAboveLine ? `${goal.safebuf}d` : `${goal.safebuf} days`; } else if (goal.safebuf === 1) { - dueText += `${goal.safebuf} day`; + dueText += showDaysAboveLine ? `${goal.safebuf}d` : `${goal.safebuf} day`; } if (goal.safebuf < 1) { const hours = timeDiffDuration.hours(); const minutes = timeDiffDuration.minutes(); - if (hours > 0) { - dueText += hours > 1 ? `${hours} hours` : `${hours} hour`; + + if (showDaysAboveLine) { + if (hours > 0) { + dueText += `${hours}h`; + } + if (minutes > 0) { + dueText += `${minutes}m`; + } + } else { + if (hours > 0) { + dueText += `${hours} ${hours > 1 ? "hours" : "hour"}`; + } + if (minutes > 0) { + if (hours > 0) dueText += " "; + dueText += `${minutes} ${minutes > 1 ? "minutes" : "minute"}`; + } } - if (minutes > 0) { - dueText += minutes > 1 ? ` ${minutes} minutes` : ` ${minutes} minute`; + } + + if (showDaysAboveLine) { + const daysAbove = getDaysAboveLine(goal); + if (Number.isFinite(daysAbove)) { + dueText += ` (${daysAbove}d above line)`; } } @@ -225,12 +273,7 @@ export default function Command() { - } + target={} /> Date: Tue, 18 Feb 2025 08:00:12 -0800 Subject: [PATCH 2/7] Use const instead of let --- extensions/beeminder/src/beeminder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/beeminder/src/beeminder.tsx b/extensions/beeminder/src/beeminder.tsx index 95c2ac1a6a7..2b0243a648c 100644 --- a/extensions/beeminder/src/beeminder.tsx +++ b/extensions/beeminder/src/beeminder.tsx @@ -166,7 +166,7 @@ export default function Command() { }; const getGoalIcon = (safebuf: number, daysAbove: number) => { - let value = sortByDaysAboveLine ? daysAbove : safebuf; + const value = sortByDaysAboveLine ? daysAbove : safebuf; if (!Number.isFinite(value)) return "🟣"; if (colorProgression === "rainbow") { if (value < 1) return "🔴"; From 69e350ad148afc8d0da9762619c98bb30e078800 Mon Sep 17 00:00:00 2001 From: Michael Hanson <186724+mybuddymichael@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:43:44 -0800 Subject: [PATCH 3/7] Update beeminder extension - Add the last datapoint back to the placeholder - Pull contributions - Initial commit --- extensions/beeminder/src/beeminder.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/extensions/beeminder/src/beeminder.tsx b/extensions/beeminder/src/beeminder.tsx index 2b0243a648c..c1ecd4decfb 100644 --- a/extensions/beeminder/src/beeminder.tsx +++ b/extensions/beeminder/src/beeminder.tsx @@ -83,7 +83,13 @@ export default function Command() { } } - function DataPointForm({ goalSlug }: { goalSlug: string }) { + function DataPointForm({ + goalSlug, + lastDatapoint, + }: { + goalSlug: string; + lastDatapoint?: number; + }) { const { pop } = useNavigation(); const { handleSubmit, itemProps } = useForm({ async onSubmit(values) { @@ -127,7 +133,11 @@ export default function Command() { @@ -273,7 +283,12 @@ export default function Command() { } + target={ + + } /> Date: Tue, 18 Feb 2025 09:50:29 -0800 Subject: [PATCH 4/7] Restore part of the previous changelog --- extensions/beeminder/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/beeminder/CHANGELOG.md b/extensions/beeminder/CHANGELOG.md index 08a2ec6199a..9406e2790dd 100644 --- a/extensions/beeminder/CHANGELOG.md +++ b/extensions/beeminder/CHANGELOG.md @@ -5,6 +5,10 @@ - Adds a preference to show how many days goals are above the red line. - Adds a preference to sort and color goals by how many days they are above the red line. +## [A better placeholder when entering data] - 2024-09-25 + +The data entry field will now show the most recent data point as its placeholder value. + ## [Better synchronization after submitting data] - 2024-08-23 Fixes an issue where the data would not be fully refreshed after submitting a data point. From a34f176b1e49c77a6d48d50753f302156baa7249 Mon Sep 17 00:00:00 2001 From: Michael Hanson <186724+mybuddymichael@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:00:32 -0800 Subject: [PATCH 5/7] Update beeminder extension - Update styling - Merge branch \'contributions/merge-1739913350312\' - Pull contributions - Merge branch \'contributions/merge-1739913314780\' - Pull contributions - Pull contributions - Initial commit --- extensions/beeminder/src/beeminder.tsx | 106 +++++++++++++------------ 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/extensions/beeminder/src/beeminder.tsx b/extensions/beeminder/src/beeminder.tsx index c1ecd4decfb..03ced882766 100644 --- a/extensions/beeminder/src/beeminder.tsx +++ b/extensions/beeminder/src/beeminder.tsx @@ -10,6 +10,7 @@ import { Cache, Icon, Keyboard, + Color, } from "@raycast/api"; import { useForm } from "@raycast/utils"; import moment from "moment"; @@ -175,24 +176,33 @@ export default function Command() { return Math.floor(goal.delta / dailyRate + 1); }; - const getGoalIcon = (safebuf: number, daysAbove: number) => { - const value = sortByDaysAboveLine ? daysAbove : safebuf; - if (!Number.isFinite(value)) return "🟣"; + const getGoalColor = (value: number): Color => { + if (!Number.isFinite(value)) return Color.Purple; if (colorProgression === "rainbow") { - if (value < 1) return "🔴"; - if (value < 2) return "🟠"; - if (value < 3) return "🟡"; - if (value < 7) return "🟢"; - if (value < 14) return "🔵"; - return "🟣"; + if (value < 1) return Color.Red; + if (value < 2) return Color.Orange; + if (value < 3) return Color.Yellow; + if (value < 7) return Color.Green; + if (value < 14) return Color.Blue; + return Color.Purple; } else { - if (value < 1) return "🔴"; - if (value < 2) return "🟠"; - if (value < 3) return "🔵"; - return "🟢"; + if (value < 1) return Color.Red; + if (value < 2) return Color.Orange; + if (value < 3) return Color.Blue; + return Color.Green; } }; + const getEmoji = (color: Color): string => { + if (color === Color.Purple) return "🟣"; + if (color === Color.Red) return "🔴"; + if (color === Color.Orange) return "🟠"; + if (color === Color.Yellow) return "🟡"; + if (color === Color.Green) return "🟢"; + if (color === Color.Blue) return "🔵"; + return "🟣"; + }; + // Sort goals by days above line if the preference is enabled const sortedGoals = goals ? [...goals].sort((a, b) => { @@ -215,47 +225,50 @@ export default function Command() { const diff = moment.unix(goal.losedate).diff(new Date()); const timeDiffDuration = moment.duration(diff); const goalRate = goal.baremin; - - const goalIcon = getGoalIcon(goal.safebuf, getDaysAboveLine(goal)); - let dueText = `${goalRate} ${goal.gunits} due in `; - if (goal.safebuf > 1) { - dueText += showDaysAboveLine ? `${goal.safebuf}d` : `${goal.safebuf} days`; - } else if (goal.safebuf === 1) { - dueText += showDaysAboveLine ? `${goal.safebuf}d` : `${goal.safebuf} day`; - } + const dueText = `${goalRate} ${goal.gunits} due`; + const daysAbove = getDaysAboveLine(goal); + const sortValue = sortByDaysAboveLine ? daysAbove : goal.safebuf; + const emoji = getEmoji(getGoalColor(sortValue)); + let dayAmount = `${goal.safebuf}d`; if (goal.safebuf < 1) { const hours = timeDiffDuration.hours(); const minutes = timeDiffDuration.minutes(); - if (showDaysAboveLine) { - if (hours > 0) { - dueText += `${hours}h`; - } - if (minutes > 0) { - dueText += `${minutes}m`; - } - } else { - if (hours > 0) { - dueText += `${hours} ${hours > 1 ? "hours" : "hour"}`; - } - if (minutes > 0) { - if (hours > 0) dueText += " "; - dueText += `${minutes} ${minutes > 1 ? "minutes" : "minute"}`; - } + if (hours > 0) { + dayAmount += `${hours}h`; } - } - - if (showDaysAboveLine) { - const daysAbove = getDaysAboveLine(goal); - if (Number.isFinite(daysAbove)) { - dueText += ` (${daysAbove}d above line)`; + if (minutes > 0) { + dayAmount += `${minutes}m`; } } const hasDataForToday = goal.last_datapoint && goal.last_datapoint.timestamp >= getCurrentDayStart(); + const accessories: List.Item.Accessory[] = [ + { + text: dueText, + }, + { + tag: { + value: dayAmount, + color: getGoalColor(goal.safebuf), + }, + }, + ]; + if (showDaysAboveLine && Number.isFinite(daysAbove)) { + accessories.push({ + tag: { + value: `${daysAbove}d delta`, + color: getGoalColor(daysAbove), + }, + }); + } + accessories.push({ + icon: emoji, + }); + return ( word.replace(/[^\w\s]/g, "")) From 04e71319b772e0ff084d316fe222ed8204f19d2a Mon Sep 17 00:00:00 2001 From: Michael Hanson <186724+mybuddymichael@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:10:36 -0800 Subject: [PATCH 6/7] Add useMemo for sortedGoals --- extensions/beeminder/src/beeminder.tsx | 32 ++++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/extensions/beeminder/src/beeminder.tsx b/extensions/beeminder/src/beeminder.tsx index 03ced882766..f4bbd8cc191 100644 --- a/extensions/beeminder/src/beeminder.tsx +++ b/extensions/beeminder/src/beeminder.tsx @@ -16,7 +16,7 @@ import { useForm } from "@raycast/utils"; import moment from "moment"; import { Goal, GoalResponse, DataPointFormValues, Preferences } from "./types"; import { fetchGoals, sendDatapoint } from "./api"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { useNavigation } from "@raycast/api"; const cache = new Cache(); @@ -203,21 +203,23 @@ export default function Command() { return "🟣"; }; - // Sort goals by days above line if the preference is enabled - const sortedGoals = goals - ? [...goals].sort((a, b) => { - if (sortByDaysAboveLine) { - const aDaysAbove = getDaysAboveLine(a); - const bDaysAbove = getDaysAboveLine(b); - if (!Number.isFinite(aDaysAbove) && !Number.isFinite(bDaysAbove)) return 0; - if (!Number.isFinite(aDaysAbove) || !Number.isFinite(bDaysAbove)) { - return Number.isFinite(aDaysAbove) ? -1 : 1; // Place non-finite numbers at the end + // Memoize sortedGoals so that it's only recalculated when `goals` or `sortByDaysAboveLine` changes + const sortedGoals = useMemo(() => { + return goals + ? [...goals].sort((a, b) => { + if (sortByDaysAboveLine) { + const aDaysAbove = getDaysAboveLine(a); + const bDaysAbove = getDaysAboveLine(b); + if (!Number.isFinite(aDaysAbove) && !Number.isFinite(bDaysAbove)) return 0; + if (!Number.isFinite(aDaysAbove) || !Number.isFinite(bDaysAbove)) { + return Number.isFinite(aDaysAbove) ? -1 : 1; + } + return aDaysAbove - bDaysAbove; } - return aDaysAbove - bDaysAbove; // Sort in ascending order - } - return 0; - }) - : goals; + return 0; + }) + : goals; + }, [goals, sortByDaysAboveLine]); return ( From d7e88f085c1bf20f86fd16228747ce562a943968 Mon Sep 17 00:00:00 2001 From: Michael Hanson <186724+mybuddymichael@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:03:56 -0800 Subject: [PATCH 7/7] Remove a comment --- extensions/beeminder/src/beeminder.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/beeminder/src/beeminder.tsx b/extensions/beeminder/src/beeminder.tsx index f4bbd8cc191..2526299a8a1 100644 --- a/extensions/beeminder/src/beeminder.tsx +++ b/extensions/beeminder/src/beeminder.tsx @@ -203,7 +203,6 @@ export default function Command() { return "🟣"; }; - // Memoize sortedGoals so that it's only recalculated when `goals` or `sortByDaysAboveLine` changes const sortedGoals = useMemo(() => { return goals ? [...goals].sort((a, b) => {