mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-06-29 23:45:58 -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:
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user