From 57e7aa6b2b6138cb50314f2a63631d70caa995f0 Mon Sep 17 00:00:00 2001 From: Kevin <39208150+Kvrnn@users.noreply.github.com> Date: Sun, 7 Apr 2024 23:05:30 -0700 Subject: [PATCH] Todoist Implementation first implementation of Todoist Widget, only label works --- src/widgets/todoist/agenda.jsx | 46 ++++++++++++++++++ src/widgets/todoist/categories/filter.jsx | 0 src/widgets/todoist/categories/label.jsx | 41 ++++++++++++++++ src/widgets/todoist/categories/project.jsx | 10 ++++ src/widgets/todoist/component.jsx | 44 +++++++++++++++++ src/widgets/todoist/event.jsx | 56 ++++++++++++++++++++++ src/widgets/todoist/widget.js | 21 ++++++++ 7 files changed, 218 insertions(+) create mode 100644 src/widgets/todoist/agenda.jsx create mode 100644 src/widgets/todoist/categories/filter.jsx create mode 100644 src/widgets/todoist/categories/label.jsx create mode 100644 src/widgets/todoist/categories/project.jsx create mode 100644 src/widgets/todoist/component.jsx create mode 100644 src/widgets/todoist/event.jsx create mode 100644 src/widgets/todoist/widget.js diff --git a/src/widgets/todoist/agenda.jsx b/src/widgets/todoist/agenda.jsx new file mode 100644 index 000000000..cb59976fe --- /dev/null +++ b/src/widgets/todoist/agenda.jsx @@ -0,0 +1,46 @@ +import classNames from "classnames"; + +import Event from "./event"; + +const colorVariants = { + // https://tailwindcss.com/docs/content-configuration#dynamic-class-names + amber: "bg-amber-500", + blue: "bg-blue-500", + cyan: "bg-cyan-500", + emerald: "bg-emerald-500", + fuchsia: "bg-fuchsia-500", + gray: "bg-gray-500", + green: "bg-green-500", + indigo: "bg-indigo-500", + lime: "bg-lime-500", + neutral: "bg-neutral-500", + orange: "bg-orange-500", + pink: "bg-pink-500", + purple: "bg-purple-500", + red: "bg-red-500", + rose: "bg-rose-500", + sky: "bg-sky-500", + slate: "bg-slate-500", + stone: "bg-stone-500", + teal: "bg-teal-500", + violet: "bg-violet-500", + white: "bg-white-500", + yellow: "bg-yellow-500", + zinc: "bg-zinc-500", +}; + +export default function Agenda({ tasks }) { + return ( +
+
+ {tasks.map((task) => ( + + ))} +
+
+ ); +} diff --git a/src/widgets/todoist/categories/filter.jsx b/src/widgets/todoist/categories/filter.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/widgets/todoist/categories/label.jsx b/src/widgets/todoist/categories/label.jsx new file mode 100644 index 000000000..e2a4947ef --- /dev/null +++ b/src/widgets/todoist/categories/label.jsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "../../../utils/proxy/use-widget-api"; +import Error from "../../../components/services/widget/error"; +import Agenda from "../agenda"; + +export default function Label({ widget }) { + const { t } = useTranslation(); + const { data: tasksData, error: tasksError } = useWidgetAPI(widget, "getTasksWithLabel", { + refreshInterval: widget.refreshInterval || 300000, // 5 minutes, use default if not provided + label: widget.name + }); + + const [tasks, setEvents] = useState([]); // State to hold events + + useEffect(() => { + if (!tasksError && tasksData && tasksData.length > 0) { // Check if tasksData is not empty + // Process label data and set tasks + const tasksToAdd = tasksData.slice(0, widget.maxTasks || tasksData.length).map((task) => ({ + title: task.content || t("Untitled Task by Label"), + date: task.due ? new Date(task.due.date) : null, + color: task.color || "blue", // Adjust color based on your preference + description: task.tags ? task.tags.join(", ") : "", + url: task.url, + id: task.id, + })); + + // Update the events state + setEvents(tasksToAdd); + } + }, [tasksData, tasksError, widget, setEvents, t]); + + const error = tasksError ?? tasksData?.error; + if (error && !widget.hideErrors) { + return ; + } + + // Render the Agenda component if tasks is not empty + return ; +} diff --git a/src/widgets/todoist/categories/project.jsx b/src/widgets/todoist/categories/project.jsx new file mode 100644 index 000000000..b1e30b6fb --- /dev/null +++ b/src/widgets/todoist/categories/project.jsx @@ -0,0 +1,10 @@ +const groupTasksByProjectId = () => { + const groupedTasks = {}; + tasks.forEach((task) => { + if (!groupedTasks[task.project_id]) { + groupedTasks[task.project_id] = []; + } + groupedTasks[task.project_id].push(task); + }); + return Object.values(groupedTasks); +}; diff --git a/src/widgets/todoist/component.jsx b/src/widgets/todoist/component.jsx new file mode 100644 index 000000000..722739a86 --- /dev/null +++ b/src/widgets/todoist/component.jsx @@ -0,0 +1,44 @@ +import { useMemo } from "react"; +import dynamic from "next/dynamic"; + +import Container from "components/services/widget/container"; +import Block from "components/services/widget/block"; // Assuming this component renders the category name as a block-level element + +export default function Component({ service }) { + const { widget } = service; + + // Load categories + const categories = useMemo( + () => + widget.categories + ?.filter((category) => category?.sort) + .map((category) => ({ + service: dynamic(() => import(`./categories/${category.sort}`)), + widget: { ...widget, ...category }, + categoryName: category.category_name, // Add categoryName property + })) ?? [], + [widget] + ); + + return ( + +
+
+ {categories.map((category) => { + const Integration = category.service; + const key = `integration-${category.widget.type}`; + + return ( +
+ + +
+ ); + })} +
+
+
+ ); +} diff --git a/src/widgets/todoist/event.jsx b/src/widgets/todoist/event.jsx new file mode 100644 index 000000000..52208fcfd --- /dev/null +++ b/src/widgets/todoist/event.jsx @@ -0,0 +1,56 @@ +import { useState } from "react"; +import { useTranslation } from "next-i18next"; +import { DateTime } from "luxon"; +import classNames from "classnames"; +import { IoMdCheckmarkCircleOutline } from "react-icons/io"; + +export default function Event({ task, colorVariants }) { + const [hover, setHover] = useState(false); + const { i18n } = useTranslation(); // Ensure you're getting 't' from useTranslation() + + const renderEventTitle = () => { + if (task.url) { + return ( + + {hover && task.additional ? task.additional : task.title} + + ); + } + + return ( +
+ {hover && task.additional ? task.additional : task.title} +
+ ); + }; + + return ( +
setHover(true)} // Change to setHover(true) and setHover(false) + onMouseLeave={() => setHover(false)} // Change to setHover(false) + key={`task-${task.id}`} + > + + {task.date && ( + + {DateTime.fromJSDate(task.date) + .setLocale(i18n.language) + .toLocaleString(DateTime.TIME_24_SIMPLE)} + + )} + + + + +
+
{renderEventTitle()}
+
+ {task.isCompleted && ( + + + + )} +
+ ); +} diff --git a/src/widgets/todoist/widget.js b/src/widgets/todoist/widget.js new file mode 100644 index 000000000..c4e3f2986 --- /dev/null +++ b/src/widgets/todoist/widget.js @@ -0,0 +1,21 @@ +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "https://api.todoist.com/rest/v2/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + getAllActiveTasks: { + method: "GET", + endpoint: "tasks", + }, + getTasksWithLabel: { + method: "GET", + endpoint: "tasks", + params: ["label"] + }, + }, +}; + +export default widget; +