diff --git a/client/pages/api/github.ts b/client/pages/api/github.ts index 290f5ffa..715c1c41 100644 --- a/client/pages/api/github.ts +++ b/client/pages/api/github.ts @@ -9,7 +9,7 @@ export function useGithubHandler() { const serverCheck = async () => { try { - const response = await fetch("https://tidytime.onrender.com/health-check", {method: 'GET'}); + const response = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/health-check`, {method: 'GET'}); if (response.ok) { return true; } else { @@ -24,7 +24,7 @@ export function useGithubHandler() { try { const response = await serverCheck(); if (response) { - window.location.assign("https://tidytime.onrender.com/github/auth"); + window.location.assign(`${process.env.NEXT_PUBLIC_BACK_URL}/github/auth`); } else { toast.error('Server appears to be down'); } @@ -37,7 +37,7 @@ export function useGithubHandler() { try { const response = await serverCheck(); if (response) { - const userResponse = await fetch('https://tidytime.onrender.com/github/logout', { + const userResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/github/logout`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -61,7 +61,7 @@ export function useGithubHandler() { try { const response = await serverCheck(); if (response) { - const userResponse = await fetch('https://tidytime.onrender.com/github/user/data', { + const userResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/github/user/data`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -87,7 +87,7 @@ export function useGithubHandler() { try { const response = await serverCheck(); if (response) { - const issues = await fetch('https://tidytime.onrender.com/github/issues/get?user=' + user, { + const issues = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/github/issues/get?user=` + user, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -108,7 +108,7 @@ export function useGithubHandler() { try { const response = await serverCheck(); if (response) { - const issues = await fetch('https://tidytime.onrender.com/github/issues/close', { + const issues = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/github/issues/close`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -137,7 +137,7 @@ export function useGithubHandler() { try { const response = await serverCheck(); if (response) { - const issues = await fetch('https://tidytime.onrender.com/github/issues/open', { + const issues = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/github/issues/open`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -166,7 +166,7 @@ export function useGithubHandler() { try { const response = await serverCheck(); if (response) { - const issues = await fetch('https://tidytime.onrender.com/github/issues/update', { + const issues = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/github/issues/update`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/client/pages/api/google.ts b/client/pages/api/google.ts index bcefdc39..3aca8c88 100644 --- a/client/pages/api/google.ts +++ b/client/pages/api/google.ts @@ -9,7 +9,7 @@ export function useGoogleHandler() { const { events, setEvents } = useEventContext(); const serverCheck = () => { - return fetch("https://tidytime.onrender.com/health-check", { method: 'GET' }) + return fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/health-check`, { method: 'GET' }) .then(response => { if (response.ok) { return true; @@ -26,7 +26,7 @@ export function useGoogleHandler() { serverCheck() .then(response => { if (response) { - fetch('https://tidytime.onrender.com/google/auth/logout', { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/google/auth/logout`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -66,7 +66,7 @@ export function useGoogleHandler() { serverCheck() .then(response => { if (response) { - fetch('https://tidytime.onrender.com/google/auth/url', { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/google/auth/url`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -128,7 +128,7 @@ export function useGoogleHandler() { .then(response => { if (response) { return new Promise((resolve, reject) => { - fetch('https://tidytime.onrender.com/google/calendar/list', { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/google/calendar/list`, { method: 'GET', credentials: 'include', }) @@ -171,7 +171,7 @@ export function useGoogleHandler() { .then(response => { if (response) { return new Promise((resolve, reject) => { - fetch('https://tidytime.onrender.com/google/events/get', { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/google/events/get`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json'}, @@ -252,7 +252,7 @@ export function useGoogleHandler() { // format date to google format const ISOStartDate = new Date(new Date(eventToShare.start).getTime() - (new Date(eventToShare.start).getTimezoneOffset() * 60000)).toISOString(); const ISOEndDate = new Date(new Date(eventToShare.end).getTime() - (new Date(eventToShare.end).getTimezoneOffset() * 60000)).toISOString(); - fetch('https://tidytime.onrender.com/google/events/insert', { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/google/events/insert`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -311,7 +311,7 @@ export function useGoogleHandler() { // format date to google format const ISOStartDate = new Date(new Date(eventToUpdate.start).getTime() - (new Date(eventToUpdate.start).getTimezoneOffset() * 60000)).toISOString(); const ISOEndDate = new Date(new Date(eventToUpdate.end).getTime() - (new Date(eventToUpdate.end).getTimezoneOffset() * 60000)).toISOString(); - fetch('https://tidytime.onrender.com/google/events/update', { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/google/events/update`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -352,7 +352,7 @@ export function useGoogleHandler() { const isAuthenticatedUser = async (emailParam:string) => { try { - const fetchResponse = await fetch('https://tidytime.onrender.com/google/auth/email', { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/google/auth/email`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/client/pages/api/inrupt.ts b/client/pages/api/inrupt.ts index 1cbdc0eb..e19fbd17 100644 --- a/client/pages/api/inrupt.ts +++ b/client/pages/api/inrupt.ts @@ -18,7 +18,7 @@ export function useInruptHandler() { const serverCheck = async () => { try { - const response = await fetch("https://tidytime.onrender.com/health-check", {method: 'GET'}); + const response = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/health-check`, {method: 'GET'}); if (response.ok) { return true; } else { @@ -33,7 +33,7 @@ export function useInruptHandler() { serverCheck() .then(response => { if (response) { - window.location.assign("https://tidytime.onrender.com/solid/login"); + window.location.assign(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/login`); } else { toast.error('Server appears to be down'); } @@ -44,7 +44,7 @@ export function useInruptHandler() { try { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/user/session", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/user/session`, { method: 'GET', credentials: 'include', }); @@ -70,7 +70,7 @@ export function useInruptHandler() { serverCheck() .then(response => { if (response) { - fetch("https://tidytime.onrender.com/solid/logout", { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/logout`, { method: 'GET', credentials: 'include', }) @@ -91,7 +91,7 @@ export function useInruptHandler() { const getProfile = async () => { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/user/profile", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/user/profile`, { method: 'GET', credentials: 'include', }); @@ -113,7 +113,7 @@ export function useInruptHandler() { const checkConfiguration = async () => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/configuration/health-check", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/configuration/health-check`, { method: 'GET', credentials: 'include' }) @@ -130,7 +130,7 @@ export function useInruptHandler() { const getAllConfiguration = async () => { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/configuration", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/configuration`, { method: 'GET', credentials: 'include' }) @@ -156,7 +156,7 @@ export function useInruptHandler() { serverCheck() .then(response => { if (response) { - fetch("https://tidytime.onrender.com/solid/configuration/store", { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/configuration/store`, { method: 'POST', credentials: 'include', headers: { @@ -187,7 +187,7 @@ export function useInruptHandler() { const getCalendarConfiguration = async () => { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/configuration/calendar", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/configuration/calendar`, { method: 'GET', credentials: 'include' }); @@ -212,7 +212,7 @@ export function useInruptHandler() { const getTasks = async () => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/tasks/get", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/tasks/get`, { method: 'GET', credentials: 'include' }) @@ -256,7 +256,7 @@ export function useInruptHandler() { const getEvents = async () => { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/events/get", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/events/get`, { method: 'GET', credentials: 'include' }) @@ -285,7 +285,7 @@ export function useInruptHandler() { const getBoardColumns = async () => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/configuration/board", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/configuration/board`, { method: 'GET', credentials: 'include' }) @@ -305,7 +305,7 @@ export function useInruptHandler() { const getLabels = async () => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/labels/get", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/labels/get`, { method: 'GET', credentials: 'include' }) @@ -326,7 +326,7 @@ export function useInruptHandler() { const listNamesAndIds = listNames.map((listname, index) => { return {name: listname, id: tasks[index].key}; }) - fetch("https://tidytime.onrender.com/solid/data/store/listNames", { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/listNames`, { method: 'POST', credentials: 'include', headers: { @@ -349,7 +349,7 @@ export function useInruptHandler() { const listNamesAndIds = listNames.map((listname, index) => { return {name: listname, id: tasks[index].key}; }) - fetch("https://tidytime.onrender.com/solid/data/store/lists/delete", { + fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/lists/delete`, { method: 'POST', credentials: 'include', headers: { @@ -373,7 +373,7 @@ export function useInruptHandler() { try { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/get", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/get`, { method: 'GET', credentials: 'include', }); @@ -431,7 +431,7 @@ export function useInruptHandler() { const createTask = async (task:Task) => { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/tasks/create", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/tasks/create`, { method: 'POST', credentials: 'include', headers: { @@ -453,7 +453,7 @@ export function useInruptHandler() { const createEvent = async (event: Event) => { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/events/create", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/events/create`, { method: 'POST', credentials: 'include', headers: { @@ -475,7 +475,7 @@ export function useInruptHandler() { const updateTaskDoneUndone = async (task:Task) => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/task/done", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/task/done`, { method: 'POST', credentials: 'include', headers: { @@ -497,7 +497,7 @@ export function useInruptHandler() { const updateTask = async (task:Task) => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/tasks/update", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/tasks/update`, { method: 'POST', credentials: 'include', headers: { @@ -519,7 +519,7 @@ export function useInruptHandler() { const updateEvent = async (event: Event) => { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/events/update", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/events/update`, { method: 'POST', credentials: 'include', headers: { @@ -541,7 +541,7 @@ export function useInruptHandler() { const updateTaskStatus = async (task:Task) => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/task/status", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/task/status`, { method: 'POST', credentials: 'include', headers: { @@ -563,7 +563,7 @@ export function useInruptHandler() { const deleteTask = async (task:Task) => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/tasks/delete", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/tasks/delete`, { method: 'POST', credentials: 'include', headers: { @@ -585,7 +585,7 @@ export function useInruptHandler() { const deleteEvent = async (event: Event) => { const response = await serverCheck(); if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/events/delete", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/events/delete`, { method: 'POST', credentials: 'include', headers: { @@ -607,7 +607,7 @@ export function useInruptHandler() { const storeBoardColumns = async (boardColumns: string[]) => { const response = await serverCheck() if (response) { - const fetchResponse = await fetch("https://tidytime.onrender.com/solid/data/store/boardColumns", { + const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/data/store/boardColumns`, { method: 'POST', credentials: 'include', headers: { diff --git a/client/pages/tidier/index.tsx b/client/pages/tidier/index.tsx index fa31d54e..17bcd5c6 100644 --- a/client/pages/tidier/index.tsx +++ b/client/pages/tidier/index.tsx @@ -1,42 +1,139 @@ import { useSessionContext } from "@/src/components/Context/SolidContext"; import { useRouter } from "next/router"; import { useState, useEffect } from "react"; -import {} from "react" import { useInruptHandler } from "../api/inrupt"; import Loader from "@/src/components/Loading/Loading"; -import toast from "react-hot-toast"; +import { useTaskContext } from "@/src/components/Context/TaskContext"; +import { ScheduleItem, Task } from "@/src/model/Scheme"; +import { RxClock } from "react-icons/rx"; +import CheckableTaskList, { TasksPreview } from "@/src/components/List/CheckableTaskList"; +import { earliestDeadlineFirst, mostDifficultyFirst } from "../../src/algorithms/tidier"; +import { Icon } from "../../src/components/Icon/Icon"; +import { GiFlatPlatform } from "react-icons/gi"; +import { GrFormNext } from "react-icons/gr"; export default function Tidier() { const { solidSession } = useSessionContext(); - const { getSession } = useInruptHandler(); + const { getSession, getTasks } = useInruptHandler(); + const { tasks, listNames } = useTaskContext(); + const [reRender, setRerender] = useState(Math.random()); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); + const [selectedTasks, setSelectedTasks] = useState([]); + const [availableTime, setAvailableTime] = useState(''); + const [plan, setPlan] = useState([]); + const router = useRouter(); + const checkAuth = async () => { + if (solidSession === undefined) { + } else { + if (!solidSession?.info.isLoggedIn) { + router.push("/"); + } + } + }; + useEffect(() => { getSession(); - }, [reRender]); + }, [reRender]); - useEffect(() => { - if (solidSession === undefined) { - setLoading(true); + useEffect(() => { + checkAuth(); + fetchData(); + }, [solidSession]) + + const fetchData = async () => { + await getTasks(); + } + + const handleTaskCheck = (task: Task) => { + let index = selectedTasks.findIndex((selectedTask) => selectedTask.id === task.id); + if (index !== -1) { + setSelectedTasks(selectedTasks.filter((selectedTask) => selectedTask.id !== task.id)) } else { - if (!solidSession?.info.isLoggedIn) { - router.push("/"); - } + setSelectedTasks([...selectedTasks, task]) + } + } + + useEffect(() => { + if (tasks && listNames) { setLoading(false); - } - }, [solidSession]) + } + }, [tasks, listNames]) + + const handleFromHourChange = (event:any) => { + const { value } = event.target; + setAvailableTime(value); + }; + + const handleGeneratePlan = ({ variant = 'dueDate' } : {variant: 'dueDate' | 'difficulty'}) => { + const [hoursStr, minutesStr] = availableTime.split(':'); + const hours = parseInt(hoursStr, 10); + const minutes = parseInt(minutesStr, 10); + let result; + switch (variant) { + case 'dueDate': + result = earliestDeadlineFirst(selectedTasks, hours, minutes); + break; + case 'difficulty': + result = mostDifficultyFirst(selectedTasks, hours, minutes) + default: + break; + } + // @ts-ignore + setPlan(result); + } return ( loading ? :
- +
+
+

What shall get done today...

+
+ +
+

Available time:

+
+
+ + +
+
+
+ +
+
+
+ + +
+
+ { + plan.map((slot, index) => { + return ( +
+
+

{slot.hours} : {slot.minutes}

+ {slot.title} +
+
+ ) + }) + + } +
+
) } \ No newline at end of file diff --git a/client/pages/tidier/tidier.scss b/client/pages/tidier/tidier.scss index ab5fa05f..ac1d5f1b 100644 --- a/client/pages/tidier/tidier.scss +++ b/client/pages/tidier/tidier.scss @@ -1,9 +1,193 @@ -@use "../../styles/variables" as var; +@use "../../styles/variables" as *; .tidier-container { - width: var.$view-width; + width: $view-width; height: 100%; - padding: var.$view-padding; + padding: $view-padding; box-sizing: border-box; width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + overflow-y: auto; +} + +.tidier-header { + margin-left: 1rem; + grid-column: 1/-1; + grid-row: 1/1; + + @media screen and (min-width: 320px) and (max-width: 768px) { + margin-left: 0rem; + } + + h2 { + @media screen and (min-width: 320px) and (max-width: 445px) { + font-size: 1.2rem; + } + } +} + +.tidier-setup { + height: 100%; + display: grid; + grid-column: 1/1; + grid-template-rows: 3rem 3rem 35rem 3rem 15rem; + + @media screen and (min-width: 320px) and (max-width: 1023px) { + grid-column: 1/-1; + } +} + +.tidier-generate { + grid-row: 1/1; + display: flex; + flex-direction: row; + margin-left: 2.5rem; + justify-content: left; + align-items: center; + gap: 1.5rem; + + button { + display: flex; + justify-content: center; + gap: .4rem; + background-color: $primary-green; + box-shadow: 0px 0px 18px 0px rgba(0,0,0,0.37); + color: $white; + outline: none; + border: .1rem solid $primary-green; + border-radius: .4rem; + padding-inline: .4rem; + font-size: 1rem; + + @media screen and (min-width: 320px) and (max-width: 445px) { + font-size: .9rem; + } + } + + @media screen and (min-width: 320px) and (max-width: 1023px) { + width: 100%; + justify-content: center; + margin-left: 0rem; + } + + @media screen and (min-width: 320px) and (max-width: 445px) { + flex-direction: column; + gap: .9rem; + } +} + +.plan-result { + grid-row: 2/2; + max-height: 40rem; + overflow-y: auto; + margin-top: 2rem; + position: relative; + padding-left: 2.5rem; +} + +.tidier-plan { + padding-top: 5rem; + grid-column: 2/2; + width: 100%; + display: grid; + grid-template-rows: 3rem 1fr; + box-sizing: border-box; + height: 100%; + justify-content: center; + + @media screen and (min-width: 320px) and (max-width: 1023px) { + grid-column: 1/-1; + grid-template-columns: 1fr; + padding-top: .3rem; + justify-content: left; + margin-bottom: 2rem; + justify-items: center; + } + + @media screen and (min-width: 320px) and (max-width: 445px) { + font-size: .9rem; + grid-template-rows: 6rem 1fr; + } +} + +.plan-item { + position: relative; + border-bottom: .1rem solid $primary-grey; + margin-left: 2rem; + padding: .5rem 0rem; + max-width: 25rem; + + p { + display: flex; + align-items: center; + gap: .3rem; + } +} + +.line { + position: absolute; + width: .3rem; /* Grosor de la línea */ + background: #A1B89D; /* Degradado de color */ + top: 0; + bottom: 0; + left: 0; /* Centra la línea en la columna */ + transform: translateX(-50%); /* Ajusta para centrar la línea */ + margin-left: -2rem; +} + +.circle { + position: absolute; + width: 1rem; + height: 1rem; + background-color: #A1B89D; /* Color del círculo */ + border-radius: 50%; + left: -2.45rem; /* Ajusta para centrar el círculo en la línea */ + top: 50%; + transform: translateY(-50%); +} + +.available-time { + grid-column: 1/1; + grid-row: 4/4; + margin-left: 2rem; + margin-top: 1rem; + justify-content: left; + display: flex; + gap: 1rem; + width: 100%; + + .from-hour { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + width: 6rem; + grid-row: 2/2; + } + + .input-container { + position: relative; + display: flex; + align-items: center; + } + + .input-container input { + width: 100%; + padding-right: 2rem; /* Espacio para el ícono */ + box-sizing: border-box; + } + + .input-container .clock-from { + position: absolute; + right: 0.5rem; + pointer-events: none; /* Asegura que el ícono no bloquee la interacción con el input */ + } + + input[type="time"]::-webkit-calendar-picker-indicator { + display: none; + } + + @media screen and (min-width: 320px) and (max-width: 768px) { + margin-left: 0rem; + } } \ No newline at end of file diff --git a/client/src/algorithms/tidier.ts b/client/src/algorithms/tidier.ts new file mode 100644 index 00000000..3de8ad44 --- /dev/null +++ b/client/src/algorithms/tidier.ts @@ -0,0 +1,169 @@ +import { Task, ScheduleItem } from "../model/Scheme"; + +/** + * Calculates a task priority based on the task difficulty. + * @param task Task to be prioritized. + * @returns Task priority as a number. + */ +export function calculatePriority(task: Task): number { + const today = new Date(); + const dueDate = task.endDate ? new Date(task.endDate) : undefined; + let daysUntilDueDate: number; + + if (dueDate) { + const timeDifference = dueDate.getTime() - today.getTime(); + daysUntilDueDate = Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); + if (daysUntilDueDate < 0) { + // High priority to past due dates + daysUntilDueDate = 0.01; + } else { + // Avoid division by 0 + daysUntilDueDate = Math.max(1, daysUntilDueDate); + } + } else { + // If it does not have a due date, the priority is low + daysUntilDueDate = 1; + } + // Default difficulty if not present + const difficulty = task.difficulty !== undefined ? task.difficulty : 1; + + return difficulty; + +} + +/** + * Formats a number for it to have at least two digits. + * @param number Number to format. + * @returns Formatted number as string. + */ +function formatNumber(number: number): string { + return number < 10 ? `0${number}` : `${number}`; +} + +/** + * Convert time in hours and minutes to total minutes. + * @param hours Number of hours. + * @param minutes Number of minutes. + * @returns Total minutes. + */ +function convertTimeToMinutes(hours: number, minutes: number): number { + return hours * 60 + minutes; +} + +/** + * Adds a task block to the plan + * @param plan Current task plan. + * @param task Task to be added. + * @param minutes Task's duration in minutes. + */ +function addTaskBlock(plan: ScheduleItem[], task: Task, minutes: number): void { + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + plan.push({ title: task.title, hours: formatNumber(hours), minutes: formatNumber(remainingMinutes) }); +} + +/** + * Adds a break to the plan. + * @param plan Current task plan. + * @param breakTitle Break title. + * @param breakMinutes Brake's duration in minutes. + */ +function addBreak(plan: ScheduleItem[], breakTitle: string, breakMinutes: number): void { + plan.push({ title: breakTitle, hours: '00', minutes: formatNumber(breakMinutes) }); +} + +/** + * Organizes the tasks in a plan prioritizing their difficulty. + * @param tasks Task list to plan. + * @param availableHours Available hours. + * @param availableMinutes Available minutes. + * @returns Task plan. + */ +export function mostDifficultyFirst(tasks: Task[], availableHours: number, availableMinutes: number): ScheduleItem[] { + let availableTime = convertTimeToMinutes(availableHours, availableMinutes); + const sortedTasks = tasks.sort((a, b) => calculatePriority(b) - calculatePriority(a)); + const plan: ScheduleItem[] = []; + const shortBreak = 5; + const longBreak = 15; + + sortedTasks.forEach(currentTask => { + // There's no more time available + if (availableTime <= 0) return; + const difficulty = currentTask.difficulty ?? 1; + let durationMinutes = Math.min(difficulty * 30, availableTime); + // While there's time still, divide long tasks in 60 minutes blocks + while (durationMinutes > 60 && availableTime > 0) { + const blockMinutes = Math.min(60, durationMinutes); + addTaskBlock(plan, currentTask, blockMinutes); + availableTime -= blockMinutes; + durationMinutes -= blockMinutes; + + if (availableTime > shortBreak) { + addBreak(plan, 'Short break', shortBreak); + availableTime -= shortBreak; + } + } + // Add tasks whose duration is less than 60 minutes + if (durationMinutes > 0 && availableTime > 0) { + addTaskBlock(plan, currentTask, durationMinutes); + availableTime -= durationMinutes; + // if there's enough time to make a break and do at least 10 minutes of work + if (availableTime >= longBreak + 10) { + // Add a long break after long tasks and a short break after short ones + if (durationMinutes >= 55) { + addBreak(plan, 'Long break', longBreak); + availableTime -= longBreak; + } else if (durationMinutes >= 30) { + addBreak(plan, 'Short break', shortBreak); + availableTime -= shortBreak; + } + } + } + }); + + return plan; +} + +export function earliestDeadlineFirst(tasks: Task[], availableHours: number, availableMinutes: number): ScheduleItem[] { + // Ordena las tareas por fecha límite, poniendo las tareas sin fecha límite al final + tasks.sort((task1, task2) => { + if (task1.endDate && task2.endDate) { + return new Date(task1.endDate).getTime() - new Date(task2.endDate).getTime(); + } else if (task1.endDate) { + return -1; // Las tareas con fecha van primero + } else if (task2.endDate) { + return 1; // Las tareas sin fecha van después + } else { + return 0; // Ambas tareas sin fecha se consideran iguales + } + }); + + let schedule: ScheduleItem[] = []; + const breakTime = 5; + let currentTime = 0; + let availableTime = convertTimeToMinutes(availableHours, availableMinutes); + + for (let task of tasks) { + // Si agregar la tarea actual excede el tiempo disponible, termina la planificación + const difficulty = task.difficulty && task.difficulty !== 0 ? task.difficulty : 1; + let durationMinutes = Math.min(difficulty * 20, availableTime); + if (currentTime + durationMinutes > availableTime) { + let time = availableTime - currentTime; + addTaskBlock(schedule, task, time); + break; + } + + // Agrega la tarea al horario + addTaskBlock(schedule, task, durationMinutes); + currentTime += durationMinutes; + + // Si agregar un descanso excede el tiempo disponible o si esta es la última tarea, no agrega un descanso + if (currentTime + breakTime >= availableTime || task === tasks[tasks.length - 1]) continue; + + // Agrega un descanso al horario + addBreak(schedule, 'Break', breakTime); + currentTime += breakTime; + } + + return schedule; +} \ No newline at end of file diff --git a/client/src/components/ComboBox/ComboBox.tsx b/client/src/components/ComboBox/ComboBox.tsx index 4c706569..a0095600 100644 --- a/client/src/components/ComboBox/ComboBox.tsx +++ b/client/src/components/ComboBox/ComboBox.tsx @@ -45,20 +45,6 @@ export default function ComboBox({text, options, colors, checkedOption, onOption ))} )} - {/* { - // come here if a combobox without colors as options is needed - isOpen && options && ( // content combobox -
- {options.map((option, index) => ( - - ))} -
- )} */} ) } \ No newline at end of file diff --git a/client/src/components/DifficultyRate/Difficulty.tsx b/client/src/components/DifficultyRate/Difficulty.tsx index 3db04859..f3997190 100644 --- a/client/src/components/DifficultyRate/Difficulty.tsx +++ b/client/src/components/DifficultyRate/Difficulty.tsx @@ -2,9 +2,10 @@ import { VscCircleLargeFilled } from "react-icons/vsc" export interface Props { difficulty: number, + color?: string } -export default function Difficulty({difficulty} : Props) { +export default function Difficulty({difficulty, color} : Props) { return ( <> @@ -12,7 +13,7 @@ export default function Difficulty({difficulty} : Props) { ? [...Array(difficulty)].map((point, index) => { return ( // eslint-disable-next-line react/jsx-key - + ) }) : <> diff --git a/client/src/components/List/CheckableTaskList.scss b/client/src/components/List/CheckableTaskList.scss new file mode 100644 index 00000000..6df694e1 --- /dev/null +++ b/client/src/components/List/CheckableTaskList.scss @@ -0,0 +1,219 @@ +@use "../../../styles/variables" as *; + +.get-done-tasks { + grid-column: 1/1; + grid-row: 2/2; + display: flex; + flex-direction: row; + gap: 1rem; + margin-top: 1rem; + margin-left: 2rem; + position: relative; + + @media screen and (min-width: 320px) and (max-width: 768px) { + margin-left: 0rem; + } + + p.get-done-p { + @media screen and (min-width: 320px) and (max-width: 445px) { + display: none; + } + } +} + +.list-combobox, .list-combobox-open { + width: 12rem; + padding-inline: .4rem; + border-radius: .4rem; + background-color: $medium-grey; + color: $white; + cursor: pointer; + + div { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + gap: .3rem; + + p { + width: 10rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + @media screen and (min-width: 320px) and (max-width: 445px) { + font-size: .9rem; + } + } +} + +.list-drop-down-hidden { + display: none; +} + +.list-drop-down-visible { + position: absolute; + top: 108%; + display: flex; + flex-direction: column; + background-color: $white; + border: .1rem solid $primary-green; + border-radius: .4rem; + gap: .1rem; + padding-top: .1rem; + padding-bottom: .1rem; + + label { + &:hover { + background-color: $primary-grey; + } + } + + label, label.selected-list { + border-radius: .4rem; + padding: 0 .4rem; + &:hover { + cursor: pointer; + } + } + + label.selected-list { + background-color: $hover-green; + } + + @media screen and (min-width: 320px) and (max-width: 445px) { + font-size: .8rem; + } + +} + +.selected-list-tasks, .selected-list-tasks-empty { + grid-column: 1/1; + grid-row: 3/3; + margin-left: 2rem; + margin-top: 1rem; + border: .1rem solid $primary-grey; + border-radius: .3rem; + background-color: $white; + display: flex; + flex-direction: column; + max-height: 35rem; + height: 30rem; + overflow-y: scroll; + + @media screen and (min-width: 320px) and (max-width: 768px) { + margin-left: 0rem; + } + + @media screen and (min-width: 320px) and (max-width: 445px) { + font-size: .9rem; + } +} + +.selected-list-tasks-empty { + justify-content: center; // necessary when there are no tasks to show + align-items: center; + box-shadow: inset 0px 0px 22px rgba(0,0,0,0.20); +} + +.selected-list-tasks-item, .selected-list-tasks-item-selected { + width: calc(100% - 4rem); + box-shadow: inset 0px 0px 7px -4px $primary-green; + border-bottom: .1rem solid $white; + border-radius: .2rem; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + padding: .5rem 2rem; + background-color: $white; + cursor: pointer; + height: 3rem; + padding-top: .5rem; + padding-bottom: .5rem; + + .due-date { + grid-row: 1/1; + grid-column: 2/-1; + display: flex; + justify-content: right; + font-size: .9rem; + color: #b13838; + } + + .difficulty-assigned { + grid-row: 1/1; + grid-column: 1/1; + display: flex; + justify-content: left; + align-items: center; + } + + .task-title { + grid-row: 2/2; + grid-column: 1/-1; + display: flex; + justify-content: center; + align-items: center; + } +} + +.selected-list-tasks-item:last-child { + border-bottom: 0rem solid $light-grey; +} + +.selected-list-tasks-item-selected { + background-color: $hover-green; +} + +.tasks-preview { + grid-column: 1/1; + grid-row: 5/-1; + display: flex; + flex-direction: row; + flex-wrap: wrap; + min-height: 4rem; + max-height: 15rem; + align-content: baseline; + justify-content: center; + overflow-y: auto; + margin-left: 2rem; + margin-top: 2rem; + gap: .5rem; + + .task-container { + height: fit-content; + max-width: 17rem; + width: auto; + background-color: $hover-green; + color: $black; + border-radius: .6rem; + display: flex; + padding-inline: .4rem; + gap: .2rem; + align-items: center; + row-gap: .2rem; + + p { + max-width: 15rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .task-delete { + &:hover { + cursor: pointer; + } + } + } + + @media screen and (min-width: 320px) and (max-width: 768px) { + margin-left: 0rem; + } + + @media screen and (min-width: 320px) and (max-width: 445px) { + font-size: .9rem; + } +} \ No newline at end of file diff --git a/client/src/components/List/CheckableTaskList.tsx b/client/src/components/List/CheckableTaskList.tsx new file mode 100644 index 00000000..f2da2e4b --- /dev/null +++ b/client/src/components/List/CheckableTaskList.tsx @@ -0,0 +1,135 @@ +import { useEffect, useState } from "react"; +import { useTaskContext } from "@/src/components/Context/TaskContext"; +import { Task } from "@/src/model/Scheme"; +import Difficulty from "../../../src/components/DifficultyRate/Difficulty"; +import { IoIosArrowDropdownCircle, IoMdClose } from "react-icons/io"; + +export interface Props { + selectedTasks: Task[], + handleTaskCheck: (task:Task) => void +} + + +export default function CheckableTaskList({ selectedTasks, handleTaskCheck} : Props) { + + const { tasks, listNames } = useTaskContext(); + + const [flatTasks, setFlatTasks] = useState([]); + const [isOpenLists, setOpenLists] = useState(false); + const [selectedListIndex, setSelectedListIndex] = useState(-1); + + const handleListSelection = (index:number) => { + setSelectedListIndex(index); + let listId = tasks?.at(index)?.key ?? ""; + if (listId !== "") { + showTasksFromList(listId); + } + setOpenLists(false); + } + + const showTasksFromList = (id:string) => { + if (tasks) { + let result: Task[] = []; + let list = tasks.find((taskList) => taskList.key === id) + if (list) { + list.value.forEach((task) => { + if (!task.done) { + result.push(task); + } + }) + } + setFlatTasks(result); + } + } + + useEffect(() => { + if (listNames && listNames.length > 0 && selectedListIndex === -1) { + handleListSelection(0); + } + }, [listNames]) + + + return ( + <> +
+

Tasks to get done:

+
+
+
setOpenLists(!isOpenLists)}> +

{selectedListIndex >= 0 ? listNames?.at(selectedListIndex) : 'Task lists'}

+ +
+
+
+ {listNames && ( + listNames?.length > 0 + ? listNames.map((list, index) => ( + + )) + :

No lists to show

+ )} +
+
+
+
+ { + flatTasks && + ( + flatTasks.length > 0 + ? flatTasks.map((task, index) => { + return ( + + ) + }) + :

No tasks to show

+ ) + } +
+ + ) +} + +export function TasksPreview({ selectedTasks, handleTaskCheck } : Props) { + + return ( +
+ { + selectedTasks.map((task, index) => { + return ( +
+

{task.title}

+ handleTaskCheck(task)} + /> +
+ ) + }) + } +
+ ) +} \ No newline at end of file diff --git a/client/src/model/Scheme.ts b/client/src/model/Scheme.ts index 3a875c67..3bd707af 100644 --- a/client/src/model/Scheme.ts +++ b/client/src/model/Scheme.ts @@ -32,19 +32,6 @@ export function taskToListOfTaskList(tasks: Task[]) { return result; } -// export function listOfTaskListToTask(tasks: TaskList[]) { -// const result: Task[] = []; -// // Iterar sobre cada TaskList en el arreglo -// tasks.forEach((taskList, listIndex) => { -// // Iterar sobre cada tarea en la TaskList -// taskList.forEach(task => { -// // Añadir la tarea al resultado, asignándole el listIndex correspondiente -// result.push({ ...task, listIndex }); -// }); -// }); -// return result; -// } - export interface Event { start: Date, end: Date, @@ -63,4 +50,10 @@ export interface Label { name: string, } -export interface CalendarItem { id: string, name: string, color: string}; \ No newline at end of file +export interface CalendarItem { id: string, name: string, color: string}; + +export interface ScheduleItem { + title: string; + hours: string; + minutes: string; +} \ No newline at end of file diff --git a/client/styles/globals.scss b/client/styles/globals.scss index 3c90b16a..fe3f1626 100644 --- a/client/styles/globals.scss +++ b/client/styles/globals.scss @@ -34,6 +34,7 @@ @use "../src/components/Panel/BoardPreferences/BoardPanel.scss"; @use "../src/components/Panel/ListPreferences/ListPanel.scss"; @use "../src/components/Panel/StatisticsPanel/StatisticsPanel.scss"; +@use "../src/components/List/CheckableTaskList.scss"; @font-face { font-family: 'Primaria'; diff --git a/server/routes/github.js b/server/routes/github.js index 29bb5b55..d77c28af 100644 --- a/server/routes/github.js +++ b/server/routes/github.js @@ -53,15 +53,14 @@ router.get("/github/auth/callback", async function (req, res) { res.cookie('access_token', encryptedToken, { secure: true, httpOnly: true, - domain: '.onrender.com', - sameSite: "Strict" + sameSite: "none" }) - res.redirect(`https://tidytime-wh88.onrender.com/list?status=success`); + res.redirect(`${process.env.FRONT_URL}/list?status=success`); } else { - res.redirect(`https://tidytime-wh88.onrender.com/list?status=failure`) + res.redirect(`${process.env.FRONT_URL}/list?status=failure`) } } catch(error) { - res.redirect(`https://tidytime-wh88.onrender.com/list?status=failure`) + res.redirect(`${process.env.FRONT_URL}/list?status=failure`) }; }); diff --git a/server/routes/google.js b/server/routes/google.js index 23e01831..8ddaee49 100644 --- a/server/routes/google.js +++ b/server/routes/google.js @@ -56,9 +56,9 @@ router.get('/google/auth/callback', async (req, res) => { oauth2Client.setCredentials(tokens); try { const email = await getEmail(req); - res.redirect(`https://tidytime-wh88.onrender.com/calendar?user=${email}`); + res.redirect(`${process.env.FRONT_URL}/calendar?user=${email}`); } catch (error) { - res.redirect('https://tidytime-wh88.onrender.com/calendar'); + res.redirect(`${process.env.FRONT_URL}/calendar`); } }); diff --git a/server/routes/inrupt.js b/server/routes/inrupt.js index f1f71efc..f82c7c01 100644 --- a/server/routes/inrupt.js +++ b/server/routes/inrupt.js @@ -890,7 +890,7 @@ router.get("/solid/login", async function (req, res) { res.redirect(url); } await session.login({ - redirectUrl: "https://tidytime.onrender.com/solid/login/callback", + redirectUrl: `${process.env.BACK_URL}/solid/login/callback`, oidcIssuer: "https://login.inrupt.com", clientName: "TidyTimeDev", handleRedirect: redirectToIDP @@ -903,14 +903,14 @@ router.get("/solid/login", async function (req, res) { router.get("/solid/login/callback", async function (req, res) { try { const session = await getSessionFromStorage(req.cookies.inruptSessionId, storage); - await session.handleIncomingRedirect(`https://tidytime.onrender.com${req.url}`); + await session.handleIncomingRedirect(`${process.env.BACK_URL}${req.url}`); if (session.info.isLoggedIn) { res.cookie("webId", session.info.webId, { secure: true, httpOnly: true, sameSite: "none" }); - res.redirect(`https://tidytime-wh88.onrender.com?user=${session.info.webId}`) + res.redirect(`${process.env.FRONT_URL}?user=${session.info.webId}`) } } catch (error) { console.log(error); diff --git a/server/server.js b/server/server.js index 642a6e38..bb1a79a9 100644 --- a/server/server.js +++ b/server/server.js @@ -8,7 +8,7 @@ const cookieParser = require('cookie-parser'); // ============== APPLICATION CONFIGURATIONS ========== const app = express(); -const PORT = 443; +const PORT = 8080; // ============== APPLICATION MODULES ================ app.use(cors({