Skip to content

Commit 5ad6897

Browse files
committed
feat(settings): Add options to hide optional pages
1 parent b0196d0 commit 5ad6897

6 files changed

Lines changed: 214 additions & 46 deletions

File tree

src/app/layout.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import { Layout } from "components/layouts/layout"
1515
import { Github } from "components/ui/icon"
1616
import { IconButton } from "components/ui/icon-button"
1717
import { ErrorBoundary } from "components/utility/error-boundary"
18+
import { preferencesData } from "data/preferences"
19+
import { useAtom } from "lib/yaasl"
1820
import { useTrans } from "locales/locale-provider"
1921

20-
const routes = [
22+
export const routes = [
2123
{ to: "/", title: msg`Time Tracker`, icon: ClockFading },
2224
{ to: "/calendar", title: msg`Calendar View`, icon: CalendarRange },
2325
{ to: "/search", title: msg`Search`, icon: Search },
@@ -34,13 +36,18 @@ const routes = [
3436
const SideActions = () => {
3537
const [path] = useHashLocation()
3638
const trans = useTrans()
39+
const { hiddenRoutes = [] } = useAtom(preferencesData)
40+
41+
const visibleRoutes = routes.filter(
42+
route => !hiddenRoutes.includes(route.to ?? route.href)
43+
)
3744

3845
const isActive = (to?: string) =>
3946
!to ? false : to.split("/").find(Boolean) === path.split("/").find(Boolean)
4047

4148
return (
4249
<>
43-
{routes.map(props => (
50+
{visibleRoutes.map(props => (
4451
<IconButton
4552
key={props.to ?? props.href}
4653
{...props}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { KeyboardEvent } from "react"
2+
3+
import { css, keyframes } from "goober"
4+
import { Check } from "lucide-react"
5+
6+
import { Icon } from "components/ui/icon"
7+
import { IconProp } from "types/base-props"
8+
import { cn } from "utils/cn"
9+
import { focusNavigator } from "utils/focus-navigator"
10+
import { hstack, interactive } from "utils/styles"
11+
12+
const shiftFocus = (event: KeyboardEvent, x: number) =>
13+
focusNavigator({
14+
event,
15+
cellSelector: "button[role='checkbox']",
16+
offset: { x, y: 0 },
17+
})
18+
const eventByKey: Record<string, (event: KeyboardEvent) => void> = {
19+
ArrowLeft: event => shiftFocus(event, -1),
20+
ArrowUp: event => shiftFocus(event, -1),
21+
ArrowRight: event => shiftFocus(event, 1),
22+
ArrowDown: event => shiftFocus(event, 1),
23+
}
24+
export const checkOptionFocusManager = (event: KeyboardEvent) => {
25+
if (!Object.keys(eventByKey).includes(event.key)) {
26+
return
27+
}
28+
event.preventDefault()
29+
eventByKey[event.key]?.(event)
30+
}
31+
32+
const wiggle = keyframes`
33+
0% {
34+
rotate: 0deg;
35+
scale: 0;
36+
}
37+
25% {
38+
rotate: 10deg;
39+
}
40+
50% {
41+
rotate: -10deg;
42+
scale: 1;
43+
}
44+
75% {
45+
rotate: 10deg;
46+
}
47+
100% {
48+
rotate: 0deg;
49+
}
50+
`
51+
52+
const checkAnimation = css`
53+
animation: 500ms ${wiggle} ease-in-out forwards;
54+
`
55+
56+
interface CheckOptionProps extends Required<IconProp> {
57+
label: string
58+
active: boolean
59+
onClick: () => void
60+
}
61+
export const CheckOption = ({
62+
active,
63+
label,
64+
icon,
65+
onClick,
66+
}: CheckOptionProps) => (
67+
<label
68+
className={cn(
69+
interactive({ look: "ghost" }),
70+
hstack({ align: "center", justify: "between", gap: 2 }),
71+
"h-10 rounded-lg border-stroke-gentle pr-4 pl-2.5"
72+
)}
73+
>
74+
<button
75+
role="checkbox"
76+
aria-checked={active}
77+
onClick={onClick}
78+
className={cn(
79+
"relative inline-grid size-5 cursor-pointer place-content-center rounded-sm border-2 border-stroke-gentle",
80+
active && "border-stroke-invert"
81+
)}
82+
>
83+
{active && (
84+
<Icon
85+
icon={Check}
86+
size="xs"
87+
strokeWidth={4}
88+
color="default"
89+
className={cn(checkAnimation)}
90+
/>
91+
)}
92+
</button>
93+
<Icon icon={icon} size="md" />
94+
{label}
95+
</label>
96+
)

src/app/routes/settings/settings-general.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import {
99
TableProperties,
1010
} from "lucide-react"
1111

12+
import { routes } from "app/layout"
1213
import { Card } from "components/ui/card"
1314
import { Input } from "components/ui/input"
1415
import { Select } from "components/ui/select"
1516
import { preferencesData } from "data/preferences"
1617
import { useAtom } from "lib/yaasl"
18+
import { useTrans } from "locales/locale-provider"
1719
import { cn } from "utils/cn"
1820
import { vstack } from "utils/styles"
1921

22+
import { CheckOption, checkOptionFocusManager } from "./fragments/check-option"
2023
import { OrChain } from "./fragments/or-chain"
2124
import { RadioOption, radioOptionFocusManager } from "./fragments/radio-option"
2225

@@ -169,10 +172,46 @@ const SelectStyle = () => {
169172
)
170173
}
171174

175+
const HiddenRoutes = () => {
176+
const trans = useTrans()
177+
const { hiddenRoutes = [] } = useAtom(preferencesData)
178+
const hidableRoutes = routes.filter(({ to, href }) =>
179+
/search|stats|calendar|github/i.test(to ?? href)
180+
)
181+
return (
182+
<Card
183+
title={t`Hidden pages`}
184+
description={t`Hide optional pages from the navigation of the side-menu, if you don't use them.`}
185+
>
186+
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
187+
<div
188+
className="flex flex-row flex-wrap gap-2"
189+
onKeyDown={checkOptionFocusManager}
190+
>
191+
{hidableRoutes.map(({ title, icon, href, to }) => {
192+
const active = !hiddenRoutes.includes(href ?? to)
193+
return (
194+
<CheckOption
195+
key={href ?? to}
196+
icon={icon}
197+
label={trans(title)}
198+
active={active}
199+
onClick={() =>
200+
preferencesData.actions.toggleHiddenRoute(href ?? to, active)
201+
}
202+
/>
203+
)
204+
})}
205+
</div>
206+
</Card>
207+
)
208+
}
209+
172210
export const SettingsGeneral = () => (
173211
<div className={cn(vstack({ gap: 2 }))}>
174212
<Language />
175213
<Locale />
214+
<HiddenRoutes />
176215
<SummaryStyle />
177216
<SelectStyle />
178217
</div>

src/data/preferences/preferences.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createSlice, indexedDb, sync } from "lib/yaasl"
44
import { Resolve } from "types/util-types"
55

66
export const preferencesSchema = z.object({
7+
hiddenRoutes: z.optional(z.array(z.string())), // optional for legacy reasons
78
locale: z.string(),
89
language: z.string(),
910
summaryStyle: z.enum(["table", "grid"]),
@@ -23,6 +24,15 @@ export const preferencesData = createSlice({
2324
defaultValue,
2425
effects: [indexedDb(), sync()],
2526
reducers: {
27+
toggleHiddenRoute: (state, route: string, checked: boolean) => {
28+
const hiddenRoutes = (state.hiddenRoutes ?? []).filter(
29+
path => path !== route
30+
)
31+
return {
32+
...state,
33+
hiddenRoutes: checked ? [...hiddenRoutes, route] : hiddenRoutes,
34+
}
35+
},
2636
setLocale: (state, locale: string) => ({
2737
...state,
2838
locale: locale || "iso",

0 commit comments

Comments
 (0)