Published
Edited
Feb 22, 2021
Importers
1 star
Insert cell
Insert cell
Insert cell
tools = FileAttachment("categorized-tools.json").json()
Insert cell
Insert cell
tool = tools.find(d => d.name === toolName);
Insert cell
Insert cell
Insert cell
categorizedTools = {
if (!(categorization instanceof Map)) {
return "are to be stored as JSON, after successfull categorization";
}
return tools.map(d => {
return {
canonicalUrl: d.canonicalUrl,
categorySlug: categorization.get(d.slug),
description: d.description,
id: d.id,
imageUrl: d.imageUrl,
name: d.name,
ossRepo: d.ossRepo,
slug: d.slug,
title: d.title,
websiteUrl: d.websiteUrl
}
});
}
Insert cell
categorization = {
return "is to be run manually"; // comment out to run
const toolSlugs = tools.map(d => d.slug);
return categorize(toolSlugs);
}
Insert cell
async function* categorize(slugs) {
yield html`<progress value="0" max="${slugs.length}"></progress>`;
const categorization = new Map();
for (let i = 0; i < slugs.length; i++) {
const slug = slugs[i];
const categorySlug = await fetchCategorySlug(slug);
categorization.set(slug, categorySlug);
yield html`<progress value="${i}" max="${slugs.length}"></progress>`;
// 75 request tokens per minute from the proxy, 4500 requests per hour
await Promises.delay(1000);
}
yield categorization;
}
Insert cell
fetchCategorySlug = async (toolSlug) => {
const proxyUrl = "https://nikita-sharov.herokuapp.com/";
const url = `${proxyUrl}https://stackshare.io/${toolSlug}`;
const document = await fetch(url)
.then(response => response.text())
.then(text => (new DOMParser).parseFromString(text, "text/html"));
const link = document.querySelector("a.css-ld8qhm:nth-child(7)");
if (link) {
return link.getAttribute("href");
}
return undefined;
}
Insert cell
Insert cell
toolLiteral = (tool) => {
return `
<div class="tool">
${categoriesLiteral(tool)}
<div class="header">
<div class="hero">
${logoLiteral(tool)}
${nameplateLiteral(tool)}
</div>
${websiteLiteral(tool)}
</div>
${descriptionLiteral(tool)}
</div>`;
}
Insert cell
categoriesLiteral = (tool) => {
const path = getCategoryPath(tool.categorySlug);
path[0].name = "Home";
const links = path.map(d => `<a class="category" href="https://stackshare.io/${d.slug}">${d.name}</a>`);
return `
<div class="breadcrumbs">
${links.join(`<span class="separator">/</span>`)}
</div>
`
}
Insert cell
getCategoryPath = (categorySlug) => {
const root = d3.hierarchy(categories);
const target = root.find(d => d.data.slug === categorySlug);
return root.path(target).map(d => d.data);
}
Insert cell
logoLiteral = (tool) => {
const stackshareUrl = tool.canonicalUrl ?? `https://stackshare.io/${tool.slug}`;
// StackShare is denying access to several hosted images (URLs returned by the API)
const fallbackLogoUrl = "https://img.stackshare.io/no-img-open-source.png";
return `
<a class="stackshare-link" href="${stackshareUrl}" target="_blank" title="${tool.name} on StackShare">
<img class="logo" src="${tool.imageUrl}" onerror="this.src='${fallbackLogoUrl}'">
</a>`;
}
Insert cell
nameplateLiteral = (tool) => {
return `
<div class="nameplate">
<div class="name-block">
${nameLiteral(tool)}
${repoLiteral(tool)}
</div>
${titleLiteral(tool)}
</div>`;
}
Insert cell
nameLiteral = (tool) => `<div class="h1">${tool.name}</div>`
Insert cell
repoLiteral = (tool) => {
if (!tool.ossRepo) {
return "";
}
return `<a href="${tool.ossRepo}" target="_blank" title="${tool.name} repo">${icons.gitFork}</a>`;
}
Insert cell
titleLiteral = (tool) => `<div class="title">${tool.title}</div>`
Insert cell
websiteLiteral = (tool) => {
if (!tool.websiteUrl) {
return "";
}
try {
const url = new URL(tool.websiteUrl);
const text = url.hostname.replace("www.", "")
return `<a class="website" href="${url.href}" target="_blank">${icons.externalLink}${text}</a>`;
} catch {
return "";
}
}
Insert cell
descriptionLiteral = (tool) => {
let description = "";
if (tool.description) {
description += `<div class="p">${tool.description}</div>`;
}
description += `<div class="p">${categoryLiteral(tool)}</div>`;
return `
<div class="nav">
<a class="nav-link active" href="#">
${icons.textFile}
<div class="nav-item-name">Description</div>
</a>
</div>
<section>
<div class="h2">What is ${tool.name}?</div>
<div class="description">${description}</div>
<section>`;
}
Insert cell
categoryLiteral = (tool) => {
const category = getCategory(tool.categorySlug);
return `${tool.name} is a tool in the <strong>${category.name}</strong> category of a tech stack.`;
}
Insert cell
getCategory = (categorySlug) => d3.hierarchy(categories).find(d => d.data.slug === categorySlug).data;
Insert cell
Insert cell
d3 = require("d3-hierarchy@2")
Insert cell
Insert cell
icons = ({
gitFork: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="22" viewBox="0 0 16 22" class="git-fork-icon">
<path fill-opacity=".35" d="M12.8 0c-1.776 0-3.2 1.399-3.2 3.143 0 1.147.656 2.168 1.6 2.703v2.011L8 11 4.8 7.857V5.846c.944-.535 1.6-1.54 1.6-2.703C6.4 1.399 4.976 0 3.2 0 1.424 0 0 1.399 0 3.143 0 4.29.656 5.31 1.6 5.846v2.797l4.8 4.714v2.797c-.944.535-1.6 1.54-1.6 2.703C4.8 20.601 6.224 22 8 22c1.776 0 3.2-1.399 3.2-3.143 0-1.147-.656-2.168-1.6-2.703v-2.797l4.8-4.714V5.846c.944-.535 1.6-1.54 1.6-2.703C16 1.399 14.576 0 12.8 0zM3.2 5.029c-1.056 0-1.92-.865-1.92-1.886 0-1.022.88-1.886 1.92-1.886s1.92.864 1.92 1.886c0 1.021-.88 1.886-1.92 1.886zM8 20.743c-1.056 0-1.92-.864-1.92-1.886 0-1.021.88-1.886 1.92-1.886s1.92.865 1.92 1.886c0 1.022-.88 1.886-1.92 1.886zm4.8-15.714c-1.056 0-1.92-.865-1.92-1.886 0-1.022.88-1.886 1.92-1.886s1.92.864 1.92 1.886c0 1.021-.88 1.886-1.92 1.886z"></path>
</svg>`,
textFile: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" class="text-file-icon">
<g>
<path stroke-width=".2" d="M20.35 23H8.95a.933.933 0 0 1-.95-.95V6.95c0-.538.412-.95.95-.95h8.677L21.3 9.673V22.05c0 .538-.412.95-.95.95zm.317-13.073l-2.85-2.85v2.09c0 .19.126.316.316.316h2.534v.634h-2.534a.933.933 0 0 1-.95-.95V6.633H8.95c-.19 0-.317.127-.317.317v15.1c0 .19.127.317.317.317h11.4c.19 0 .317-.127.317-.317V9.927z"></path>
<path d="M11.87 18.45h5.23a.5.5 0 1 1 0 1h-5.23a.5.5 0 1 1 0-1zm0-5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 2.55h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1zm0-5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1 0-1z"></path>
</g>
</svg>`,
externalLink: `<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11" class="external-link-icon">
<g fill="#49A8F9" fill-rule="evenodd">
<path d="M10.5 0h-4a.5.5 0 0 0 0 1h2.793L4.146 6.146a.5.5 0 1 0 .708.708L10 1.707V4.5a.5.5 0 0 0 1 0v-4a.5.5 0 0 0-.5-.5"></path>
<path d="M8.5 5a.5.5 0 0 0-.5.5V10H1V3h4.5a.5.5 0 0 0 0-1h-5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-5a.5.5 0 0 0-.5-.5"></path>
</g>
</svg>`
})
Insert cell
html`<style>
.tool {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
border-top: 1px solid #e1e1e1;
margin: 17px 0;
}

.breadcrumbs {
margin-top: 10px;
font-size: 13px;
line-height: 22.1px;
}

.breadcrumbs,
.category[href] {
color: rgb(194, 194, 194);
}

.category[href]:hover {
color: rgb(6, 141, 254);
text-decoration: none;
}

.separator {
padding: 0 4px;
}

.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-top: 20px;
}

.hero {
display: flex;
align-items: center;
}

.nameplate {
max-width: 500px;
}

.logo {
height: 110px;
width: 110px;
margin: 0 25px 10px 0;
border-radius: 4px;
border: 1px solid rgb(225, 225, 225);
}

.name-block {
display: flex;
}

.h1,
.h2 {
font-weight: 600;
color: rgb(51, 51, 51);
}

.h1 {
font-size: 25px;
line-height: 1;
}

.git-fork-icon {
margin: 5px 0 0 5px;
height: 15px;
}

.website[href] {
display: flex;
align-items: center;
align-self: flex-start;
font-size: 13px;
line-height: 1.7;
color: rgb(112, 112, 112);
}

.website[href]:hover {
text-decoration: none;
}

.external-link-icon {
margin-right: 6px;
}

.nav {
display: flex;
margin: 15px 0 28px 0;
border-bottom: 2px solid #e1e1e1;
height: 82px;
}

.nav-link {
display: flex;
margin-right: 20px;
margin-bottom: -2px;
border-bottom: 2px solid rgb(6, 141, 254);
width: 82px;
align-items: center;
flex-direction: column;
}

.nav-link[href]:hover {
text-decoration: none;
}

.nav-item-name {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans;
font-weight: 400;
line-height: 1.7;
color: rgb(6, 141, 254);
margin-top: 4px;
font-size: 14px;
}

.text-file-icon {
fill: rgb(6, 141, 254);
stroke: rgb(6, 141, 254);
height: 34px;
}

.h2 {
font-size: 18px;
line-height: 1.7;
}

.title,
.description {
font-weight: 400;
color: rgb(112, 112, 112);
}

.title {
font-size: 16px;
line-height: 1.7;
padding: 5px 0;
max-width: 500px
}

.description {
font-size: 13px;
line-height: 1.69;
margin-top: 8px;
max-width: 860px;
padding-bottom: 28px;
border-bottom: 1px solid #e1e1e1;
}

.p:not(:first-child) {
margin-top: 8px;
}
</style>`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more