feat(app): url persistence
This commit is contained in:
committed by
Tobias Fried
parent
6db9a08f7f
commit
b9e41ac135
@@ -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",
|
||||||
|
|||||||
@@ -9,40 +9,33 @@ 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-card": isDark ? "white" : "var(--moss)",
|
"--foreground": isDark ? "white" : "var(--moss)",
|
||||||
"--foreground-secondary": isDark ? "var(--pewter)" : "var(--elephant)",
|
"--foreground-card": isDark ? "white" : "var(--moss)",
|
||||||
"--background": isDark ? "var(--slate)" : "var(--vellum)",
|
"--foreground-secondary": isDark ? "var(--pewter)" : "var(--elephant)",
|
||||||
"--background-card": isDark ? "var(--stone)" : "var(--vellum)",
|
"--background": isDark ? "var(--slate)" : "var(--vellum)",
|
||||||
"--background-layer": isDark ? "var(--scrim)" : "var(--translucent)",
|
"--background-card": isDark ? "var(--stone)" : "var(--vellum)",
|
||||||
"--border-card": isDark ? "var(--shadow)" : "var(--moss-shadow)",
|
"--background-layer": isDark ? "var(--scrim)" : "var(--translucent)",
|
||||||
"--border-secondary": isDark ? "var(--scrim)" : "var(--moss-shadow)",
|
"--border-card": isDark ? "var(--shadow)" : "var(--moss-shadow)",
|
||||||
"--hover-tabs": isDark ? "var(--slate-sheer)" : "var(--ghost-sheer)",
|
"--border-secondary": isDark ? "var(--scrim)" : "var(--moss-shadow)",
|
||||||
"--hover-buttons": isDark ? "var(--scrim)" : "var(--slate)",
|
"--hover-tabs": isDark ? "var(--slate-sheer)" : "var(--ghost-sheer)",
|
||||||
}),
|
"--hover-buttons": isDark ? "var(--scrim)" : "var(--slate)",
|
||||||
[isDark]
|
}),
|
||||||
|
[isDark]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
useCSSVariables(properties);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user