mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-09-06 03:35:32 -04:00
more optimization/error handling
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import useSWR from "swr";
|
||||
import Loading from "../Loading";
|
||||
import { fetcher } from "../../lib/helpers/fetcher";
|
||||
import { siteLocale } from "../../lib/config";
|
||||
import { commafy } from "../../lib/helpers/format-number";
|
||||
|
||||
export type HitCounterProps = {
|
||||
slug: string;
|
||||
@@ -15,29 +15,22 @@ const HitCounter = ({ slug, className }: HitCounterProps) => {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
try {
|
||||
// show spinning loading indicator if data isn't fetched yet
|
||||
if (!data) {
|
||||
return <Loading boxes={3} width={20} />;
|
||||
}
|
||||
// show spinning loading indicator if data isn't fetched yet
|
||||
if (!data) {
|
||||
return <Loading boxes={3} width={20} />;
|
||||
}
|
||||
|
||||
// fail secretly
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// we have data!
|
||||
return (
|
||||
<span
|
||||
title={`${data.hits.toLocaleString(siteLocale)} ${data.hits === 1 ? "view" : "views"}`}
|
||||
className={className}
|
||||
>
|
||||
{data.hits.toLocaleString(siteLocale)}
|
||||
</span>
|
||||
);
|
||||
} catch (error) {
|
||||
// fail secretly
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// we have data!
|
||||
return (
|
||||
<span title={`${commafy(data.hits)} ${data.hits === 1 ? "view" : "views"}`} className={className}>
|
||||
{commafy(data.hits)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default HitCounter;
|
||||
|
||||
@@ -42,6 +42,8 @@ const Loading = ({ width, boxes = 3, timing = 0.1, css, ...rest }: LoadingProps)
|
||||
key={i}
|
||||
css={{
|
||||
width: `${width / (boxes + 1)}px`,
|
||||
}}
|
||||
style={{
|
||||
animationDelay: `${i * timing}s`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import Link from "next/link";
|
||||
import Time from "../Time";
|
||||
import HitCounter from "../HitCounter";
|
||||
@@ -108,15 +109,17 @@ const NoteMeta = ({ slug, date, title, htmlTitle, tags = [] }: NoteMetaProps) =>
|
||||
|
||||
{/* only count hits on production site */}
|
||||
{process.env.NEXT_PUBLIC_VERCEL_ENV === "production" && (
|
||||
<MetaItem
|
||||
// fix potential layout shift when number of hits loads
|
||||
css={{ minWidth: "7em", marginRight: 0 }}
|
||||
>
|
||||
<span>
|
||||
<Icon as={ViewsIcon} />
|
||||
</span>
|
||||
<HitCounter slug={`notes/${slug}`} />
|
||||
</MetaItem>
|
||||
<ErrorBoundary fallback={null}>
|
||||
<MetaItem
|
||||
// fix potential layout shift when number of hits loads
|
||||
css={{ minWidth: "7em", marginRight: 0 }}
|
||||
>
|
||||
<span>
|
||||
<Icon as={ViewsIcon} />
|
||||
</span>
|
||||
<HitCounter slug={`notes/${slug}`} />
|
||||
</MetaItem>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</Wrapper>
|
||||
|
||||
|
||||
@@ -3,17 +3,20 @@ import { formatDateTZ, formatDateISO, formatTimeAgo, FlexibleDate } from "../../
|
||||
|
||||
export type RelativeTimeProps = {
|
||||
date: FlexibleDate;
|
||||
prefix?: string; // optional "Updated", "Published", "Created", etc.
|
||||
staticFormat?: string; // full date (without timestamp)
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const RelativeTime = ({ date, className }: RelativeTimeProps) => {
|
||||
const RelativeTime = ({ date, prefix, staticFormat = "PP", className }: RelativeTimeProps) => {
|
||||
// play nice with SSR -- only use relative time on the client, since it'll quickly become outdated on the server and
|
||||
// cause a react hydration mismatch error.
|
||||
const hasMounted = useHasMounted();
|
||||
|
||||
return (
|
||||
<time dateTime={formatDateISO(date)} title={formatDateTZ(date)} className={className}>
|
||||
Updated {hasMounted ? formatTimeAgo(date) : `on ${formatDateTZ(date, "PP")}`}
|
||||
{prefix && `${prefix} `}
|
||||
{hasMounted ? formatTimeAgo(date) : `on ${formatDateTZ(date, staticFormat)}`}
|
||||
</time>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Link from "../Link";
|
||||
import RelativeTime from "../RelativeTime";
|
||||
import { StarOcticon, ForkOcticon } from "../Icons";
|
||||
import { commafy } from "../../lib/helpers/format-number";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import { siteLocale } from "../../lib/config";
|
||||
import type { RepositoryType } from "../../types";
|
||||
|
||||
const Wrapper = styled("div", {
|
||||
@@ -100,12 +100,12 @@ const RepositoryCard = ({
|
||||
<MetaItem>
|
||||
<MetaLink
|
||||
href={`${url}/stargazers`}
|
||||
title={`${stars.toLocaleString(siteLocale)} ${stars === 1 ? "star" : "stars"}`}
|
||||
title={`${commafy(stars)} ${stars === 1 ? "star" : "stars"}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<MetaIcon as={StarOcticon} />
|
||||
<span>{stars.toLocaleString(siteLocale)}</span>
|
||||
<span>{commafy(stars)}</span>
|
||||
</MetaLink>
|
||||
</MetaItem>
|
||||
)}
|
||||
@@ -114,19 +114,19 @@ const RepositoryCard = ({
|
||||
<MetaItem>
|
||||
<MetaLink
|
||||
href={`${url}/network/members`}
|
||||
title={`${forks.toLocaleString(siteLocale)} ${forks === 1 ? "fork" : "forks"}`}
|
||||
title={`${commafy(forks)} ${forks === 1 ? "fork" : "forks"}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<MetaIcon as={ForkOcticon} />
|
||||
<span>{forks.toLocaleString(siteLocale)}</span>
|
||||
<span>{commafy(forks)}</span>
|
||||
</MetaLink>
|
||||
</MetaItem>
|
||||
)}
|
||||
|
||||
{/* only use relative "time ago" on client side, since it'll be outdated via SSG and cause hydration errors */}
|
||||
<MetaItem>
|
||||
<RelativeTime date={updatedAt} />
|
||||
<RelativeTime date={updatedAt} prefix="Updated" />
|
||||
</MetaItem>
|
||||
</Meta>
|
||||
</Wrapper>
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import { memo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { minify } from "uglify-js";
|
||||
import { clientScript } from "./script";
|
||||
import { darkModeQuery, themeStorageKey, themeClassNames } from "../../lib/config/themes";
|
||||
|
||||
const ThemeScript = () => {
|
||||
// since the function above will end up being injected as a plain dumb string, we need to set the dynamic values here:
|
||||
const functionString = String(clientScript)
|
||||
.replace('"__MEDIA_QUERY__"', `"${darkModeQuery}"`)
|
||||
.replace('"__STORAGE_KEY__"', `"${themeStorageKey}"`)
|
||||
.replace('"__CLASS_NAMES__"', JSON.stringify(themeClassNames))
|
||||
.replace(
|
||||
'"__LIST_OF_CLASSES__"',
|
||||
Object.values(themeClassNames)
|
||||
.map((t: string) => `"${t}"`)
|
||||
.join(",")
|
||||
);
|
||||
const minified = useMemo(() => {
|
||||
// since the client function will end up being injected as a plain dumb string, we need to set dynamic values here:
|
||||
const functionString = String(clientScript)
|
||||
.replace('"__MEDIA_QUERY__"', `"${darkModeQuery}"`)
|
||||
.replace('"__STORAGE_KEY__"', `"${themeStorageKey}"`)
|
||||
.replace('"__CLASS_NAMES__"', JSON.stringify(themeClassNames))
|
||||
.replace(
|
||||
'"__LIST_OF_CLASSES__"',
|
||||
Object.values(themeClassNames)
|
||||
.map((t) => `"${t}"`)
|
||||
.join(",")
|
||||
);
|
||||
|
||||
// minify the final code, a bit hacky but this is ONLY done at build-time, so uglify-js is never bundled or sent to
|
||||
// the browser to execute:
|
||||
const minified = minify(`(${functionString})()`, {
|
||||
toplevel: true,
|
||||
compress: {
|
||||
negate_iife: false,
|
||||
},
|
||||
parse: {
|
||||
bare_returns: true,
|
||||
},
|
||||
}).code;
|
||||
// minify the final code, a bit hacky but this is ONLY done at build-time, so uglify-js is never bundled or sent to
|
||||
// the browser to execute:
|
||||
return minify(`(${functionString})()`, {
|
||||
toplevel: true,
|
||||
compress: {
|
||||
negate_iife: false,
|
||||
},
|
||||
parse: {
|
||||
bare_returns: true,
|
||||
},
|
||||
}).code;
|
||||
}, []);
|
||||
|
||||
// the script tag injected manually into `<head>` in _document.tsx.
|
||||
// even though it's the proper method, using next/script with `strategy="beforeInteractive"` still causes flash of
|
||||
@@ -36,10 +38,10 @@ const ThemeScript = () => {
|
||||
key="restore-theme"
|
||||
dangerouslySetInnerHTML={{
|
||||
// make it an IIFE:
|
||||
__html: `(function(){${minified}})();`,
|
||||
__html: `(function(){${minified}})()`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ThemeScript);
|
||||
export default ThemeScript;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable no-empty, no-var, prefer-destructuring */
|
||||
|
||||
// this function is converted to a string verbatim, substitutions are made to insert dynamic values, minified, and then
|
||||
// finally exported as an inline `<script>` tag in ThemeScript.tsx for pages/_document.tsx to use.
|
||||
// finally exported as an inline `<script>` tag in ThemeScript.tsx for _document.tsx to use.
|
||||
export const clientScript = () => {
|
||||
// `try/catch` in case I messed something up here bigly... (will default to light theme)
|
||||
try {
|
||||
@@ -9,9 +9,9 @@ export const clientScript = () => {
|
||||
var pref = localStorage.getItem("__STORAGE_KEY__");
|
||||
// map of theme -> classname:
|
||||
var classNames = "__CLASS_NAMES__";
|
||||
// the list of <html>'s current class(es), from which `classNames` are removed to start fresh
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
// the list of <html>'s current class(es)...
|
||||
var classList = document.documentElement.classList;
|
||||
// ...from which `classNames` are removed to start fresh:
|
||||
classList.remove("__LIST_OF_CLASSES__");
|
||||
|
||||
if (pref === "light" || pref === "dark") {
|
||||
@@ -23,5 +23,5 @@ export const clientScript = () => {
|
||||
var prefersDark = window.matchMedia(darkQuery);
|
||||
classList.add(classNames[prefersDark.media !== darkQuery || prefersDark.matches ? "dark" : "light"]);
|
||||
}
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user