2020-04-23 13:31:39 -07:00
|
|
|
import React from "react";
|
2020-04-24 21:17:03 -07:00
|
|
|
import { Box, Heading } from "@chakra-ui/core";
|
2020-04-23 13:31:39 -07:00
|
|
|
|
2020-04-26 00:46:05 -07:00
|
|
|
/**
|
|
|
|
* Delay hides its content and first, then shows it after the given delay.
|
|
|
|
*
|
|
|
|
* This is useful for loading states: it can be disruptive to see a spinner or
|
|
|
|
* skeleton element for only a brief flash, we'd rather just show them if
|
|
|
|
* loading is genuinely taking a while!
|
|
|
|
*
|
|
|
|
* 300ms is a pretty good default: that's about when perception shifts from "it
|
|
|
|
* wasn't instant" to "the process took time".
|
|
|
|
* https://developers.google.com/web/fundamentals/performance/rail
|
|
|
|
*/
|
2020-04-23 23:43:39 -07:00
|
|
|
export function Delay({ children, ms = 300 }) {
|
2020-04-23 13:31:39 -07:00
|
|
|
const [isVisible, setIsVisible] = React.useState(false);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
const id = setTimeout(() => setIsVisible(true), ms);
|
|
|
|
return () => clearTimeout(id);
|
|
|
|
}, [ms, setIsVisible]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Box opacity={isVisible ? 1 : 0} transition="opacity 0.5s">
|
|
|
|
{children}
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
2020-04-24 21:17:03 -07:00
|
|
|
|
2020-04-26 00:46:05 -07:00
|
|
|
/**
|
|
|
|
* Heading1 is a large, page-title-ish heading, with our DTI-brand-y Delicious
|
|
|
|
* font and some special typographical styles!
|
|
|
|
*/
|
2020-04-24 21:17:03 -07:00
|
|
|
export function Heading1({ children, ...props }) {
|
|
|
|
return (
|
2020-05-18 00:56:46 -07:00
|
|
|
<Heading
|
2020-08-12 00:37:31 -07:00
|
|
|
size="2xl"
|
2020-05-18 00:56:46 -07:00
|
|
|
fontFamily="Delicious, sans-serif"
|
|
|
|
fontWeight="800"
|
|
|
|
{...props}
|
|
|
|
>
|
2020-04-24 21:17:03 -07:00
|
|
|
{children}
|
|
|
|
</Heading>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-26 00:46:05 -07:00
|
|
|
/**
|
|
|
|
* Heading2 is a major subheading, with our DTI-brand-y Delicious font and some
|
|
|
|
* special typographical styles!!
|
|
|
|
*/
|
2020-04-24 21:17:03 -07:00
|
|
|
export function Heading2({ children, ...props }) {
|
|
|
|
return (
|
2020-05-18 00:56:46 -07:00
|
|
|
<Heading
|
|
|
|
size="xl"
|
|
|
|
fontFamily="Delicious, sans-serif"
|
|
|
|
fontWeight="700"
|
|
|
|
{...props}
|
|
|
|
>
|
2020-04-24 21:17:03 -07:00
|
|
|
{children}
|
|
|
|
</Heading>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-02 15:41:02 -07:00
|
|
|
/**
|
|
|
|
* safeImageUrl returns an HTTPS-safe image URL for Neopets assets!
|
|
|
|
*/
|
|
|
|
export function safeImageUrl(url) {
|
2020-09-22 03:03:01 -07:00
|
|
|
let safeUrl = `/api/assetProxy?url=${encodeURIComponent(url)}`;
|
|
|
|
|
|
|
|
// On our Storybook server, we need to request from the main dev server.
|
|
|
|
const { host } = document.location;
|
|
|
|
if (host === "localhost:6006") {
|
|
|
|
safeUrl = "http://localhost:3000" + safeUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
return safeUrl;
|
2020-05-02 15:41:02 -07:00
|
|
|
}
|
|
|
|
|
2020-04-26 00:46:05 -07:00
|
|
|
/**
|
|
|
|
* useDebounce helps make a rapidly-changing value change less! It waits for a
|
|
|
|
* pause in the incoming data before outputting the latest value.
|
|
|
|
*
|
|
|
|
* We use it in search: when the user types rapidly, we don't want to update
|
|
|
|
* our query and send a new request every keystroke. We want to wait for it to
|
|
|
|
* seem like they might be done, while still feeling responsive!
|
|
|
|
*
|
|
|
|
* Adapted from https://usehooks.com/useDebounce/
|
|
|
|
*/
|
2020-09-01 19:53:38 -07:00
|
|
|
export function useDebounce(
|
|
|
|
value,
|
|
|
|
delay,
|
|
|
|
{ waitForFirstPause = false, initialValue = null } = {}
|
|
|
|
) {
|
2020-04-24 21:17:03 -07:00
|
|
|
// State and setters for debounced value
|
2020-09-01 19:53:38 -07:00
|
|
|
const [debouncedValue, setDebouncedValue] = React.useState(
|
|
|
|
waitForFirstPause ? initialValue : value
|
|
|
|
);
|
2020-04-24 21:17:03 -07:00
|
|
|
|
|
|
|
React.useEffect(
|
|
|
|
() => {
|
|
|
|
// Update debounced value after delay
|
|
|
|
const handler = setTimeout(() => {
|
|
|
|
setDebouncedValue(value);
|
|
|
|
}, delay);
|
|
|
|
|
|
|
|
// Cancel the timeout if value changes (also on delay change or unmount)
|
|
|
|
// This is how we prevent debounced value from updating if value is changed ...
|
|
|
|
// .. within the delay period. Timeout gets cleared and restarted.
|
|
|
|
return () => {
|
|
|
|
clearTimeout(handler);
|
|
|
|
};
|
|
|
|
},
|
|
|
|
[value, delay] // Only re-call effect if value or delay changes
|
|
|
|
);
|
|
|
|
|
|
|
|
return debouncedValue;
|
|
|
|
}
|
2020-05-17 23:26:00 -07:00
|
|
|
|
2020-05-17 23:44:33 -07:00
|
|
|
/**
|
|
|
|
* usePageTitle sets the page title!
|
|
|
|
*/
|
2020-09-12 19:29:20 -07:00
|
|
|
export function usePageTitle(title, { skip = false } = {}) {
|
2020-05-17 23:26:00 -07:00
|
|
|
React.useEffect(() => {
|
2020-09-12 19:29:20 -07:00
|
|
|
if (skip) return;
|
2020-09-11 23:53:57 -07:00
|
|
|
document.title = title ? `${title} | Dress to Impress` : "Dress to Impress";
|
2020-09-12 19:29:20 -07:00
|
|
|
}, [title, skip]);
|
2020-05-17 23:26:00 -07:00
|
|
|
}
|
2020-05-17 23:44:33 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* useFetch uses `fetch` to fetch the given URL, and returns the request state.
|
|
|
|
*
|
|
|
|
* Our limited API is designed to match the `use-http` library!
|
|
|
|
*/
|
|
|
|
export function useFetch(url, { responseType }) {
|
|
|
|
// Just trying to be clear about what you'll get back ^_^` If we want to
|
|
|
|
// fetch non-binary data later, extend this and get something else from res!
|
|
|
|
if (responseType !== "arrayBuffer") {
|
|
|
|
throw new Error(`unsupported responseType ${responseType}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const [loading, setLoading] = React.useState(true);
|
|
|
|
const [error, setError] = React.useState(null);
|
|
|
|
const [data, setData] = React.useState(null);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
let canceled = false;
|
|
|
|
|
|
|
|
fetch(url)
|
|
|
|
.then(async (res) => {
|
|
|
|
if (canceled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const arrayBuffer = await res.arrayBuffer();
|
|
|
|
setLoading(false);
|
|
|
|
setError(null);
|
|
|
|
setData(arrayBuffer);
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
if (canceled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setLoading(false);
|
|
|
|
setError(error);
|
|
|
|
setData(null);
|
|
|
|
});
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
canceled = true;
|
|
|
|
};
|
|
|
|
}, [url]);
|
|
|
|
|
|
|
|
return { loading, error, data };
|
|
|
|
}
|
2020-08-28 22:58:39 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* useLocalStorage is like React.useState, but it persists the value in the
|
|
|
|
* device's `localStorage`, so it comes back even after reloading the page.
|
|
|
|
*
|
|
|
|
* Adapted from https://usehooks.com/useLocalStorage/.
|
|
|
|
*/
|
2020-09-22 05:39:48 -07:00
|
|
|
let storageListeners = [];
|
2020-08-28 22:58:39 -07:00
|
|
|
export function useLocalStorage(key, initialValue) {
|
2020-09-22 05:39:48 -07:00
|
|
|
const loadValue = React.useCallback(() => {
|
2020-08-28 22:58:39 -07:00
|
|
|
try {
|
|
|
|
const item = window.localStorage.getItem(key);
|
|
|
|
return item ? JSON.parse(item) : initialValue;
|
|
|
|
} catch (error) {
|
|
|
|
console.log(error);
|
|
|
|
return initialValue;
|
|
|
|
}
|
2020-09-22 05:39:48 -07:00
|
|
|
}, [key, initialValue]);
|
|
|
|
|
|
|
|
const [storedValue, setStoredValue] = React.useState(loadValue);
|
2020-08-28 22:58:39 -07:00
|
|
|
|
|
|
|
const setValue = (value) => {
|
|
|
|
try {
|
|
|
|
setStoredValue(value);
|
|
|
|
window.localStorage.setItem(key, JSON.stringify(value));
|
2020-09-22 05:39:48 -07:00
|
|
|
storageListeners.forEach((l) => l());
|
2020-08-28 22:58:39 -07:00
|
|
|
} catch (error) {
|
|
|
|
console.log(error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-09-22 05:39:48 -07:00
|
|
|
const reloadValue = React.useCallback(() => {
|
|
|
|
setStoredValue(loadValue());
|
|
|
|
}, [loadValue, setStoredValue]);
|
|
|
|
|
|
|
|
// Listen for changes elsewhere on the page, and update here too!
|
|
|
|
React.useEffect(() => {
|
|
|
|
storageListeners.push(reloadValue);
|
|
|
|
return () => {
|
|
|
|
storageListeners = storageListeners.filter((l) => l !== reloadValue);
|
|
|
|
};
|
|
|
|
}, [reloadValue]);
|
|
|
|
|
|
|
|
// Listen for changes in other tabs, and update here too! (This does not
|
|
|
|
// catch same-page updates!)
|
|
|
|
React.useEffect(() => {
|
|
|
|
window.addEventListener("storage", reloadValue);
|
|
|
|
return () => window.removeEventListener("storage", reloadValue);
|
|
|
|
}, [reloadValue]);
|
|
|
|
|
2020-08-28 22:58:39 -07:00
|
|
|
return [storedValue, setValue];
|
|
|
|
}
|