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.9 KB · 139 lines · JavaScript Raw History
  1import React, { useContext, useState } from "react";
  2import { useRouter } from "next/router";
  3import { GlobalHotKeys, configure } from "react-hotkeys";
  4import PropTypes from "prop-types";
  5import { toast } from "react-toastify";
  6
  7import { Context } from "../components/context";
  8import KeyHelpOverlay from "./keyHelpOverlay";
  9import contextStrings from "../l10n/context";
 10
 11configure({
 12  ignoreTags: [],
 13  ignoreEventsCondition: (keyEvent) => {
 14    if (keyEvent.key === "Enter" || keyEvent.key === " ") {
 15      return true;
 16    }
 17    const t = keyEvent.target;
 18    const isTypingField =
 19      t &&
 20      (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable);
 21    // Arrow keys move the caret / change values inside a field; stealing
 22    // them for log selection while typing is hostile.
 23    if (
 24      isTypingField &&
 25      (keyEvent.key === "?" ||
 26        keyEvent.key === "ArrowUp" ||
 27        keyEvent.key === "ArrowDown")
 28    ) {
 29      return true;
 30    }
 31    return false;
 32  },
 33});
 34
 35const keyMap = {
 36  RESET: "alt+r",
 37  ADD_LOG: "alt+a",
 38  PAUSE_TOGGLE: "alt+p",
 39  TIMER_PAGE: "alt+t",
 40  LOG_PAGE: "alt+l",
 41  ABOUT_PAGE: "alt+o",
 42  SUMMARY_PAGE: "alt+s",
 43  CLEAR_LOG: "alt+c",
 44  LOG_NEXT: "ArrowDown",
 45  LOG_PREVIOUS: "ArrowUp",
 46  LOG_EDIT: "alt+e",
 47  LOG_DELETE_SINGLE: "alt+d",
 48  KEY_HELP: ["shift+/", "?"],
 49};
 50
 51const HotKeysMapping = (props) => {
 52  const { state, dispatch } = useContext(Context);
 53  const [showHelp, setShowHelp] = useState(false);
 54
 55  const router = useRouter();
 56
 57  const onLogPage = () => router.pathname === "/log";
 58
 59  const handlers = {
 60    RESET: () => dispatch({ type: "NEW_TIMER" }),
 61    ADD_LOG: (event) => {
 62      event.preventDefault();
 63      if (state.note.trim()) {
 64        dispatch({ type: "ADD_LOG", note: state.note });
 65        contextStrings.setLanguage(state.language);
 66        toast.success(contextStrings.addedEntry);
 67      }
 68    },
 69    PAUSE_TOGGLE: (event) => {
 70      event.preventDefault();
 71      dispatch({
 72        type: state.timerPausedAt ? "RESUME_TIMER" : "PAUSE_TIMER",
 73      });
 74    },
 75    TIMER_PAGE: (event) => {
 76      event.preventDefault();
 77      router.push("/");
 78    },
 79    LOG_PAGE: () => router.push("/log"),
 80    ABOUT_PAGE: () => router.push("/about"),
 81    SUMMARY_PAGE: () => router.push("/summary"),
 82    CLEAR_LOG: () => {
 83      if (state.log.length === 0) return;
 84      contextStrings.setLanguage(state.language);
 85      if (!window.confirm(contextStrings.confirmClearLog)) return;
 86      dispatch({ type: "CLEAR_LOG" });
 87      toast.error(contextStrings.resetLog);
 88    },
 89    LOG_NEXT: (event) => {
 90      event.preventDefault();
 91      if (onLogPage()) {
 92        dispatch({ type: "TOGGLE_EDITION", edit: false });
 93        dispatch({ type: "NEXT_LOG_ITEM" });
 94      }
 95    },
 96    LOG_PREVIOUS: (event) => {
 97      event.preventDefault();
 98      if (onLogPage()) {
 99        dispatch({ type: "TOGGLE_EDITION", edit: false });
100        dispatch({ type: "PREVIOUS_LOG_ITEM" });
101      }
102    },
103    LOG_EDIT: (event) => {
104      event.preventDefault();
105      if (!state.logSelectedEntry) return;
106      if (onLogPage()) dispatch({ type: "TOGGLE_EDITION", edit: true });
107    },
108    LOG_DELETE_SINGLE: (event) => {
109      event.preventDefault();
110      if (!state.logSelectedEntry) return;
111      if (!onLogPage()) return;
112      contextStrings.setLanguage(state.language);
113      if (!window.confirm(contextStrings.confirmDeleteEntry)) return;
114      dispatch({ type: "REMOVE_LOG" });
115      toast.error(contextStrings.deletedEntry);
116    },
117    KEY_HELP: (event) => {
118      event.preventDefault();
119      setShowHelp((v) => !v);
120    },
121  };
122
123  return (
124    <GlobalHotKeys keyMap={keyMap} handlers={handlers}>
125      {props.children}
126      <KeyHelpOverlay open={showHelp} onClose={() => setShowHelp(false)} />
127    </GlobalHotKeys>
128  );
129};
130
131HotKeysMapping.propTypes = {
132  children: PropTypes.oneOfType([
133    PropTypes.element,
134    PropTypes.arrayOf(PropTypes.element),
135  ]),
136};
137
138export default HotKeysMapping;