Skip to content

Commit ec4d1ad

Browse files
KristjanESPERANTOCopilot
andcommitted
feat(website): surface per-module check results in dialog
Closes #91 Co-authored-by: Copilot <copilot@github.com>
1 parent 5dafe79 commit ec4d1ad

8 files changed

Lines changed: 311 additions & 128 deletions

File tree

eslint.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ export default defineConfig([
110110
"max-depth": ["warn", 6]
111111
}
112112
},
113+
{
114+
files: ["website/*.js"],
115+
languageOptions: {
116+
sourceType: "module"
117+
}
118+
},
113119
{ files: ["**/*.json"], ignores: ["package.json", "package-lock.json"], plugins: { json }, extends: ["json/recommended"], language: "json/json" },
114120
{ files: ["package.json"], plugins: { packageJson }, extends: ["packageJson/recommended"], rules: { "package-json/sort-collections": "off", "package-json/require-files": "off", "package-json/require-sideEffects": "off" } },
115121
{ files: ["**/*.md"], plugins: { markdown }, language: "markdown/gfm", extends: ["markdown/recommended"] }

scripts/shared/module-catalogue-output.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,11 @@ interface PreviousStatsPayload {
8282

8383
export interface PublishedOutputResult {
8484
changeSummary: ChangeSummary;
85+
issuesJsonPath: string;
8586
modulesJsonPath: string;
8687
modulesMinPath: string;
8788
outputPaths: {
89+
issuesJsonPath: string;
8890
modulesJsonPath: string;
8991
modulesMinPath: string;
9092
statsPath: string;
@@ -374,6 +376,7 @@ export async function writePublishedCatalogueOutputs(
374376
projectRoot: string
375377
): Promise<PublishedOutputResult> {
376378
const normalizedProcessedModules = processedModules as ProcessedModule[];
379+
const issuesJsonPath = resolve(projectRoot, "website/data/issues.json");
377380
const modulesJsonPath = resolve(projectRoot, "website/data/modules.json");
378381
const modulesMinPath = resolve(projectRoot, "website/data/modules.min.json");
379382
const statsPath = resolve(projectRoot, "website/data/stats.json");
@@ -389,15 +392,17 @@ export async function writePublishedCatalogueOutputs(
389392
const comparableFinalModules = normalizedProcessedModules.map(module => toFinalModule(module, comparisonTimestamp));
390393
const changeSummary = buildModuleDiffSummary(previousModules, comparableFinalModules);
391394

392-
const outputsAlreadyPresent = await allFilesExist([modulesJsonPath, modulesMinPath, statsPath]);
395+
const outputsAlreadyPresent = await allFilesExist([issuesJsonPath, modulesJsonPath, modulesMinPath, statsPath]);
393396
const shouldSkipWrites = !changeSummary.hasChanges && outputsAlreadyPresent;
394397

395398
if (shouldSkipWrites) {
396399
return {
397400
changeSummary,
401+
issuesJsonPath,
398402
modulesJsonPath,
399403
modulesMinPath,
400404
outputPaths: {
405+
issuesJsonPath,
401406
modulesJsonPath,
402407
modulesMinPath,
403408
statsPath
@@ -414,15 +419,25 @@ export async function writePublishedCatalogueOutputs(
414419
: comparableFinalModules;
415420
const stats = buildStats(normalizedProcessedModules, finalModules, lastUpdate);
416421

422+
const issuesMap: Record<string, string[]> = {};
423+
for (const module of normalizedProcessedModules) {
424+
if (typeof module.id === "string" && Array.isArray(module.issues) && module.issues.length > 0) {
425+
issuesMap[module.id] = module.issues as string[];
426+
}
427+
}
428+
429+
await writeFile(issuesJsonPath, stringifyDeterministic(issuesMap, 0), "utf-8");
417430
await writeFile(modulesJsonPath, stringifyDeterministic({ modules: finalModules }), "utf-8");
418431
await writeFile(modulesMinPath, stringifyDeterministic({ modules: finalModules }, 0), "utf-8");
419432
await writeFile(statsPath, stringifyDeterministic(stats), "utf-8");
420433

421434
return {
422435
changeSummary,
436+
issuesJsonPath,
423437
modulesJsonPath,
424438
modulesMinPath,
425439
outputPaths: {
440+
issuesJsonPath,
426441
modulesJsonPath,
427442
modulesMinPath,
428443
statsPath

website/card.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { openHintsDialog } from "./hints-dialog.js";
2+
3+
const cardTemplate = document.getElementById("card-template");
4+
5+
export function createCard(moduleData, { filterByMaintainer, filterByTag }) {
6+
const card = document.importNode(cardTemplate.content, true);
7+
8+
if (moduleData.skipped) {
9+
card.querySelector(".card").classList.add("skipped");
10+
card.querySelector(".name").textContent = moduleData.name || "Unknown Module";
11+
card.querySelector(".name").href = moduleData.url || "#";
12+
card.querySelector(".description").innerHTML = `<span style='color:red;font-weight:bold'>Error: Module could not be loaded.</span><br>${moduleData.error || "Unknown Error"}`;
13+
card.querySelector(".maintainer").textContent = moduleData.maintainer || "?";
14+
[".stars", ".tags", ".img-container", ".info", ".outdated-note"].forEach((selector) => {
15+
const element = card.querySelector(selector);
16+
if (element) {
17+
element.remove();
18+
}
19+
});
20+
return card;
21+
}
22+
23+
card.querySelector(".name").href = moduleData.url;
24+
card.querySelector(".name").textContent = moduleData.name;
25+
26+
const maintainerContainer = card.querySelector(".maintainer");
27+
maintainerContainer.textContent = `${moduleData.maintainer}`;
28+
maintainerContainer.addEventListener("click", () => {
29+
filterByMaintainer(moduleData.maintainer);
30+
});
31+
32+
if (typeof moduleData.stars === "undefined") {
33+
card.querySelector(".stars").remove();
34+
}
35+
else {
36+
card.querySelector(".stars").textContent = `${moduleData.stars} stars`;
37+
}
38+
39+
if (moduleData.tags) {
40+
moduleData.tags.forEach((tag) => {
41+
const tagElement = document.createElement("div");
42+
tagElement.setAttribute("data-tag", tag);
43+
tagElement.textContent = tag;
44+
45+
tagElement.addEventListener("click", () => {
46+
filterByTag(tag);
47+
});
48+
49+
card.querySelector(".tags").appendChild(tagElement);
50+
});
51+
}
52+
else {
53+
card.querySelector(".tags").remove();
54+
}
55+
56+
card.querySelector(".description").innerHTML = moduleData.description;
57+
58+
if (moduleData.image) {
59+
const imagePath = `./images/${moduleData.image}`;
60+
const image = card.querySelector(".img-container img");
61+
image.src = imagePath;
62+
image.alt = `${moduleData.name} image`;
63+
64+
const overlay = image.nextElementSibling;
65+
image.onclick = () => {
66+
// Move overlay to body to escape card's overflow/containment
67+
document.body.appendChild(overlay);
68+
overlay.style.display = "flex";
69+
overlay.getElementsByTagName("img")[0].src = image.src;
70+
};
71+
72+
overlay.onclick = () => {
73+
overlay.style.display = "none";
74+
// Move overlay back to its original position
75+
image.parentElement.appendChild(overlay);
76+
};
77+
}
78+
else {
79+
card.querySelector(".img-container").remove();
80+
}
81+
82+
const license = card.querySelector(".info .container.license .text");
83+
license.href = `${moduleData.url}`;
84+
if (moduleData.license) {
85+
license.textContent = ${moduleData.license}`;
86+
}
87+
else {
88+
license.style.color = "red";
89+
license.textContent = "unknown";
90+
}
91+
92+
if (moduleData.lastCommit) {
93+
const commit = card.querySelector(".info .container.commit .text");
94+
commit.href = `${moduleData.url}/commits/`;
95+
commit.textContent = `${moduleData.lastCommit.split("T")[0]}`;
96+
}
97+
else {
98+
card.querySelector(".info .container.commit").remove();
99+
}
100+
101+
if (moduleData.issues) {
102+
const url = `result.html#${moduleData.name}-by-${moduleData.maintainer.replaceAll(" ", "-").replaceAll("&", "").replaceAll("/", "")}`;
103+
const issuesLink = card.querySelector(".info .container.issues .text");
104+
issuesLink.href = url;
105+
issuesLink.addEventListener("click", (event) => {
106+
event.preventDefault();
107+
openHintsDialog(moduleData, url);
108+
});
109+
}
110+
else {
111+
card.querySelector(".info .container.issues").remove();
112+
}
113+
114+
if (moduleData.outdated) {
115+
card.querySelector(".card").classList.add("outdated");
116+
card.querySelector(".outdated-note").innerHTML = moduleData.outdated;
117+
}
118+
else {
119+
card.querySelector(".outdated-note").remove();
120+
}
121+
122+
return card;
123+
}

website/data/issues.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

website/hints-dialog.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
let issuesCachePromise = null;
2+
3+
function fetchIssues() {
4+
if (!issuesCachePromise) {
5+
issuesCachePromise = fetch("data/issues.json")
6+
.then(res => res.json())
7+
.catch(() => ({}));
8+
}
9+
return issuesCachePromise;
10+
}
11+
12+
function formatIssueText(text) {
13+
return text
14+
.replace(/`([^`]+)`/gu, "<code>$1</code>")
15+
.replace(/\[([^\]]+)\]\(([^)]+)\)/gu, "<a href=\"$2\" target=\"_blank\" rel=\"noopener\">$1</a>")
16+
.replace(/\n/gu, "<br>");
17+
}
18+
19+
function renderIssuesList(issues) {
20+
if (!issues?.length) {
21+
return "<p>No issues found for this module.</p>";
22+
}
23+
24+
const items = issues.map(text => `<li>${formatIssueText(text)}</li>`);
25+
return `<ol>${items.join("")}</ol>`;
26+
}
27+
28+
export async function openHintsDialog(moduleData, fullUrl) {
29+
const dialog = document.getElementById("hints-dialog");
30+
const title = document.getElementById("hints-dialog-title");
31+
const body = document.getElementById("hints-dialog-body");
32+
const fullLink = document.getElementById("hints-dialog-fulllink");
33+
34+
title.textContent = `${moduleData.name} by ${moduleData.maintainer}`;
35+
body.innerHTML = "<p>Loading…</p>";
36+
fullLink.href = fullUrl;
37+
dialog.showModal();
38+
39+
const issuesMap = await fetchIssues();
40+
body.innerHTML = renderIssuesList(issuesMap[moduleData.id]);
41+
}
42+
43+
document.getElementById("hints-dialog-close").addEventListener("click", () => {
44+
document.getElementById("hints-dialog").close();
45+
});
46+
47+
document.getElementById("hints-dialog").addEventListener("click", (event) => {
48+
if (event.target === event.currentTarget) {
49+
event.currentTarget.close();
50+
}
51+
});

website/index.css

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,96 @@ main {
402402
}
403403
}
404404

405+
/* ------------------------------- HINTS DIALOG */
406+
#hints-dialog {
407+
border: none;
408+
border-radius: 10px;
409+
padding: 0;
410+
max-width: min(600px, 90vw);
411+
max-height: 80vh;
412+
background-color: var(--color-card-background);
413+
color: var(--color-background);
414+
box-shadow: 0 8px 32px rgb(0 0 0 / 40%);
415+
overflow: hidden;
416+
display: flex;
417+
flex-direction: column;
418+
}
419+
420+
#hints-dialog::backdrop {
421+
background: rgb(0 0 0 / 50%);
422+
}
423+
424+
#hints-dialog-header {
425+
display: flex;
426+
justify-content: space-between;
427+
align-items: center;
428+
padding: 0.8em 1em;
429+
background-color: var(--color-background);
430+
color: var(--color-foreground);
431+
gap: 1em;
432+
}
433+
434+
#hints-dialog-title {
435+
margin: 0;
436+
font-size: 1rem;
437+
font-weight: 600;
438+
flex: 1;
439+
overflow: hidden;
440+
text-overflow: ellipsis;
441+
white-space: nowrap;
442+
}
443+
444+
#hints-dialog-close {
445+
background: none;
446+
border: none;
447+
color: var(--color-foreground);
448+
cursor: pointer;
449+
font-size: 1.1rem;
450+
padding: 0.1em 0.3em;
451+
border-radius: 4px;
452+
line-height: 1;
453+
flex-shrink: 0;
454+
}
455+
456+
#hints-dialog-close:hover {
457+
background-color: #fff3;
458+
}
459+
460+
#hints-dialog-body {
461+
padding: 0.8em 1.2em;
462+
overflow-y: auto;
463+
flex: 1;
464+
}
465+
466+
#hints-dialog-body ol {
467+
margin: 0;
468+
padding-left: 1.4em;
469+
}
470+
471+
#hints-dialog-body li {
472+
margin-bottom: 0.5em;
473+
font-size: 0.9rem;
474+
line-height: 1.4;
475+
}
476+
477+
#hints-dialog-body code {
478+
background-color: #0001;
479+
border-radius: 3px;
480+
padding: 0.1em 0.3em;
481+
font-size: 0.85em;
482+
}
483+
484+
#hints-dialog-footer {
485+
padding: 0.6em 1.2em;
486+
border-top: 1px solid #0002;
487+
text-align: right;
488+
font-size: 0.85rem;
489+
}
490+
491+
#hints-dialog-footer a {
492+
color: var(--color-selected);
493+
}
494+
405495
[data-tag].selected {
406496
margin: 0 0.4em;
407497
}

0 commit comments

Comments
 (0)