Source: public/js/todo.js

// todo.js

//temporary username until database is operational
//const username = "username_test";

// DOM Elements
/**
 * Element for the Habit Tracker's list.
 * @type {HTMLElement}
 */
const habitsList = document.getElementById("habits-list"); // For Habit Tracker

/**
 * Element to display a motivational quote.
 * @type {HTMLElement}
 */
const quoteElement = document.getElementById("motivational-quote");

//Calendar
/**
 * Calendar header element.
 * @type {HTMLElement}
 */
const calendarHeader = document.getElementById("calendar-header");

/**
 * Calendar grid element.
 * @type {HTMLElement}
 */
const calendarGrid = document.getElementById("calendar-grid");

/**
 * Current calendar time reference.
 * @type {Date}
 */
var calendarTime = new Date();
var calendarDisplay = "month";

// Set date constants for today's date info as well as 7 and
// 30 days from now
/**
 * Today's date with time set to 23:59:59.999.
 * @type {Date}
 */
const currentDate = new Date();
currentDate.setHours(23, 59, 59, 999);

/**
 * The current year.
 * @type {number}
 */
const currentYear = currentDate.getFullYear();

/**
 * The current month (0-based index).
 * @type {number}
 */
const currentMonth = currentDate.getMonth();

/**
 * The current day of the month.
 * @type {number}
 */
const currentDay = currentDate.getDate();

//const weekDate = new Date();
//weekDate.setDate(currentDay + 7);
//weekDate.setHours(23, 59, 59, 999);

//const monthDate = new Date();
//monthDate.setDate(currentDay + 30);
//monthDate.setHours(23, 59, 59, 999);

//temporary todo tasks until database is operational
/*
var widgetData = {
  today: [
    { name: "Task 1", id: new Date().valueOf() + Math.random(), dueDate: currentDate, completed: false },
    { name: "Task 2", id: new Date().valueOf() + Math.random(), dueDate: currentDate, completed: false },
    { name: "Task 3", id: new Date().valueOf() + Math.random(), dueDate: currentDate, completed: false },
    { name: "Task 4", id: new Date().valueOf() + Math.random(), dueDate: currentDate, completed: false },
  ],
  week: [
    { name: "Week Task 1", id: new Date().valueOf() + Math.random(), dueDate: weekDate, completed: false },
    { name: "Week Task 2", id: new Date().valueOf() + Math.random(), dueDate: weekDate, completed: false },
    { name: "Week Task 3", id: new Date().valueOf() + Math.random(), dueDate: weekDate, completed: false },
    { name: "Week Task 4", id: new Date().valueOf() + Math.random(), dueDate: weekDate, completed: false },
  ],
  month: [
    { name: "Month Task 1", id: new Date().valueOf() + Math.random(), dueDate: monthDate, completed: false },
    { name: "Month Task 2", id: new Date().valueOf() + Math.random(), dueDate: monthDate, completed: false },
    { name: "Month Task 3", id: new Date().valueOf() + Math.random(), dueDate: monthDate, completed: false },
    { name: "Month Task 4", id: new Date().valueOf() + Math.random(), dueDate: monthDate, completed: false },
  ],

  habits: [
    {
      name: "Did you drink water today?",
      id: new Date().valueOf() + Math.random(),
      completed: false,
    },
    {
      name: "Did you exercise today?",
      id: new Date().valueOf() + Math.random(),
      completed: false,
    },
    {
      name: "Did you do your XYZ today?",
      id: new Date().valueOf() + Math.random(),
      completed: false,
    },
  ],
};
*/

/**
 * Tracks the current index for widget pagination.
 * @type {{today: number, week: number, month: number, habits: number}}
 */
var widgetIndex = {
  today: 0,
  week: 0,
  month: 0,
  habits: 0,
};

/**
 * Items per page for widget pagination.
 * @type {{today: number, week: number, month: number, habits: number}}
 */
var itemsPerPage = {
  today: 4,
  week: 4,
  month: 2,
  habits: 2,
};

layouts = window.LAYOUTS;
themes = window.THEMES;

/*
// Task Modal Functions
function addTaskWindow() {
  document.getElementById("add-task").style.display = "block";
}

function closeTaskWindow() {
  document.getElementById("add-task").style.display = "none";
}

function addTask(event) {
  event.preventDefault(); // Prevent the form from reloading the page

  const taskName = document.getElementById("task-name").value;
  const taskDueInput = document.getElementById("task-due-date").value;

  const taskDueDate = new Date(taskDueInput);
  taskDueDate.setDate(taskDueDate.getDate() + 1);

  const newTask = {
    id: taskDueDate.valueOf() + Math.random(),
    name: taskName,
    dueDate: taskDueDate.valueOf(),
    completed: false,
  };

  // Close modal and reset form
  closeTaskWindow();
  document.getElementById("add-task-form").reset();

  var listId;
  if (newTask.dueDate <= currentDate.valueOf()) {
    listId = "today";
  } else if (newTask.dueDate <= weekDate.valueOf()) {
    listId = "week";
  } else if (newTask.dueDate <= monthDate.valueOf()) {
    listId = "month";
  }

  tasks = widgetData[listId];

  // Push into the array
  // Update the lists with the new task
  tasks.push(newTask);
  tasks.sort((a, b) => a.dueDate - b.dueDate);

  // Local storage
  localStorage.setItem(listId, JSON.stringify(tasks));

  renderWidget(listId, 0);
}

// Widget Functions
function renderWidget(id, offset) {
  const data = widgetData[id];
  widgetIndex[id] += offset;

  const start = widgetIndex[id] * itemsPerPage[id];
  const end = start + itemsPerPage[id];
  const pageData = data.slice(start, end);

  const backButton = document.getElementById(id + "-back");
  const forwardButton = document.getElementById(id + "-forward");
  if (start === 0) {
    backButton.disabled = true;
  } else {
    backButton.disabled = false;
  }

  if (end >= data.length) {
    forwardButton.disabled = true;
  } else {
    forwardButton.disabled = false;
  }

  if (id === "habits") {
    updateHabits(pageData);
  } else {
    updateTasks(id + "-list", pageData);
  }
}

function updateTasks(listId, taskList) {
  listTarget = document.getElementById(listId);
  listTarget.innerHTML = "";

  taskList.forEach((task) => {
    // Create <li>
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";

    const span = document.createElement("span");
    const taskDate = new Date(task.dueDate);
    span.textContent = `${task.name} (Due: ${taskDate.toLocaleDateString()})`;

    const li = document.createElement("li");
    li.appendChild(checkbox);
    li.appendChild(span);

    listTarget.appendChild(li);
  });
}

// Function to render habits in the Habit Tracker widget
function updateHabits(habitsArray) {
  habitsList.innerHTML = "";

  habitsArray.forEach((habit) => {
    const span = document.createElement("span");
    span.textContent = habit.name;

    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.checked = habit.completed;
    checkbox.addEventListener("change", () => toggleHabit(habit.id));

    if (habit.completed) {
      checkbox.checked = true;
      span.style.textDecoration = "line-through";
      span.style.color = "#777"; // Gray out completed habits
    } else {
      checkbox.checked = false;
    }

    const li = document.createElement("li");
    li.appendChild(checkbox);
    li.appendChild(span);

    habitsList.appendChild(li);
  });
}

// Function to toggle habit completion
function toggleHabit(habitId) {
  habitTarget = widgetData["habits"].find((habit) => habit.id === habitId);
  habitTarget.completed = !habitTarget.completed;

  renderWidget("habits", 0);

  localStorage.setItem("habits", JSON.stringify(widgetData["habits"]));
}
*/

/**
 * Sets the layout for the calendar based on the saved layout in local storage.
 *
 * If the layout is "layout-week-calendar", the calendar display mode is set to week,
 * otherwise it is set to month. The calendar is then initialized.
 */
function setLayout() {
  layout = localStorage.getItem("layout");
  if (layout === "layout-week-calendar") {
    calendarDisplay = "week";
    initCalendar();
  } else {
    calendarDisplay = "month";
    initCalendar();
  }
}

/**
 * Initializes the calendar based on the current display mode.
 *
 * For "week" display, the calendar is set to start on the first day of the current week.
 * For "month" display, the calendar is set to the first day of the current month.
 * After setting the date reference, it calls setCalendar to render the view.
 */
function initCalendar() {
  if (calendarDisplay === "week") {
    weekday = currentDate.getDay();
    dayDiff = currentDate.getDate() - weekday;

    calendarTime = new Date(currentYear, currentMonth, dayDiff);
  } else if (calendarDisplay === "month") {
    calendarTime = new Date(currentYear, currentMonth, 1);
  }

  setCalendar(0);
}

/**
 * Updates the calendar based on the current display mode and a provided offset.
 *
 * For a week display, the calendar is offset by multiples of 7 days.
 * For a month display, the calendar is offset by full months.
 *
 * @param {number} offset - The offset value (weeks if in week mode, months if in month mode).
 */
function setCalendar(offset) {
  if (calendarDisplay === "week") {
    calendarTime.setDate(calendarTime.getDate() + offset * 7);

    year = calendarTime.getFullYear();
    month = calendarTime.getMonth();

    lastDay = 7;
    weekday = 0;
  } else if (calendarDisplay === "month") {
    calendarTime.setMonth(calendarTime.getMonth() + offset);
    
    year = calendarTime.getFullYear();
    month = calendarTime.getMonth();

    lastDay = new Date(year, month + 1, 0).getDate();
    weekday = calendarTime.getDay();
  }

  const monthName = calendarTime.toLocaleString('default', {month: 'long'});
  const dateText = monthName + " " + year;

  loadCalendar(weekday, lastDay, dateText);
}

/**
 * Renders the calendar header and grid based on parameters.
 *
 * @param {number} weekday - The day of the week that the first day of the calendar falls on.
 * @param {number} lastDay - The number of days in the calendar period.
 * @param {string} dateText - The formatted date text for the calendar header.
 */
async function loadCalendar(weekday, lastDay, dateText) {
  var calendarDateText = document.createElement('h2');
  calendarDateText.innerText = dateText;

  calendarHeader.innerHTML = "";
  calendarHeader.appendChild(calendarDateText);

  try {
    const response = await fetch("/api.events");
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    var events = await response.json();
  } catch (error) {
    console.error("Fetch error:", error);
  }

  calendarGrid.innerHTML = "";
  calendarLoopTime = new Date(calendarTime);
  for (let day = 0; day < lastDay + weekday; day++) {
    const dayCellDiv = document.createElement('div');

    if (day >= weekday) {
      dayCellDiv.className = 'calendar-cell';

      if (calendarLoopTime.toDateString() === currentDate.toDateString()) {
        dayCellDiv.id = "current-day";
      }

      const dayNumberSpan = document.createElement('span');
      dayNumberSpan.className = "day-number";
      dayNumberSpan.textContent = calendarLoopTime.getDate();
      dayCellDiv.appendChild(dayNumberSpan);

      //example code for adding tasts to day (currently adding task icon
      //to current date)
      var todayEvents = events.filter(function(event) {
        eventDate = new Date(event.startDate);
        eventDate.setDate(eventDate.getDate() + 1);
        return calendarLoopTime.toDateString() === eventDate.toDateString();
      });
      if (todayEvents.length > 0) {
        console.log(todayEvents[0].startDate)
        const taskDiv = document.createElement('div');
        taskDiv.className = "calendar-task";

        dayCellDiv.appendChild(taskDiv);
      }

      calendarLoopTime.setDate(calendarLoopTime.getDate() + 1);
    }

    calendarGrid.appendChild(dayCellDiv);
  }
}

// Initialization on Page Load
/**
 * Handles the initial setup when the window loads.
 *
 * - Sets the layout and initializes the calendar.
 * - Loads motivational quotes, applies the saved theme, layout, and font.
 * - (Commented code is present to load and render tasks from local storage.)
 */
window.addEventListener("load", () => {
  // Load Tasks
  /*savedPages = ["today", "week", "month", "habits"];
  savedPages.forEach((page) => {
    const savedTasks = localStorage.getItem(page);
    if (savedTasks) {
      // Parse the JSON string back into an array of tasks
      widgetData[page] = JSON.parse(savedTasks);
    }

    renderWidget(page, 0);
  });*/

  setLayout();

  // Initialize quote cycling on window load
  // Start cycling motivational quotes
  
  let checkedQuotes = localStorage.getItem('checkedQuotes').split(',');
  for (let option of checkedQuotes) {
    quotes = quotes.concat(window[option]);
  }

  shuffleArray(quotes);
  cycleQuotes();

  // Apply saved theme
  document.body.classList.add(localStorage.getItem("theme"));
  // Apply saved layout
  document.body.getElementsByClassName("main-content")[0].id = localStorage.getItem("layout");
  // Apply saved font
  document.body.style.fontFamily = localStorage.getItem("font");
});

// 6. Motivational Quotes Functionality
/**
 * Array to store motivational quotes.
 * @type {Array<string>}
 */
let quotes = [];

/**
 * Shuffles an array in place using the Fisher-Yates algorithm.
 *
 * @param {Array<any>} array - The array to shuffle.
 */
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
  }
}

// Function to cycle through quotes
/**
 * Cycles through motivational quotes and updates the display element with a fade effect.
 *
 * The function initially displays the first quote, and then every 5 seconds it changes
 * the quote with a fade-out and fade-in transition.
 */
function cycleQuotes() {
  const quoteElement = document.getElementById("motivational-quote");
  let quoteIndex = 0;
  
    // Function to display the next quote
      /**
   * Displays the next quote with a fade transition.
   */
  const showNextQuote = () => {
    // Remove 'visible' class to start fade-out
    quoteElement.classList.remove("visible");

    // After the fade-out transition ends, update the text and fade in
    setTimeout(() => {
      quoteElement.textContent = quotes[quoteIndex];
      quoteElement.classList.add("visible");

      // Update index to the next quote, looping back if necessary
      quoteIndex = (quoteIndex + 1) % quotes.length;
    }, 1000); // 1000ms matches the CSS transition duration
  };

  // Initially show the first quote
  showNextQuote();

  // Set interval to change quotes every 5 seconds
  setInterval(showNextQuote, 5000);
}