1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-29 20:06:00 -04:00

use preact for common components across site (#663)

* convert GitHub cards grid from lit-html to preact

* give hit counter the preact treatment

* extract loading spinner component to a shared location

* move *some* loading spinner styles to its JSX

* Update .percy.yml

* pick up images in JS w/ webpack

* pull star/fork icons straight from @primer/octicons

* a bit of cleanup

* check `typeof window !== "undefined"` before rendering

* bump misc. deps

* silence missing license warnings for preact-hooks and preact-compat

* add source-map-loader

* Update loading.js
This commit is contained in:
2021-11-24 13:51:29 -05:00
committed by GitHub
parent 9b3ae0f62a
commit b755b66d19
20 changed files with 541 additions and 315 deletions
+43
View File
@@ -0,0 +1,43 @@
import { h } from "preact";
const Loading = (props) => {
// allow a custom number of pulsing boxes (defaults to 3)
const boxes = props.boxes || 3;
// each individual box's animation has a staggered start in corresponding order
const animationTiming = props.timing || 0.1; // seconds
// each box is just an empty div
const divs = [];
for (let i = 0; i < boxes; i++) {
divs.push(
<div
style={{
// width of each box correlates with number of boxes (with a little padding)
width: `${props.width / (boxes + 1)}px`,
height: "100%",
display: "inline-block",
// see assets/sass/components/_animation.scss:
animation: "loading 1.5s infinite ease-in-out both",
"animation-delay": `${i * animationTiming}s`,
}}
/>
);
}
return (
<div
class="loading"
style={{
width: `${props.width}px`,
height: `${props.width / 2}px`,
display: "inline-block",
"text-align": "center",
...props.style,
}}
>
{divs}
</div>
);
};
export default Loading;
+1 -1
View File
@@ -13,7 +13,7 @@ disableTransitionCSSHack.sheet.insertRule(`
initDarkMode({
toggle: document.querySelector(".dark-mode-toggle"),
onInit: function (t) {
onInit: (t) => {
// make toggle visible now that we know JS is enabled
t.style.display = "block";
+31 -29
View File
@@ -1,14 +1,42 @@
import { h, render } from "preact";
import { useState, useEffect } from "preact/hooks";
import fetch from "cross-fetch";
import canonicalUrl from "get-canonical-url";
// shared react components:
import Loading from "./components/loading.js";
// API endpoint
const HITS_ENDPOINT = "/api/hits/";
const Counter = (props) => {
const [hits, setHits] = useState();
// start fetching hits from API once slug is set
useEffect(() => {
fetch(`${HITS_ENDPOINT}?slug=${encodeURIComponent(props.slug)}`)
.then((response) => response.json())
.then((data) => setHits(data.hits || 0));
}, [props.slug]);
// show spinning loading indicator if data isn't fetched yet
if (!hits) {
return <Loading boxes={3} width={20} />;
}
// we have data!
return (
<span title={`${hits.toLocaleString("en-US")} ${hits === 1 ? "view" : "views"}`}>
{hits.toLocaleString("en-US")}
</span>
);
};
// don't continue if there isn't a span#meta-hits element on this page
const wrapper = document.querySelector("div#meta-hits");
const wrapper = document.querySelector("div#meta-hits-counter");
// page must have both span#meta-hits and canonical URL to enter
if (wrapper) {
if (typeof window !== "undefined" && wrapper) {
// use <link rel="canonical"> to deduce a consistent identifier for this page
const canonical = canonicalUrl({
normalize: true,
@@ -19,34 +47,8 @@ if (wrapper) {
},
});
// javascript is enabled so show the loading indicator
wrapper.style.display = "inline-flex";
// get path and strip beginning and ending forward slash
const slug = new URL(canonical).pathname.replace(/^\/|\/$/g, "");
fetch(`${HITS_ENDPOINT}?slug=${encodeURIComponent(slug)}`)
.then((response) => response.json())
.then((data) => {
// pretty number and units
const hitsComma = data.hits.toLocaleString("en-US");
const hitsPlural = data.hits === 1 ? "view" : "views";
wrapper.title = `${hitsComma} ${hitsPlural}`;
// finally inject the hits...
const counter = document.querySelector("span#meta-hits-counter");
if (counter) {
counter.append(hitsComma);
}
// ...and hide the loading spinner
const spinner = document.querySelector("div#meta-hits-loading");
if (spinner) {
spinner.remove();
}
})
.catch(() => {
// something went horribly wrong, initiate coverup
wrapper.remove();
});
render(<Counter slug={slug} />, wrapper);
}
+85 -78
View File
@@ -1,93 +1,100 @@
import { h, render, Fragment } from "preact";
import { useState, useEffect } from "preact/hooks";
import fetch from "cross-fetch";
import { render } from "lit-html";
import { html } from "lit-html/static.js";
import { ifDefined } from "lit-html/directives/if-defined.js";
import dayjs from "dayjs";
import dayjsLocalizedFormat from "dayjs/plugin/localizedFormat.js";
import dayjsRelativeTime from "dayjs/plugin/relativeTime.js";
import { parse as parseEmoji } from "imagemoji";
// shared react components:
import { StarIcon, RepoForkedIcon } from "@primer/octicons-react";
import Loading from "./components/loading.js";
// API endpoint (sort by stars, limit to 12)
const PROJECTS_ENDPOINT = "/api/projects/?top&limit=12";
// don't continue if there isn't a span#meta-hits element on this page
// TODO: be better.
const RepositoryGrid = () => {
const [repos, setRepos] = useState([]);
// start fetching repos from API immediately
useEffect(() => {
fetch(PROJECTS_ENDPOINT)
.then((response) => response.json())
.then((data) => setRepos(data || []));
}, []);
// show spinning loading indicator if data isn't fetched yet
if (repos.length === 0) {
return <Loading boxes={3} width={40} style={{ margin: "0.7em auto" }} />;
}
// we have data!
return (
<Fragment>
{repos.map((repo) => (
// eslint-disable-next-line react/jsx-key
<RepositoryCard {...repo} />
))}
</Fragment>
);
};
const RepositoryCard = (repo) => (
<div class="github-card">
<a class="repo-name" href={repo.url} target="_blank" rel="noopener noreferrer">
{repo.name}
</a>
{repo.description && (
<p
class="repo-description"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: parseEmoji(repo.description, (icon) => `/assets/emoji/${icon}.svg`) }}
/>
)}
<div class="repo-meta">
{repo.language && (
<div class="repo-meta-item">
<span class="repo-language-color" style={{ "background-color": repo.language.color }} />
<span>{repo.language.name}</span>
</div>
)}
{repo.stars > 0 && (
<div
class="repo-meta-item"
title={`${repo.stars.toLocaleString("en-US")} ${repo.stars === 1 ? "star" : "stars"}`}
>
<StarIcon size={16} fill="currentColor" />
<span>{repo.stars.toLocaleString("en-US")}</span>
</div>
)}
{repo.forks > 0 && (
<div
class="repo-meta-item"
title={`${repo.forks.toLocaleString("en-US")} ${repo.forks === 1 ? "fork" : "forks"}`}
>
<RepoForkedIcon size={16} fill="currentColor" />
<span>{repo.forks.toLocaleString("en-US")}</span>
</div>
)}
<div class="repo-meta-item" title={dayjs(repo.updatedAt).format("lll Z")}>
<span>Updated {dayjs(repo.updatedAt).fromNow()}</span>
</div>
</div>
</div>
);
// detect if these cards are wanted on this page (only /projects)
const wrapper = document.querySelector("div#github-cards");
if (wrapper) {
if (typeof window !== "undefined" && wrapper) {
// dayjs plugins: https://day.js.org/docs/en/plugin/loading-into-nodejs
dayjs.extend(dayjsLocalizedFormat);
dayjs.extend(dayjsRelativeTime);
// this is a total sh*tshow, but safer than setting one big string via innerHTML :)
// TODO: consider making this a real LitElement?
const template = (repo) => html`
<a class="repo-name" href="${repo.url}" target="_blank" rel="noopener">${repo.name}</a>
${repo.description ? html`<p class="repo-description">${repo.description}</p>` : null}
<div class="repo-meta">
${repo.language
? html`<div class="repo-meta-item">
<span class="repo-language-color" style="background-color: ${ifDefined(repo.language.color)}"></span>
<span>${repo.language.name}</span>
</div>`
: null}
${repo.stars > 0
? html`<div
class="repo-meta-item"
title="${repo.stars.toLocaleString("en-US")} ${repo.stars === 1 ? "star" : "stars"}"
>
<svg viewBox="0 0 16 16" height="16" width="16">
<path
fill-rule="evenodd"
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"
></path>
</svg>
<span>${repo.stars.toLocaleString("en-US")}</span>
</div>`
: null}
${repo.forks > 0
? html`<div
class="repo-meta-item"
title="${repo.forks.toLocaleString("en-US")} ${repo.forks === 1 ? "fork" : "forks"}"
>
<svg viewBox="0 0 16 16" height="16" width="16">
<path
fill-rule="evenodd"
d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"
></path>
</svg>
<span>${repo.forks.toLocaleString("en-US")}</span>
</div>`
: null}
<div class="repo-meta-item" title="${dayjs(repo.updatedAt).format("lll Z")}">
<span>Updated ${dayjs(repo.updatedAt).fromNow()}</span>
</div>
</div>
`;
fetch(PROJECTS_ENDPOINT)
.then((response) => response.json())
.then((data) => {
data.forEach((repo) => {
const div = document.createElement("div");
div.classList.add("github-card");
render(template(repo), div);
wrapper.append(div);
});
// we're done, hide the loading spinner
const spinner = document.querySelector("div.loading");
if (spinner) {
spinner.remove();
}
// the repo descriptions were added after the first twemoji parsing
parseEmoji(wrapper, (icon) => `/assets/emoji/${icon}.svg`);
})
.catch(() => {
// something went horribly wrong, initiate coverup
wrapper.remove();
});
render(<RepositoryGrid />, wrapper);
}