Timestamp task worked on per user

I would like to allow the user to show that they have practiced one or more of their specific tasks on different dates.

the task is not to be marked as “done” in a boolean sense but rather I need to track each time a specific user works on a specific task.

I am trying to keep the row count down so I am thinking the most efficient way to store the data would be to create a row for userA/task 1, then use split tags to record each time userA works on task1. - Row 2 would be userA task2, with timestamps for each time they worked on task2.

Row 3 would be userB, Task1 timestamp, row 4 userB/task2, etc

The issue is I can see the row count exploding as users and tasks increase. No doubt there is some Glide-y work around that is beyond my noob skills at this point. Help appreciated.

Carl

I think it should be done in a JSON trebuchet sense.

  • Have two user-specific fields for “start time” and “stop time”. Then a JSON object with those two fields.
  • Have a multiple files column to store the JSON array.
  • Have a “Make array” column to combine the multiple files column with the JSON object.
  • Create two buttons with two actions, “start” and “stop” for your front end.
  • Start button would be visible when the “start time” field is empty. It would set the current timestamp to the “start time” field.
  • End button would be visible when the “start time” field is not empty. It would set the current timestamp to the “end time” field, set the “Make array” column value to the “Multiple files” field, then clear both the start and end time.

I will record a video later for this.

HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>AlpineJS Stopwatch</title>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.10.5/cdn.min.js"
      defer
    ></script>
    <style>
      body {
          font-family: Arial, sans-serif;
          display: flex;
          justify-content: center;
          align-items: center;
          height: 100vh;
          margin: 0;
          background-color: #f0f0f0;
      }
      .stopwatch {
          background-color: #ffffff;
          border-radius: 10px;
          padding: 20px;
          box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
          text-align: center;
      }
      .time {
          font-size: 48px;
          font-weight: bold;
          margin-bottom: 20px;
          color: #333;
      }
      button {
          font-size: 16px;
          padding: 10px 20px;
          margin: 0 5px;
          border: none;
          border-radius: 5px;
          cursor: pointer;
          transition: background-color 0.3s;
      }
      .start { background-color: #4CAF50; color: white; }
      .pause { background-color: #FFC107; color: black; }
      .resume { background-color: #2196F3; color: white; }
      .stop { background-color: #F44336; color: white; }
      .reset { background-color: #9E9E9E; color: white; }
      button:hover { opacity: 0.8; }
    </style>
  </head>
  <body>
    <div class="stopwatch" x-data="stopwatch()">
      <div
        class="time"
        x-text="formatTime(elapsedTime)"
      ></div>
      <button
        @click="start"
        x-show="!isRunning && !isPaused"
        class="start"
      >
        Start
      </button>
      <button
        @click="pause"
        x-show="isRunning"
        class="pause"
      >
        Pause
      </button>
      <button
        @click="resume"
        x-show="isPaused"
        class="resume"
      >
        Resume
      </button>
      <button
        @click="stop"
        x-show="isRunning || isPaused"
        class="stop"
      >
        Stop
      </button>
      <button
        @click="reset"
        x-show="isPaused || (!isRunning && !isPaused)"
        class="reset"
      >
        Reset
      </button>
    </div>
    <script>
      function stopwatch() {
        return {
          isRunning: false,
          isPaused: false,
          startTime: 0,
          elapsedTime: 0,
          timerInterval: null,
          start() {
            if (!this.isRunning && !this.isPaused) {
              this.isRunning = true;
              this.startTime =
                Date.now() - this.elapsedTime;
              this.timerInterval = setInterval(() => {
                this.elapsedTime =
                  Date.now() - this.startTime;
              }, 10);
              startRecord();
            }
          },
          pause() {
            if (this.isRunning) {
              this.isRunning = false;
              this.isPaused = true;
              clearInterval(this.timerInterval);
            }
          },
          resume() {
            if (this.isPaused) {
              this.isRunning = true;
              this.isPaused = false;
              this.startTime =
                Date.now() - this.elapsedTime;
              this.timerInterval = setInterval(() => {
                this.elapsedTime =
                  Date.now() - this.startTime;
              }, 10);
            }
          },
          stop() {
            this.isRunning = false;
            this.isPaused = false;
            clearInterval(this.timerInterval);
            S.duration = this.elapsedTime / 1000;
            this.elapsedTime = 0;
            setTimeout(() => {
              logRecord();
            }, 1000);
          },
          reset() {
            this.elapsedTime = 0;
            reset();
          },
          formatTime(ms) {
            let centiseconds = Math.floor((ms % 1000) / 10);
            let seconds = Math.floor((ms / 1000) % 60);
            let minutes = Math.floor(
              (ms / (1000 * 60)) % 60
            );
            let hours = Math.floor(
              (ms / (1000 * 60 * 60)) % 24
            );
            return `${hours
              .toString()
              .padStart(2, "0")}:${minutes
              .toString()
              .padStart(2, "0")}:${seconds
              .toString()
              .padStart(2, "0")}.${centiseconds
              .toString()
              .padStart(2, "0")}`;
          },
        };
      }
    </script>
  </body>
</html>

JavaScript to generate the table:

function jsonToHtmlTable(jsonString) {
  
  // Parse JSON
  const data = JSON.parse(jsonString);
  
  // Function to format date
  function formatDate(dateString) {
    const date = new Date(dateString);
    return date.toLocaleString('en-US', { 
      year: 'numeric', 
      month: '2-digit', 
      day: '2-digit', 
      hour: '2-digit', 
      minute: '2-digit', 
      second: '2-digit', 
      fractionalSecondDigits: 3 
    });
  }
  
  // Function to format duration
  function formatDuration(durationSeconds) {
    const totalSeconds = parseFloat(durationSeconds);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = Math.floor(totalSeconds % 60);
    const milliseconds = Math.round((totalSeconds % 1) * 1000);
    
    let formattedDuration = '';
    if (hours > 0) {
      formattedDuration += `${hours}h `;
    }
    if (minutes > 0 || hours > 0) {
      formattedDuration += `${minutes}m `;
    }
    formattedDuration += `${seconds}.${milliseconds.toString().padStart(3, '0')}s`;
    
    return formattedDuration.trim();
  }
  
  // Generate table HTML
  let tableHtml = '<table style="border-collapse: collapse; width: 100%; font-family: Arial, sans-serif;"><thead style="background-color: #f2f2f2;"><tr><th style="border: 1px solid #ddd; padding: 12px; text-align: left;">Start Time</th><th style="border: 1px solid #ddd; padding: 12px; text-align: left;">End Time</th><th style="border: 1px solid #ddd; padding: 12px; text-align: left;">Duration</th></tr></thead><tbody>';
  
  data.forEach((item, index) => {
    const rowStyle = index % 2 === 0 ? 'background-color: #f9f9f9;' : 'background-color: #ffffff;';
    tableHtml += `<tr style="${rowStyle}">`;
    tableHtml += `<td style="border: 1px solid #ddd; padding: 12px;">${formatDate(item['Start Time'])}</td>`;
    tableHtml += `<td style="border: 1px solid #ddd; padding: 12px;">${formatDate(item['End Time'])}</td>`;
    tableHtml += `<td style="border: 1px solid #ddd; padding: 12px;">${formatDuration(item['Duration'])}</td>`;
    tableHtml += '</tr>';
  });
  
  tableHtml += '</tbody></table>';
  
  return tableHtml;
}

return jsonToHtmlTable(p1);
1 Like

Thanks for the detailed explanaion, video and code. There’s a lot to absorb but its great to now have a pausable stopwatch function.

Initially I am going to implement a simple “I completed task 1 today” for earch user and will implement the duration in the next version.

MUCH appreciated.

1 Like

I still don’t get how the actions are connected to the buttons. Could you please explain it? Thank you!

There are 3 defined actions with Glide: startRecord, logRecord and reset. I name them in the actions section. You can check the code part and see where I reference those.

Basically each button I have in the HTML would have an on-click action defined by the @click part. Then with each action, I do some stuff (change the status of some boolean variables, set some values, and call actions where needed).

I apologize for my blindness! So the names of the actions simply become function names we can use however we like, right?

Yes that’s correct. You just have to call it again in the body of the HTML.

1 Like

Thanks for sharing Thinh, the new custom component is so powerful. I was sure that you would love it and test it asap