feat(app): add persistence of settings across sessions
This commit is contained in:
@@ -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 = <Notice message="Search error" />;
|
||||
const waitingFallback = <Notice type="none" message="" />;
|
||||
|
||||
const App: React.FC<any> = () => {
|
||||
useIconParameters();
|
||||
usePersistSettings();
|
||||
|
||||
return (
|
||||
<React.StrictMode>
|
||||
|
||||
13
src/components/SettingsActions/SettingsActions.css
Normal file
13
src/components/SettingsActions/SettingsActions.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
53
src/components/SettingsActions/SettingsActions.tsx
Normal file
53
src/components/SettingsActions/SettingsActions.tsx
Normal file
@@ -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<boolean>(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 (
|
||||
<>
|
||||
<button
|
||||
className="action-button"
|
||||
title="Restore default settings"
|
||||
onClick={reset}
|
||||
>
|
||||
<ArrowCounterClockwise size={24} />
|
||||
</button>
|
||||
<button
|
||||
className="action-button"
|
||||
title="Copy URL for current settings"
|
||||
onClick={copyDeepLinkToClipboard}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCircle size={24} color="#1FA647" weight="fill" />
|
||||
) : (
|
||||
<Link size={24} />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsActions;
|
||||
@@ -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<ToolbarProps> = () => {
|
||||
<SearchInput />
|
||||
<SizeInput />
|
||||
<ColorInput />
|
||||
<SettingsActions />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
18
src/hooks/usePersistSettings.ts
Normal file
18
src/hooks/usePersistSettings.ts
Normal file
@@ -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]
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const iconWeightAtom = atom<IconStyle>({
|
||||
|
||||
export const iconSizeAtom = atom<number>({
|
||||
key: "iconSizeAtom",
|
||||
default: 48,
|
||||
default: 32,
|
||||
});
|
||||
|
||||
export const iconColorAtom = atom<string>({
|
||||
|
||||
@@ -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,7 +57,9 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
|
||||
IconCategory
|
||||
>({
|
||||
key: "singleCategoryQueryResultsSelector",
|
||||
get: (category: IconCategory) => ({ get }) => {
|
||||
get:
|
||||
(category: IconCategory) =>
|
||||
({ get }) => {
|
||||
const filteredResults = get(filteredQueryResultsSelector);
|
||||
return new Promise((resolve) =>
|
||||
resolve(
|
||||
@@ -66,3 +73,13 @@ export const isDarkThemeSelector = selector<boolean>({
|
||||
key: "isDarkThemeSelector",
|
||||
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
|
||||
});
|
||||
|
||||
export const resetSettingsSelector = selector<null>({
|
||||
key: "resetSettings",
|
||||
get: () => null,
|
||||
set: ({ reset }) => {
|
||||
reset(iconWeightAtom);
|
||||
reset(iconSizeAtom);
|
||||
reset(iconColorAtom);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user