Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update beeminder extension #17150

Merged
merged 8 commits into from
Mar 4, 2025
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
3 changes: 2 additions & 1 deletion extensions/beeminder/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
raycast-env.d.ts

# misc
.DS_Store
.DS_Store
.cursor*
5 changes: 5 additions & 0 deletions extensions/beeminder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Beeminder Changelog

## [Show how many days goals are above the red line] - 2025-02-18

- 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.
Expand Down
12 changes: 11 additions & 1 deletion extensions/beeminder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
18 changes: 18 additions & 0 deletions extensions/beeminder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider making label text match title for consistency ('Days Above Line' vs '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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider making label text match title for consistency ('Sort by Days Above the Line' vs '# of days above the red line')

"default": false
}
],
"scripts": {
Expand Down
129 changes: 97 additions & 32 deletions extensions/beeminder/src/beeminder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import {
Cache,
Icon,
Keyboard,
Color,
} from "@raycast/api";
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();
Expand Down Expand Up @@ -146,58 +147,129 @@ export default function Command() {
}

function GoalsList({ goalsData }: { goalsData: GoalResponse }) {
const { beeminderUsername, colorProgression } = getPreferenceValues<Preferences>();
const { beeminderUsername, colorProgression, showDaysAboveLine, sortByDaysAboveLine } =
getPreferenceValues<Preferences>();
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 getGoalColor = (value: number): Color => {
if (!Number.isFinite(value)) return Color.Purple;
if (colorProgression === "rainbow") {
if (safebuf < 1) return "🔴";
if (safebuf < 2) return "🟠";
if (safebuf < 3) return "🟡";
if (safebuf < 7) return "🟢";
if (safebuf < 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 (safebuf < 1) return "🔴";
if (safebuf < 2) return "🟠";
if (safebuf < 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 "🟣";
};

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 0;
})
: goals;
}, [goals, sortByDaysAboveLine]);

return (
<List isLoading={isLoading}>
{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);
let dueText = `${goalRate} ${goal.gunits} due in `;
if (goal.safebuf > 1) {
dueText += `${goal.safebuf} days`;
} else if (goal.safebuf === 1) {
dueText += `${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 (hours > 0) {
dueText += hours > 1 ? `${hours} hours` : `${hours} hour`;
dayAmount += `${hours}h`;
}
if (minutes > 0) {
dueText += minutes > 1 ? ` ${minutes} minutes` : ` ${minutes} minute`;
dayAmount += `${minutes}m`;
}
}

const hasDataForToday =
goal.last_datapoint && goal.last_datapoint.timestamp >= getCurrentDayStart();
Comment on lines 247 to 248
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: getCurrentDayStart() is recreated on every render - consider moving it outside the component or memoizing


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 (
<List.Item
key={goal.slug}
Expand All @@ -208,14 +280,7 @@ export default function Command() {
? { value: Icon.Checkmark, tooltip: "Data entered today" }
: undefined
}
accessories={[
{
text: dueText,
},
{
icon: goalIcon,
},
]}
accessories={accessories}
keywords={goal.title
.split(" ")
.map((word) => word.replace(/[^\w\s]/g, ""))
Expand Down
2 changes: 2 additions & 0 deletions extensions/beeminder/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export interface Preferences {
beeminderApiToken: string;
beeminderUsername: string;
colorProgression: string;
showDaysAboveLine: boolean;
sortByDaysAboveLine: boolean;
}

export interface DataPointFormValues {
Expand Down
Loading