1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 09:45:22 -04:00

more optimization/error handling

This commit is contained in:
Jake Jarvis 2022-04-22 09:18:53 -04:00
parent 831139c132
commit 8a97b706be
Signed by: jake
GPG Key ID: 2B0C9CF251E69A39
12 changed files with 93 additions and 72 deletions

View File

@ -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;

View File

@ -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`,
}}
/>

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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;

View File

@ -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) {}
};

View File

@ -0,0 +1,5 @@
import { siteLocale } from "../config";
// adds thousands separator
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
export const commafy = Intl.NumberFormat(siteLocale, { useGrouping: true }).format;

View File

@ -54,6 +54,7 @@
"query-string": "^7.1.1",
"react": "18.0.0",
"react-dom": "18.0.0",
"react-error-boundary": "^3.1.4",
"react-gist": "^1.2.4",
"react-innertext": "^1.1.5",
"react-intersection-observer": "^9.0.0",

View File

@ -2,7 +2,6 @@ import { NextSeo } from "next-seo";
import Content from "../../components/Content";
import NotesList, { NotesListProps } from "../../components/NotesList";
import { getAllNotes } from "../../lib/helpers/parse-notes";
import { formatDateTZ } from "../../lib/helpers/format-date";
import type { GetStaticProps } from "next";
const Notes = ({ notesByYear }: NotesListProps) => (
@ -26,7 +25,7 @@ export const getStaticProps: GetStaticProps = async () => {
const notesByYear: NotesListProps["notesByYear"] = {};
getAllNotes().map((note) => {
const year = formatDateTZ(note.date, "yyyy");
const year = new Date(note.date).getUTCFullYear();
(notesByYear[year] || (notesByYear[year] = [])).push(note);
});

View File

@ -1,6 +1,8 @@
import { useEffect, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import dynamic from "next/dynamic";
import { NextSeo } from "next-seo";
import Terminal from "../components/Terminal";
import { styled } from "../lib/styles/stitches.config";
// obviously, an interactive VNC display will not work even a little bit server-side
@ -56,7 +58,11 @@ const Y2K = () => {
/>
<Wallpaper style={{ backgroundImage: wallpaperUrl ? `url(${wallpaperUrl})` : "" }}>
<VNC server={SOCKET_PROXY} />
<ErrorBoundary
fallback={<Terminal>Oh dear, it looks like something's gone VERY wrong. Sorry about that!</Terminal>}
>
<VNC server={SOCKET_PROXY} />
</ErrorBoundary>
</Wallpaper>
</>
);

View File

@ -982,7 +982,7 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
@ -5105,6 +5105,13 @@ react-dom@18.0.0:
loose-envify "^1.1.0"
scheduler "^0.21.0"
react-error-boundary@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
dependencies:
"@babel/runtime" "^7.12.5"
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"