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)}
+
+ )}
+
+
+
+
+
+ {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;
+