A dead simple time tracker that keeps everything in local storage. Next.js, no accounts and no server.
handcodedlocalstoragenextjsreactself-hostedserverlesstime-trackingtimer
1import React, { useContext } from "react";
2import Link from "next/link";
3import { useRouter } from "next/router";
4
5import { Context } from "./context";
6import strings from "../l10n/sidebar";
7
8import styles from "../styles/components/sidebar.module.css";
9
10const Sidebar = () => {
11 const { state } = useContext(Context);
12 strings.setLanguage(state.language);
13 const router = useRouter();
14
15 return (
16 <aside className={styles.side}>
17 <Link href="/" className={styles.brand} aria-label={strings.name}>
18 <span className={styles.brandMark} aria-hidden="true">
19 <svg viewBox="0 0 24 24" width="22" height="22">
20 <circle
21 cx="12"
22 cy="12"
23 r="9.5"
24 fill="none"
25 stroke="currentColor"
26 strokeWidth="1.5"
27 opacity="0.9"
28 />
29 <circle cx="12" cy="12" r="4" fill="currentColor" />
30 </svg>
31 </span>
32 <span className={styles.brandText}>{strings.name}</span>
33 </Link>
34 <div className={styles.pages}>
35 <Link
36 href="/"
37 className={`${styles.pageLink} ${router.pathname === "/" ? styles.pageLinkActive : ""}`.trim()}
38 aria-label={strings.timer}
39 title={`${strings.timer} (⎇+T)`}
40 >
41 <TimerIcon />
42 </Link>
43 <Link
44 href="/log"
45 className={`${styles.pageLink} ${router.pathname === "/log" ? styles.pageLinkActive : ""}`.trim()}
46 aria-label={strings.log}
47 title={`${strings.log} (⎇+L)`}
48 >
49 <LogIcon />
50 </Link>
51 <Link
52 href="/summary"
53 className={`${styles.pageLink} ${router.pathname === "/summary" ? styles.pageLinkActive : ""}`.trim()}
54 aria-label={strings.summary}
55 title={`${strings.summary} (⎇+S)`}
56 >
57 <SummaryIcon />
58 </Link>
59 </div>
60 <Link
61 href="/about"
62 className={styles.about}
63 aria-label={strings.about}
64 title={`${strings.about} (⎇+O) — press ? for shortcuts`}
65 >
66 <HelpIcon />
67 </Link>
68 </aside>
69 );
70};
71
72export default Sidebar;
73
74const HelpIcon = () => (
75 <svg
76 xmlns="http://www.w3.org/2000/svg"
77 height="22"
78 viewBox="0 0 24 24"
79 width="22"
80 fill="currentColor"
81 aria-hidden="true"
82 >
83 <path d="M0 0h24v24H0V0z" fill="none" />
84 <path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z" />
85 </svg>
86);
87
88const TimerIcon = () => (
89 <svg
90 xmlns="http://www.w3.org/2000/svg"
91 height="22"
92 viewBox="0 0 24 24"
93 width="22"
94 fill="currentColor"
95 aria-hidden="true"
96 >
97 <path d="M0 0h24v24H0V0z" fill="none" />
98 <path d="M15.07 1.01h-6v2h6v-2zm-4 13h2v-6h-2v6zm8.03-6.62l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42C16.14 4.74 14.19 4 12.07 4c-4.97 0-9 4.03-9 9s4.02 9 9 9 9-4.03 9-9c0-2.11-.74-4.06-1.97-5.61zm-7.03 12.62c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z" />
99 </svg>
100);
101
102const LogIcon = () => (
103 <svg
104 xmlns="http://www.w3.org/2000/svg"
105 height="22"
106 viewBox="0 0 24 24"
107 width="22"
108 fill="currentColor"
109 aria-hidden="true"
110 >
111 <path d="M0 0h24v24H0V0z" fill="none" />
112 <path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" />
113 </svg>
114);
115
116const SummaryIcon = () => (
117 <svg
118 xmlns="http://www.w3.org/2000/svg"
119 enableBackground="new 0 0 24 24"
120 height="22"
121 viewBox="0 0 24 24"
122 width="22"
123 fill="currentColor"
124 aria-hidden="true"
125 >
126 <g>
127 <rect fill="none" height="24" width="24" />
128 <g>
129 <path d="M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3z M19,19H5V5h14V19z" />
130 <rect height="5" width="2" x="7" y="12" />
131 <rect height="10" width="2" x="15" y="7" />
132 <rect height="3" width="2" x="11" y="14" />
133 <rect height="2" width="2" x="11" y="10" />
134 </g>
135 </g>
136 </svg>
137);