feat(app): url persistence
This commit is contained in:
committed by
Tobias Fried
parent
6db9a08f7f
commit
b9e41ac135
@@ -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 />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user