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:
parent
831139c132
commit
8a97b706be
@ -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) {}
|
||||
};
|
||||
|
5
lib/helpers/format-number.ts
Normal file
5
lib/helpers/format-number.ts
Normal 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;
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user