Skip to content

Commit

Permalink
FIX: Everhour Extension (raycast#225)
Browse files Browse the repository at this point in the history
* feat(extensions): create everhour extension

* feat(everhour): render hours and min for task time

* chore: fix package.json typos

* feat(task-list): replace console log with error toast

* chore(package-json): improve title and command name

* feat(api): add get current user and time records api funcs

* feat(utils): create toast state resolver

* refactor(project-list): use time records for data flow

* refactor: use time records data

* refactor(time-submit-form): use dropdown

* Update extensions/everhour/package.json

* feat(api): add func to get tasks

* feat(task-list): fetch tasks and use to filter time records

* feat(task-list-item): use time records to get current day task time

* refactor(time-submit-form): use more readable dropdown items

* Update TimeSubmitForm.tsx

Co-authored-by: Thomas Paul Mann <hi@thomaspaulmann.com>
  • Loading branch information
2 people authored and FezVrasta committed Mar 23, 2022
1 parent 95c6107 commit 3a255e9
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 40 deletions.
21 changes: 21 additions & 0 deletions extensions/everhour/src/api/index.ts
Expand Up @@ -56,6 +56,27 @@ export const getProjects = async (): Promise<Project[]> => {
}));
};

export const getTasks = async (projectId: string): Promise<Task[]> => {
const response = await fetch(
`https://api.everhour.com/projects/${projectId}/tasks?page=1&limit=250&excludeClosed=true&query=`,
{
headers,
}
);
const tasks = (await response.json()) as any;

if (tasks.code) {
throw new Error(tasks.message);
}

return tasks.map(({ id, name, time }: TaskResp) => ({
id,
name,
timeInMin: time ? Math.round(time.total / 60) : 0,
}));
};


export const getCurrentTimer = async (): Promise<string | null> => {
const response = await fetch("https://api.everhour.com/timers/current", {
headers,
Expand Down
29 changes: 24 additions & 5 deletions extensions/everhour/src/components/TaskListItem.tsx
@@ -1,3 +1,4 @@
import React, { useState } from "react";
import { List, ActionPanel, PushAction, Icon, Color, showToast, ToastStyle } from "@raycast/api";
import { TimeSubmitForm } from "../views";
import { Task } from "../types";
Expand All @@ -9,12 +10,16 @@ export function TaskListItem({
hasActiveTimer,
refreshActiveTimer,
refreshRecords,
todaysTimeRecords,
}: {
task: Task;
hasActiveTimer: boolean;
refreshActiveTimer: () => Promise<void>;
refreshRecords: () => Promise<void>;
refreshRecords: () => Promise<any>;
todaysTimeRecords: Array<Array<Task>>;
}) {
const [timeRecords, setTimeRecords] = useState<Array<any>>(todaysTimeRecords);

const enableTaskTimer = async () => {
const toast = await showToast(ToastStyle.Animated, "Starting timer");
try {
Expand Down Expand Up @@ -42,7 +47,13 @@ export function TaskListItem({
};

const resolveTaskTime = (): string => {
const { timeInMin } = task;
const taskTimeToday = timeRecords.find((timeRecord) => timeRecord.id === task.id);

if (!taskTimeToday) {
return "0 min";
}

const { timeInMin } = taskTimeToday;
if (timeInMin >= 60) {
const hours = Math.floor(timeInMin / 60);
const min = timeInMin % 60;
Expand All @@ -56,14 +67,22 @@ export function TaskListItem({
<List.Item
id={task.id}
key={task.id}
title={`${task.name} - ${resolveTaskTime()}`}
title={`${task.name} - ${resolveTaskTime()} today`}
subtitle={hasActiveTimer ? "Timer Active" : ""}
icon={{ source: Icon.Dot, tintColor: Color.Green }}
actions={
<ActionPanel>
<PushAction
title="Submit Hours"
target={<TimeSubmitForm refreshRecords={refreshRecords} taskId={task.id} />}
title="Submit Custom Time"
target={
<TimeSubmitForm
refreshRecords={async () => {
const records = await refreshRecords();
setTimeRecords(records);
}}
taskId={task.id}
/>
}
/>
<ActionPanel.Item title="Start Timer" onAction={enableTaskTimer} />
<ActionPanel.Item title="Stop Active Timer" onAction={disableActiveTimer} />
Expand Down
48 changes: 40 additions & 8 deletions extensions/everhour/src/views/TaskList.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { List, Icon, showToast, ToastStyle } from "@raycast/api";
import { TaskListItem } from "../components";
import { getCurrentTimer } from "../api";
import { getCurrentTimer, getTasks } from "../api";
import { createResolvedToast } from "../utils";

const filterTasks = (records: Array<any>, projectId: string) => {
Expand All @@ -15,10 +15,11 @@ export function TaskList({
}: {
projectId: string;
timeRecords: Array<any>;
refreshRecords: () => Promise<Array<any>>;
refreshRecords: () => Promise<any[]>;
}) {
const [activeTimerTaskId, setActiveTimerTaskId] = useState<null | string>(null);
const [tasks, setTasks] = useState<Array<any>>(filterTasks(timeRecords, projectId));
const [tasks, setTasks] = useState<Array<any>>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);

const refreshActiveTimer = async () => {
const toast = await showToast(ToastStyle.Animated, "Refreshing tasks");
Expand All @@ -31,18 +32,43 @@ export function TaskList({
}
};

const fetchTasks = async () => {
const tasksResp = await getTasks(projectId);

setTasks(tasksResp);
};

useEffect(() => {
async function fetch() {
const toast = await showToast(ToastStyle.Animated, "Fetching tasks");
try {
await fetchTasks();
setIsLoading(false);
createResolvedToast(toast, "Tasks fetched").success();
} catch (error) {
const message = (error as { message: string }).message;
createResolvedToast(toast, message || "Failed to fetch projects").error();
setIsLoading(false);
}
}
fetch();
}, []);

useEffect(() => {
refreshActiveTimer();
}, [activeTimerTaskId]);

const todaysTimeRecords = filterTasks(timeRecords, projectId);

const renderTasks = () => {
if (tasks[0]) {
return tasks.map((task) => (
<TaskListItem
key={task.id}
refreshRecords={async () => {
const records = await refreshRecords();
setTasks(filterTasks(records, projectId));
todaysTimeRecords={todaysTimeRecords}
refreshRecords={() => {
fetchTasks();
return refreshRecords();
}}
refreshActiveTimer={refreshActiveTimer}
task={task}
Expand All @@ -51,8 +77,14 @@ export function TaskList({
));
}

return <List.Item title="No tasks found" icon={Icon.XmarkCircle} />;
if (!isLoading && tasks[0]) {
return <List.Item title="No tasks found" icon={Icon.XmarkCircle} />;
}
};

return <List searchBarPlaceholder="Filter tasks by name...">{renderTasks()}</List>;
return (
<List isLoading={isLoading} searchBarPlaceholder="Filter tasks by name...">
{renderTasks()}
</List>
);
}
49 changes: 22 additions & 27 deletions extensions/everhour/src/views/TimeSubmitForm.tsx
@@ -1,40 +1,35 @@
import { Form, ActionPanel, SubmitFormAction, showToast, ToastStyle, useNavigation } from "@raycast/api";
import { Form, ActionPanel, SubmitFormAction, showToast, ToastStyle } from "@raycast/api";
import { submitTaskHours } from "../api";
import { createResolvedToast } from "../utils";

const timeOptions = [
"0.10",
"0.15",
"0.25",
"0.5",
"1",
"1.5",
"2",
"2.5",
"3",
"3.5",
"4",
"4.5",
"5",
"5.5",
"6",
"6.5",
"7",
"7.5",
"8",
{ title: "15 min", value: "0.25" },
{ title: "30 min", value: "0.5" },
{ title: "45 min", value: "0.75" },
{ title: "1 hour", value: "1" },
{ title: "1 hour 30 min", value: "1.5" },
{ title: "2 hour", value: "2" },
{ title: "2 hour 30 min", value: "2.5" },
{ title: "3 hour", value: "3" },
{ title: "3 hour 30 min", value: "3.5" },
{ title: "4 hour", value: "4" },
{ title: "4 hour 30 min", value: "4.5" },
{ title: "5 hour", value: "5" },
{ title: "5 hour 30 min", value: "5.5" },
{ title: "6 hour", value: "6" },
{ title: "6 hour 30 min", value: "6.5" },
{ title: "7 hour", value: "7" },
{ title: "7 hour 30 min", value: "7.5" },
];

export function TimeSubmitForm({ taskId, refreshRecords }: { taskId: string; refreshRecords: () => Promise<void> }) {
const { pop } = useNavigation();

const handleSubmit = async ({ hours }: { hours: string }) => {
const toast = await showToast(ToastStyle.Animated, "Adding Time");
try {
const { taskName } = await submitTaskHours(taskId, hours);

if (taskName) {
await refreshRecords();
pop();
createResolvedToast(
toast,
`Added ${hours} ${parseInt(hours) === 1 ? "hour" : "hours"} to ${taskName}`
Expand All @@ -51,13 +46,13 @@ export function TimeSubmitForm({ taskId, refreshRecords }: { taskId: string; ref
<Form
actions={
<ActionPanel>
<SubmitFormAction title="Add Hours" onSubmit={handleSubmit} />
<SubmitFormAction title="Add Time" onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.Dropdown id="hours" title="Hours" defaultValue="0.25">
{timeOptions.map((option) => (
<Form.Dropdown.Item value={option} title={option} icon="⏱" />
<Form.Dropdown id="hours" title="Time Spent" defaultValue="0.25">
{timeOptions.map(({ value, title }) => (
<Form.Dropdown.Item key={value} value={value} title={title} icon="⏱" />
))}
</Form.Dropdown>
</Form>
Expand Down

0 comments on commit 3a255e9

Please sign in to comment.