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-dom": "^18.2.0",
"react-dropdown-select": "^4.4.2",
"react-ga4": "^2.0.0",
"react-ga4": "^2.1.0",
"react-hotkeys-hook": "^3.2.1",
"react-use": "^17.4.0",
"react-use": "^17.4.2",
"recoil": "^0.7.6",
"recoil-sync": "^0.2.0",
"svg2png-converter": "^1.0.2",

View File

@@ -9,40 +9,33 @@ import Footer from "@/components/Footer";
import ErrorBoundary from "@/components/ErrorBoundary";
import Notice from "@/components/Notice";
// import Recipes from "@/components/Recipes";
import {
useIconParameters,
usePersistSettings,
useCSSVariables,
} from "@/hooks";
import { useCSSVariables } from "@/hooks";
import { isDarkThemeSelector } from "@/state";
const errorFallback = <Notice message="Search error" />;
const waitingFallback = <Notice type="none" message="" />;
const App: React.FC<any> = () => {
// useIconParameters();
// usePersistSettings();
const isDark = useRecoilValue(isDarkThemeSelector);
const properties = useMemo(
() => ({
"--foreground": isDark ? "white" : "var(--moss)",
"--foreground-card": isDark ? "white" : "var(--moss)",
"--foreground-secondary": isDark ? "var(--pewter)" : "var(--elephant)",
"--background": isDark ? "var(--slate)" : "var(--vellum)",
"--background-card": isDark ? "var(--stone)" : "var(--vellum)",
"--background-layer": isDark ? "var(--scrim)" : "var(--translucent)",
"--border-card": isDark ? "var(--shadow)" : "var(--moss-shadow)",
"--border-secondary": isDark ? "var(--scrim)" : "var(--moss-shadow)",
"--hover-tabs": isDark ? "var(--slate-sheer)" : "var(--ghost-sheer)",
"--hover-buttons": isDark ? "var(--scrim)" : "var(--slate)",
}),
[isDark]
useCSSVariables(
useMemo(
() => ({
"--foreground": isDark ? "white" : "var(--moss)",
"--foreground-card": isDark ? "white" : "var(--moss)",
"--foreground-secondary": isDark ? "var(--pewter)" : "var(--elephant)",
"--background": isDark ? "var(--slate)" : "var(--vellum)",
"--background-card": isDark ? "var(--stone)" : "var(--vellum)",
"--background-layer": isDark ? "var(--scrim)" : "var(--translucent)",
"--border-card": isDark ? "var(--shadow)" : "var(--moss-shadow)",
"--border-secondary": isDark ? "var(--scrim)" : "var(--moss-shadow)",
"--hover-tabs": isDark ? "var(--slate-sheer)" : "var(--ghost-sheer)",
"--hover-buttons": isDark ? "var(--scrim)" : "var(--slate)",
}),
[isDark]
)
);
useCSSVariables(properties);
return (
<Fragment>
<Header />

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { atom } from "recoil";
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 { IconEntry } from "@/lib";
@@ -11,7 +12,7 @@ export const searchQueryAtom = atom<string>({
syncEffect({
itemKey: "q",
refine: custom((q) => {
return (q as string) ?? "";
return (q as string).toString() ?? "";
}),
syncDefault: false,
}),
@@ -24,10 +25,21 @@ export const iconWeightAtom = atom<IconStyle>({
effects: [
syncEffect<IconStyle>({
itemKey: "weight",
refine: custom((w) => {
const isWeight = (w as string)?.toUpperCase?.() in IconStyle;
return isWeight ? (w as IconStyle) : IconStyle.REGULAR;
}, `Unrecognized weight`),
refine: stringLiterals({
thin: IconStyle.THIN,
light: IconStyle.LIGHT,
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,
}),
],
@@ -55,8 +67,22 @@ export const iconColorAtom = atom<string>({
syncEffect({
itemKey: "color",
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,
}),
],