A dead simple time tracker that keeps everything in local storage. Next.js, no accounts and no server.
handcodedlocalstoragenextjsreactself-hostedserverlesstime-trackingtimer
1.side {
2 position: fixed;
3 left: 0;
4 top: 0;
5 bottom: 0;
6 width: 60px;
7 background-color: var(--bg-deep);
8 border-right: 1px solid var(--border);
9 display: flex;
10 justify-content: space-between;
11 flex-direction: column;
12 align-items: center;
13 padding: 14px 0;
14 z-index: 10;
15}
16
17@media (max-width: 1023.99px) {
18 .side {
19 width: 100%;
20 height: 56px;
21 bottom: 0;
22 left: 0;
23 top: auto;
24 flex-direction: row;
25 align-items: center;
26 justify-content: space-between;
27 padding: 0 12px;
28 border-right: 0;
29 border-top: 1px solid var(--border);
30 }
31}
32
33/* Brand mark + vertical wordmark */
34.brand {
35 display: flex;
36 flex-direction: column;
37 align-items: center;
38 gap: 16px;
39 text-decoration: none;
40 color: var(--text-muted);
41 transition: color 200ms ease;
42 padding-top: 4px;
43}
44
45.brand:hover {
46 color: var(--green-bright);
47}
48
49.brandMark {
50 display: flex;
51 align-items: center;
52 justify-content: center;
53 width: 32px;
54 height: 32px;
55 color: var(--green-bright);
56 filter: drop-shadow(0 0 6px rgba(107, 158, 120, 0.35));
57 transition:
58 filter 220ms ease,
59 transform 220ms ease;
60}
61
62.brandMark svg {
63 display: block;
64}
65
66.brandMark svg circle:last-child {
67 transform-origin: center;
68 animation: brandPulse 2.2s ease-in-out infinite;
69}
70
71.brand:hover .brandMark {
72 filter: drop-shadow(0 0 10px rgba(107, 158, 120, 0.6));
73 transform: scale(1.05);
74}
75
76@keyframes brandPulse {
77 0%,
78 100% {
79 opacity: 1;
80 transform: scale(1);
81 }
82 50% {
83 opacity: 0.55;
84 transform: scale(0.82);
85 }
86}
87
88.brandText {
89 font-size: 1.3em;
90 font-weight: 700;
91 text-transform: uppercase;
92 letter-spacing: 0.14em;
93 color: var(--green-bright);
94 writing-mode: vertical-rl;
95 text-orientation: mixed;
96 line-height: 1;
97 white-space: nowrap;
98 text-shadow: 0 0 10px var(--green-glow);
99}
100
101@media (max-width: 1023.99px) {
102 .brand {
103 flex-direction: row;
104 gap: 10px;
105 padding-top: 0;
106 }
107
108 .brandMark {
109 width: 26px;
110 height: 26px;
111 }
112
113 .brandMark svg {
114 width: 18px;
115 height: 18px;
116 }
117
118 .brandText {
119 font-size: 0.8em;
120 writing-mode: horizontal-tb;
121 letter-spacing: 0.2em;
122 }
123}
124
125.pages {
126 display: flex;
127 flex-direction: column;
128 gap: 4px;
129 align-items: center;
130}
131
132@media (max-width: 1023.99px) {
133 .pages {
134 flex-direction: row;
135 gap: 2px;
136 }
137}
138
139.pageLink {
140 text-decoration: none;
141 position: relative;
142 display: flex;
143 align-items: center;
144 justify-content: center;
145 width: 40px;
146 height: 40px;
147 border-radius: var(--radius);
148 color: var(--text-muted);
149 transition:
150 color 200ms ease,
151 background 200ms ease;
152}
153
154.pageLink svg {
155 width: 20px;
156 height: 20px;
157 fill: currentColor;
158}
159
160.pageLink:hover {
161 color: var(--green-bright);
162 background: var(--green-dim);
163}
164
165.pageLinkActive {
166 color: var(--green-bright);
167 background: var(--green-dim);
168 box-shadow: inset 2px 0 0 var(--green);
169}
170
171@media (max-width: 1023.99px) {
172 .pageLinkActive {
173 box-shadow: inset 0 2px 0 var(--green);
174 }
175}
176
177.about {
178 display: flex;
179 justify-content: center;
180 align-items: center;
181 width: 40px;
182 height: 40px;
183 border-radius: var(--radius);
184 text-decoration: none;
185 color: var(--text-muted);
186 transition:
187 color 200ms ease,
188 background 200ms ease;
189}
190
191.about svg {
192 width: 20px;
193 height: 20px;
194 fill: currentColor;
195}
196
197.about:hover {
198 color: var(--amber);
199 background: var(--amber-dim);
200}