repos
/ timelite-nextjs master

timelite-nextjs

mirror archived upstream

A dead simple time tracker that keeps everything in local storage. Next.js, no accounts and no server.

handcodedlocalstoragenextjsreactself-hostedserverlesstime-trackingtimer

3.5 KB · 120 lines · JavaScript Raw History
  1import React, { useState, useEffect, useContext, useRef, useMemo } from "react";
  2import { toast } from "react-toastify";
  3
  4import { Context } from "./context";
  5import TagNoteInput from "./tagNoteInput";
  6import strings from "../l10n/timer";
  7import contextStrings from "../l10n/context";
  8import { timeString, timeDiff } from "../utils/time";
  9
 10import styles from "../styles/components/timer.module.css";
 11
 12const Timer = () => {
 13  const { state, dispatch } = useContext(Context);
 14  const isPaused = !!state.timerPausedAt;
 15  const initialElapsed = isPaused
 16    ? +new Date(state.timerPausedAt) - +new Date(state.timer)
 17    : timeDiff(state.timer);
 18  const [time, setTime] = useState(timeString(initialElapsed));
 19  const refToMain = useRef(null);
 20
 21  strings.setLanguage(state.language);
 22
 23  useEffect(() => {
 24    if (isPaused) {
 25      const frozen = timeString(
 26        +new Date(state.timerPausedAt) - +new Date(state.timer)
 27      );
 28      setTime(frozen);
 29      document.title = `${frozen} — Timelite`;
 30      return;
 31    }
 32    setTime(timeString(timeDiff(state.timer)));
 33    const timerInterval = setInterval(() => {
 34      const timeCurrent = timeString(timeDiff(state.timer));
 35      setTime(timeCurrent);
 36      document.title = `${timeCurrent} — Timelite`;
 37    }, 1000);
 38    return () => {
 39      clearInterval(timerInterval);
 40    };
 41  }, [state.timer, state.timerPausedAt, isPaused]);
 42
 43  useEffect(() => {
 44    if (refToMain.current) {
 45      refToMain.current.focus();
 46    }
 47  }, []);
 48
 49  const allTags = useMemo(() => {
 50    const s = new Set();
 51    for (const e of state.log) for (const t of e.tags || []) s.add(t);
 52    return [...s].sort();
 53  }, [state.log]);
 54
 55  const submitForm = (e) => {
 56    e.preventDefault();
 57    if (state.note.trim()) {
 58      dispatch({ type: "ADD_LOG", note: state.note });
 59      dispatch({ type: "NOTE_UPDATED", note: "" });
 60      contextStrings.setLanguage(state.language);
 61      toast.success(contextStrings.addedEntry);
 62    }
 63  };
 64
 65  return (
 66    <>
 67      <div
 68        className={`${styles.time} ${isPaused ? styles.timePaused : ""}`.trim()}
 69        suppressHydrationWarning
 70      >
 71        {time}
 72        {isPaused && <span className={styles.pausedBadge}>{strings.paused}</span>}
 73      </div>
 74      <form onSubmit={submitForm}>
 75        <div className={styles.inputs}>
 76          <TagNoteInput
 77            ref={refToMain}
 78            className={styles.note}
 79            wrapperClassName={styles.noteWrap}
 80            aria-label={strings.note}
 81            placeholder={strings.note}
 82            value={state.note || ""}
 83            allTags={allTags}
 84            onChange={(val) => dispatch({ type: "NOTE_UPDATED", note: val })}
 85          />
 86        </div>
 87        <div className={styles.buttons}>
 88          <button
 89            className={`${styles.button} ${styles.resetButton} timer__button`}
 90            type="reset"
 91            onClick={() => dispatch({ type: "NEW_TIMER" })}
 92            title="⎇+R"
 93          >
 94            - {strings.reset}
 95          </button>
 96          <button
 97            className={`${styles.button} ${styles.pauseButton} timer__button`}
 98            type="button"
 99            onClick={() =>
100              dispatch({ type: isPaused ? "RESUME_TIMER" : "PAUSE_TIMER" })
101            }
102            title="⎇+P"
103          >
104            {isPaused ? strings.resume : strings.pause}
105          </button>
106          <button
107            className={`${styles.button} ${styles.addButton} timer__button`}
108            type="submit"
109            title="⎇+A"
110          >
111            {strings.add} +
112          </button>
113        </div>
114      </form>
115    </>
116  );
117};
118
119export default Timer;