feat(app): url persistence

This commit is contained in:
rektdeckard
2024-01-04 19:39:05 -07:00
committed by Tobias Fried
parent 6db9a08f7f
commit b9e41ac135
7 changed files with 86 additions and 58 deletions

View File

@@ -37,9 +37,9 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropdown-select": "^4.4.2", "react-dropdown-select": "^4.4.2",
"react-ga4": "^2.0.0", "react-ga4": "^2.1.0",
"react-hotkeys-hook": "^3.2.1", "react-hotkeys-hook": "^3.2.1",
"react-use": "^17.4.0", "react-use": "^17.4.2",
"recoil": "^0.7.6", "recoil": "^0.7.6",
"recoil-sync": "^0.2.0", "recoil-sync": "^0.2.0",
"svg2png-converter": "^1.0.2", "svg2png-converter": "^1.0.2",

View File

@@ -9,23 +9,17 @@ import Footer from "@/components/Footer";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import Notice from "@/components/Notice"; import Notice from "@/components/Notice";
// import Recipes from "@/components/Recipes"; // import Recipes from "@/components/Recipes";
import { import { useCSSVariables } from "@/hooks";
useIconParameters,
usePersistSettings,
useCSSVariables,
} from "@/hooks";
import { isDarkThemeSelector } from "@/state"; import { isDarkThemeSelector } from "@/state";
const errorFallback = <Notice message="Search error" />; const errorFallback = <Notice message="Search error" />;
const waitingFallback = <Notice type="none" message="" />; const waitingFallback = <Notice type="none" message="" />;
const App: React.FC<any> = () => { const App: React.FC<any> = () => {
// useIconParameters();
// usePersistSettings();
const isDark = useRecoilValue(isDarkThemeSelector); const isDark = useRecoilValue(isDarkThemeSelector);
const properties = useMemo( useCSSVariables(
useMemo(
() => ({ () => ({
"--foreground": isDark ? "white" : "var(--moss)", "--foreground": isDark ? "white" : "var(--moss)",
"--foreground-card": isDark ? "white" : "var(--moss)", "--foreground-card": isDark ? "white" : "var(--moss)",
@@ -39,10 +33,9 @@ const App: React.FC<any> = () => {
"--hover-buttons": isDark ? "var(--scrim)" : "var(--slate)", "--hover-buttons": isDark ? "var(--scrim)" : "var(--slate)",
}), }),
[isDark] [isDark]
)
); );
useCSSVariables(properties);
return ( return (
<Fragment> <Fragment>
<Header /> <Header />

View File

@@ -1,4 +1,5 @@
import { Component, ErrorInfo, ReactNode } from "react"; import { Component, ErrorInfo, ReactNode } from "react";
import ReactGA from "react-ga4";
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
fallback?: JSX.Element | ReactNode; fallback?: JSX.Element | ReactNode;
@@ -23,8 +24,16 @@ export default class ErrorBoundary extends Component<
} }
componentDidCatch(error: any, info: ErrorInfo) { componentDidCatch(error: any, info: ErrorInfo) {
void error; console.error(error);
console.info(info); ReactGA.event("exception", {
description: JSON.stringify({
error:
error instanceof Error
? error.message
: error.toString?.() ?? "UNSERIALIZEABLE",
info,
}),
});
} }
render(): JSX.Element | ReactNode { render(): JSX.Element | ReactNode {

View File

@@ -52,7 +52,7 @@ const SearchInput = (_: SearchInputProps) => {
}, [query]); }, [query]);
/* eslint-enable react-hooks/exhaustive-deps */ /* eslint-enable react-hooks/exhaustive-deps */
const [isReady] = useDebounce( const [_isReady, _cancel] = useDebounce(
() => { () => {
if (value !== query) { if (value !== query) {
setQuery(value); setQuery(value);
@@ -95,7 +95,7 @@ const SearchInput = (_: SearchInputProps) => {
/> />
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>} {!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
{value ? ( {value ? (
isReady() ? ( value === query ? (
<X className="clear-icon" size={18} onClick={handleCancelSearch} /> <X className="clear-icon" size={18} onClick={handleCancelSearch} />
) : ( ) : (
<HourglassHigh className="wait-icon" weight="fill" size={18} /> <HourglassHigh className="wait-icon" weight="fill" size={18} />

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { useRecoilState, useResetRecoilState } from "recoil"; import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
import { import {
ArrowCounterClockwise, ArrowCounterClockwise,
CheckCircle, CheckCircle,
@@ -21,21 +21,16 @@ import "./SettingsActions.css";
const SettingsActions = () => { const SettingsActions = () => {
const [weight, setWeight] = useRecoilState(iconWeightAtom); const [weight, setWeight] = useRecoilState(iconWeightAtom);
const [size, setSize] = useRecoilState(iconSizeAtom); const setSize = useSetRecoilState(iconSizeAtom);
const [color, setColor] = useRecoilState(iconColorAtom); const setColor = useSetRecoilState(iconColorAtom);
const reset = useResetRecoilState(resetSettingsSelector); const reset = useResetRecoilState(resetSettingsSelector);
const [copied, setCopied] = useTransientState<boolean>(false, 2000); const [copied, setCopied] = useTransientState<boolean>(false, 2000);
const [booped, setBooped] = useState<boolean>(false); const [booped, setBooped] = useState<boolean>(false);
const copyDeepLinkToClipboard = () => { const copyDeepLinkToClipboard = () => {
const paramString = new URLSearchParams([
["weight", weight.toString()],
["size", size.toString()],
["color", color.replace("#", "")],
]).toString();
void navigator.clipboard void navigator.clipboard
?.writeText(`${window.location.host}?${paramString}`) ?.writeText(`${window.location.origin}${window.location.search}`)
.then(() => { .then(() => {
setCopied(true); setCopied(true);
}) })

View File

@@ -1,10 +1,11 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { RecoilRoot } from "recoil"; import { RecoilRoot } from "recoil";
import { RecoilURLSync } from "recoil-sync"; import { RecoilURLSyncJSON } from "recoil-sync";
import App from "./components/App"; import App from "./components/App";
import RecoilSyncLocalStorage from "./state/RecoilSyncLocalStorage";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import ErrorBoundary from "./components/ErrorBoundary";
import Notice from "./components/Notice";
const GA_MEASUREMENT_ID = "G-1C1REQCLFB"; const GA_MEASUREMENT_ID = "G-1C1REQCLFB";
ReactGA.initialize(GA_MEASUREMENT_ID); ReactGA.initialize(GA_MEASUREMENT_ID);
@@ -15,18 +16,22 @@ const root = createRoot(container!);
root.render( root.render(
<StrictMode> <StrictMode>
<RecoilRoot> <RecoilRoot>
<RecoilURLSync <ErrorBoundary
location={{ part: "queryParams" }} fallback={
serialize={(data) => { <Notice
console.log(data); message={
return ""; <p>
}} An error occurred. Try going{" "}
deserialize={() => {}} <a href={window.location.origin}>home</a>.
</p>
}
/>
}
> >
<RecoilSyncLocalStorage> <RecoilURLSyncJSON location={{ part: "queryParams" }}>
<App /> <App />
</RecoilSyncLocalStorage> </RecoilURLSyncJSON>
</RecoilURLSync> </ErrorBoundary>
</RecoilRoot> </RecoilRoot>
</StrictMode> </StrictMode>
); );

View File

@@ -1,6 +1,7 @@
import { atom } from "recoil"; import { atom } from "recoil";
import { syncEffect } from "recoil-sync"; import { syncEffect } from "recoil-sync";
import { custom, number, string, stringLiterals } from "@recoiljs/refine"; import TinyColor from "tinycolor2";
import { custom, stringLiterals } from "@recoiljs/refine";
import { IconStyle } from "@phosphor-icons/core"; import { IconStyle } from "@phosphor-icons/core";
import { IconEntry } from "@/lib"; import { IconEntry } from "@/lib";
@@ -11,7 +12,7 @@ export const searchQueryAtom = atom<string>({
syncEffect({ syncEffect({
itemKey: "q", itemKey: "q",
refine: custom((q) => { refine: custom((q) => {
return (q as string) ?? ""; return (q as string).toString() ?? "";
}), }),
syncDefault: false, syncDefault: false,
}), }),
@@ -24,10 +25,21 @@ export const iconWeightAtom = atom<IconStyle>({
effects: [ effects: [
syncEffect<IconStyle>({ syncEffect<IconStyle>({
itemKey: "weight", itemKey: "weight",
refine: custom((w) => { refine: stringLiterals({
const isWeight = (w as string)?.toUpperCase?.() in IconStyle; thin: IconStyle.THIN,
return isWeight ? (w as IconStyle) : IconStyle.REGULAR; light: IconStyle.LIGHT,
}, `Unrecognized weight`), regular: IconStyle.REGULAR,
bold: IconStyle.BOLD,
fill: IconStyle.FILL,
duotone: IconStyle.DUOTONE,
}),
write: (atom, w) => {
if (typeof w === "string") {
atom.write("weight", w);
} else {
atom.reset("weight");
}
},
syncDefault: false, syncDefault: false,
}), }),
], ],
@@ -55,8 +67,22 @@ export const iconColorAtom = atom<string>({
syncEffect({ syncEffect({
itemKey: "color", itemKey: "color",
refine: custom((c) => { refine: custom((c) => {
return (c as string) ?? "#000000"; if (typeof c === "string") {
const normalizedColor = TinyColor(c);
if (normalizedColor.isValid()) {
return normalizedColor.toHexString();
}
}
return "#000000";
}), }),
write: (atom, c) => {
if (typeof c === "string") {
const color = c.replace("#", "");
atom.write("color", color);
} else {
atom.reset("color");
}
},
syncDefault: false, syncDefault: false,
}), }),
], ],