-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Update beeminder extension #17150
Changes from all commits
b0abe93
a96c8e0
69e350a
10153ea
a34f176
04e7131
d7e88f0
cbc4c8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,4 +7,5 @@ | |
raycast-env.d.ts | ||
|
||
# misc | ||
.DS_Store | ||
.DS_Store | ||
.cursor* |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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": { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(); | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} | ||
|
@@ -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, "")) | ||
|
There was a problem hiding this comment.
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')