From 14d8c9d0e764419c77ccbcc4f066d8c6f59e5c13 Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Fri, 26 Nov 2021 22:03:54 -0500 Subject: [PATCH] feat(app): add persistence of settings across sessions --- src/components/App/App.tsx | 2 + .../SettingsActions/SettingsActions.css | 13 +++++ .../SettingsActions/SettingsActions.tsx | 53 +++++++++++++++++++ src/components/Toolbar/Toolbar.tsx | 2 + src/hooks/useIconParameters.ts | 25 +++++++++ src/hooks/usePersistSettings.ts | 18 +++++++ src/state/atoms.ts | 2 +- src/state/selectors.ts | 35 ++++++++---- 8 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 src/components/SettingsActions/SettingsActions.css create mode 100644 src/components/SettingsActions/SettingsActions.tsx create mode 100644 src/hooks/usePersistSettings.ts diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 4bfd6e9..a5e3c0d 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -8,12 +8,14 @@ import Footer from "../Footer/Footer"; import ErrorBoundary from "../ErrorBoundary/ErrorBoundary"; import Notice from "../Notice/Notice"; import useIconParameters from "../../hooks/useIconParameters"; +import usePersistSettings from "../../hooks/usePersistSettings"; const errorFallback = ; const waitingFallback = ; const App: React.FC = () => { useIconParameters(); + usePersistSettings(); return ( diff --git a/src/components/SettingsActions/SettingsActions.css b/src/components/SettingsActions/SettingsActions.css new file mode 100644 index 0000000..540949c --- /dev/null +++ b/src/components/SettingsActions/SettingsActions.css @@ -0,0 +1,13 @@ +.action-button { + background-color: rgba(255, 255, 255, 0.05); + color: white; + padding: 8px; + border-radius: 8px; + cursor: pointer; +} + +@media screen and (max-width: 558px) { + .action-button { + display: none; + } +} diff --git a/src/components/SettingsActions/SettingsActions.tsx b/src/components/SettingsActions/SettingsActions.tsx new file mode 100644 index 0000000..18341b0 --- /dev/null +++ b/src/components/SettingsActions/SettingsActions.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { ArrowCounterClockwise, CheckCircle, Link } from "phosphor-react"; +import { useRecoilValue, useResetRecoilState } from "recoil"; +import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms"; +import "./SettingsActions.css"; +import useTransientState from "../../hooks/useTransientState"; +import { resetSettingsSelector } from "../../state/selectors"; + +const SettingsActions: React.FC = () => { + const weight = useRecoilValue(iconWeightAtom); + const size = useRecoilValue(iconSizeAtom); + const color = useRecoilValue(iconColorAtom); + const reset = useResetRecoilState(resetSettingsSelector); + + const [copied, setCopied] = useTransientState(false, 2000); + + const copyDeepLinkToClipboard = () => { + const paramString = new URLSearchParams([ + ["weight", weight.toString()], + ["size", size.toString()], + ["color", color.replace("#", "")], + ]).toString(); + void navigator.clipboard?.writeText( + `${window.location.host}?${paramString}` + ); + setCopied(true); + }; + + return ( + <> + + + + ); +}; + +export default SettingsActions; diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx index e5a939c..ac3c17f 100644 --- a/src/components/Toolbar/Toolbar.tsx +++ b/src/components/Toolbar/Toolbar.tsx @@ -5,6 +5,7 @@ import StyleInput from "../StyleInput/StyleInput"; import SearchInput from "../SearchInput/SearchInput"; import SizeInput from "../SizeInput/SizeInput"; import ColorInput from "../ColorInput/ColorInput"; +import SettingsActions from "../SettingsActions/SettingsActions"; type ToolbarProps = {}; @@ -16,6 +17,7 @@ const Toolbar: React.FC = () => { + ); diff --git a/src/hooks/useIconParameters.ts b/src/hooks/useIconParameters.ts index b2881bf..356e754 100644 --- a/src/hooks/useIconParameters.ts +++ b/src/hooks/useIconParameters.ts @@ -34,4 +34,29 @@ export default () => { if (normalizedColor.isValid()) setColor(normalizedColor.toHexString()); } }, [color, setColor]); + + useEffect(() => { + if (!weight && !size && !color) { + const persistedState = JSON.parse( + window.localStorage.getItem("__phosphor_settings__") || "null" + ); + + if (!!persistedState) { + const { weight, size, color } = persistedState; + if (weight) { + if (weight.toUpperCase() in IconStyle) setWeight(weight as IconStyle); + } + if (size) { + const normalizedSize = parseInt(size); + if (typeof normalizedSize === "number" && isFinite(normalizedSize)) + setSize(Math.min(Math.max(normalizedSize, 16), 96)); + } + if (color) { + const normalizedColor = TinyColor(color); + if (normalizedColor.isValid()) + setColor(normalizedColor.toHexString()); + } + } + } + }, []); }; diff --git a/src/hooks/usePersistSettings.ts b/src/hooks/usePersistSettings.ts new file mode 100644 index 0000000..689c01d --- /dev/null +++ b/src/hooks/usePersistSettings.ts @@ -0,0 +1,18 @@ +import { useRecoilValue } from "recoil"; +import useDebounce from "./useDebounce"; +import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../state/atoms"; + +export default function usePersistSettings() { + const weight = useRecoilValue(iconWeightAtom); + const size = useRecoilValue(iconSizeAtom); + const color = useRecoilValue(iconColorAtom); + + useDebounce( + () => { + const serializedState = JSON.stringify({ weight, size, color }); + window.localStorage.setItem("__phosphor_settings__", serializedState); + }, + 2000, + [weight, size, color] + ); +} diff --git a/src/state/atoms.ts b/src/state/atoms.ts index 3ec5a2e..ba6c643 100644 --- a/src/state/atoms.ts +++ b/src/state/atoms.ts @@ -13,7 +13,7 @@ export const iconWeightAtom = atom({ export const iconSizeAtom = atom({ key: "iconSizeAtom", - default: 48, + default: 32, }); export const iconColorAtom = atom({ diff --git a/src/state/selectors.ts b/src/state/selectors.ts index c76205a..bb6e3f3 100644 --- a/src/state/selectors.ts +++ b/src/state/selectors.ts @@ -2,7 +2,12 @@ import { selector, selectorFamily } from "recoil"; import TinyColor from "tinycolor2"; import Fuse from "fuse.js"; -import { searchQueryAtom, iconColorAtom } from "./atoms"; +import { + searchQueryAtom, + iconWeightAtom, + iconSizeAtom, + iconColorAtom, +} from "./atoms"; import { IconEntry, IconCategory } from "../lib"; import { icons } from "../lib/icons"; @@ -52,17 +57,29 @@ export const singleCategoryQueryResultsSelector = selectorFamily< IconCategory >({ key: "singleCategoryQueryResultsSelector", - get: (category: IconCategory) => ({ get }) => { - const filteredResults = get(filteredQueryResultsSelector); - return new Promise((resolve) => - resolve( - filteredResults.filter((icon) => icon.categories.includes(category)) - ) - ); - }, + get: + (category: IconCategory) => + ({ get }) => { + const filteredResults = get(filteredQueryResultsSelector); + return new Promise((resolve) => + resolve( + filteredResults.filter((icon) => icon.categories.includes(category)) + ) + ); + }, }); export const isDarkThemeSelector = selector({ key: "isDarkThemeSelector", get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(), }); + +export const resetSettingsSelector = selector({ + key: "resetSettings", + get: () => null, + set: ({ reset }) => { + reset(iconWeightAtom); + reset(iconSizeAtom); + reset(iconColorAtom); + }, +});